The previous lesson ended with a diagnosis: BiblioTech's domain can no longer be corrupted from outside, but the public surface of its classes has grown out of control. Loan exposes more than twenty methods, among them a getEmployeeDoNotUse() whose very name confesses that it should not exist. Encapsulation answers the question how do I hide what is inside; what is left is the harder one: what deserves to be outside. That is the territory of abstraction, OOP's fourth pillar and, unlike the other three, not a language mechanism but a design discipline: keeping what is essential to the problem being solved and discarding everything else. A metro map is a masterful abstraction —it lies about distances, ignores geography and yet gets you to your destination better than a scale map does—. This lesson teaches you to design like that: to decide what you expose, not to mix levels inside a method, to document contracts and to recognise both leaky abstractions and the opposite vice, abstracting what nobody asked for.
Contents
- Abstraction as a design discipline
- Abstraction versus encapsulation
- Levels of abstraction and why they must not be mixed
- Refactoring: separating the rule from the presentation
- Design by contract: preconditions, postconditions and invariants
- The vocabulary of the domain
- Separating the what from the how
- Minimal public API and the cost of exposing too much
- Leaky abstractions
- Over-abstraction and YAGNI
- The missing mechanisms: interfaces and abstract classes
- Common Mistakes and Tips
- Exercises
- Abstraction as a design discipline
To abstract is to keep what is essential and discard what is accidental. The key phrase is "for the problem": there is no absolutely correct abstraction, only the one that is correct for a purpose.
The same physical book is abstracted differently depending on who models it:
| System | What is essential | What is discarded |
|---|---|---|
| BiblioTech (loans) | Title, ISBN, availability, term | Weight, page count, cover colour |
| Online shop | Price, stock, cover image, shipping | Loan availability |
| Printing house | Paper weight, ink, format, binding | ISBN, author |
| Removals company | Weight and volume | Absolutely everything else |
None is more "true" than the others. A Book in BiblioTech has no weight because BiblioTech does not care about weight, and adding it "just in case" would pollute the model.
Hence the first question you must ask yourself about any class:
What does this object need to know and do for the problem we are solving? Everything else is surplus.
- Abstraction versus encapsulation
The two concepts are constantly confused because they collaborate, but they answer different questions:
| Abstraction | Encapsulation | |
|---|---|---|
| Question it answers | What do I expose? | How do I hide it? |
| Nature | A design decision | A language mechanism |
| Moment | When modelling, before writing code | When implementing |
| Tools | Choosing classes, operations and names | private, protected, getters, business operations |
| It goes wrong when... | You expose what does not matter or hide what does | You leave doors open to the internal state |
| In BiblioTech | Deciding that Loan offers registerReturn(int) |
Making the appliedFine field private |
| Car analogy | The steering wheel and pedals are the abstraction of "driving" | The closed bonnet stops you touching the engine |
You can have one without the other, and both failures are real:
- Good encapsulation, bad abstraction: every field
private, but with thirty validated getters and setters. Nothing gets corrupted, but the class means nothing; it is a spreadsheet with Java syntax. - Good abstraction, bad encapsulation: a magnificent public API —
lend(),returnItem(),calculateFine()— alongsidepublicfields that let you bypass it. The design is right and it is not enforced.
You need both. Abstraction decides which doors there are; encapsulation guarantees that there are no others.
- Levels of abstraction and why they must not be mixed
A program has layers of detail, from the concept to the bit:
flowchart TD
A["Level 4: use case
'register the return of a book'"] --> B["Level 3: business rules
calculate delay, apply cap, classify"]
B --> C["Level 2: domain operations
material.returnItem(), employee.registerReturn()"]
C --> D["Level 1: language mechanics
Math.min, printf, loops, arithmetic"]
The rule, known as the single level of abstraction principle:
Inside a single method, every statement should belong to roughly the same level.
Why? Because reading a method that jumps between levels forces you to switch mental frames on every line. A method that says "register the return, and by the way here is the VAT formula and the receipt's column width" cannot be skim-read: it has to be read in full, every time.
This is what mixing looks like, and you will probably recognise the style because it is exactly that of your module 2 main:
// BAD: four levels of abstraction in fourteen lines
public void processReturn(Loan loan, int days) {
System.out.println("========================================"); // level 1
System.out.println(" RETURN RECEIPT - BIBLIOTECH"); // level 1
int late = Math.max(0, days - loan.getMaterial().getLoanDays()); // level 3
double fine = late * loan.getMaterial().getDailyRate(); // level 3
if (fine > 20.0) { // level 3
fine = 20.0;
}
System.out.printf(" %-20s %s%n", "Material:", loan.getMaterialTitle()); // level 1
System.out.printf(" %-20s %d days%n", "Days late:", late); // level 1
System.out.printf(" %-20s %.2f EUR%n", "Fine:", fine); // level 1
loan.getMaterial().returnItem(); // level 2
System.out.println("========================================"); // level 1
}The concrete problems, beyond aesthetics:
- The rule cannot be reused. If another point in the program needs the fine, it has to copy the formula or call this method and put up with the console receipt.
- It cannot be tested. To verify the calculation you have to capture standard output (module 11).
- The channel cannot be changed. If tomorrow the receipt is a web page or a PDF, the business logic has to be rewritten along with the format.
- The €20 cap is duplicated. It already lives in
Material.MAX_FINE, and here it appears as a literal. Two truths for one fact. - It violates the Law of Demeter three times with
loan.getMaterial().getX().
- Refactoring: separating the rule from the presentation
Let us rewrite the previous method, distributing each line to its level.
Level 3, business rules: they are already in the domain. Loan.registerReturn(int) computes the fine, marks the loan, releases the material and discounts the employee (lesson 03-07). Nothing new needs writing.
Level 1, presentation: a separate class, outside the domain.
package com.nexussoftware.bibliotech.presentation;
import com.nexussoftware.bibliotech.domain.Loan;
/** Generates the texts BiblioTech shows on the console. */
public class ConsoleReceipt {
private static final String SEPARATOR = "========================================";
/** Prints the receipt of an already registered return. */
public void printReceipt(Loan loan) {
System.out.println(SEPARATOR);
System.out.println(" RETURN RECEIPT - BIBLIOTECH");
System.out.printf(" %-20s %s%n", "Reference:", loan.getReference());
System.out.printf(" %-20s %s%n", "Material:", loan.getMaterialTitle());
System.out.printf(" %-20s %s%n", "Employee:", loan.getEmployeeName());
System.out.printf(" %-20s %d days%n", "Days late:", loan.calculateDaysLate());
System.out.printf(" %-20s %.2f EUR%n", "Fine:", loan.getAppliedFine());
System.out.printf(" %-20s %s%n", "Severity:", loan.classifySeverity());
System.out.println(SEPARATOR);
}
}Level 4, the use case: it coordinates, it does not calculate or print.
/** Registers the return of a loan and hands the receipt to the employee. */
public void processReturn(Loan loan, int elapsedDays) {
loan.registerReturn(elapsedDays);
receipt.printReceipt(loan);
}Two lines. And each one at its own level.
Compare before and after:
| Criterion | Before | After |
|---|---|---|
| Levels mixed in one method | 4 | 1 |
| Where the fine formula lives | Duplicated in the method | Only in Material |
The literal 20.0 |
Hand-written | MAX_FINE |
| Switching to web output | Rewrite the method | Another presentation class |
| Testing the calculation | By capturing System.out |
By calling calculateFine() |
| Demeter violations | 3 | 0 |
| Lines in the use case | 14 | 2 |
Note the new package, presentation. BiblioTech's structure begins to reflect its abstraction levels:
com.nexussoftware.bibliotech ├── BiblioTechApp.java startup ├── domain/ WHAT the business is and its rules ├── presentation/ HOW it is shown └── service/ (module 5) use cases
This separation is the seed of the layered architecture you will see in full in module 12.
- Design by contract: preconditions, postconditions and invariants
An abstraction is a promise: "call me this way and I will give you this back". The more explicit the promise, the less room for misunderstanding. Design by contract formalises that promise in three parts:
| Element | What it is | Who guarantees it |
|---|---|---|
| Precondition | What must be true before the call | The caller |
| Postcondition | What will be true after the call | The method |
| Invariant | What is always true, before and after | The class |
Java has no syntax for contracts (other languages do), so they are documented with Javadoc, which you already know from module 1:
/**
* Registers the return of the lent material and applies the corresponding fine.
*
* <p><b>Preconditions:</b>
* <ul>
* <li>{@code elapsedDays >= 0}.</li>
* <li>The loan must not have been returned already; if it has, the call has
* no effect and the previously applied fine is returned.</li>
* </ul>
*
* <p><b>Postconditions:</b>
* <ul>
* <li>{@code isReturned()} becomes {@code true}.</li>
* <li>The material becomes available again.</li>
* <li>The employee has one active loan fewer.</li>
* <li>The returned value is between 0 and {@code Material.MAX_FINE}.</li>
* </ul>
*
* <p><b>Class invariant maintained:</b> {@code elapsedDays >= 0} and
* {@code dueDay == loanDay + material.getLoanDays()}.
*
* @param elapsedDays days elapsed since handover, never negative
* @return amount of the applied fine, in euros
*/
public double registerReturn(int elapsedDays) { ... }Advantages of writing it:
- The caller knows what they must satisfy without reading the implementation. That is, literally, the definition of abstraction.
- It forces you, as you write it, to think about the edge cases: what happens if it had already been returned? And if the days are negative? Many contracts reveal design gaps before they exist as bugs.
- The IDE shows it when autocompleting, so the contract travels with the code.
And a practical guide about preconditions: there are two strategies, and it is worth choosing consciously.
| Strategy | How | When |
|---|---|---|
| Defensive | Check and reject (warnings now, exceptions in module 6) | Public API, user input |
| Strict contract | Document the precondition and assume it holds | private methods, internal code |
Checking everything everywhere multiplies the code without adding safety: if a private method is called only by two other methods of the same class that already validated, validating again is noise.
- The vocabulary of the domain
A good abstraction speaks the language of the business. If a Nexus Software librarian read your code over your shoulder, they should recognise the words.
Compare:
// Technical vocabulary: it means nothing to the business
DataManager dm = new DataManager();
dm.process(item, 20);
if (dm.getStatus() == 2) { ... }
// Domain vocabulary
double fine = loan.registerReturn(20);
if (loan.isOverdue()) { ... }The idea of using a single vocabulary shared between the code, the documentation and the conversations with the business is called the ubiquitous language. Its practical rule is simple: if the business says "fine", the code says fine, not penaltyAmount, charge or levy.
BiblioTech's vocabulary, which you have been using since module 1, is exactly this: title, author, isbn, available, daysLate, fine, severity, reference, lend, returnItem. No technical word has crept into the domain, and that is why loan.registerReturn(20) is understandable without opening the class.
- Separating the what from the how
The tangible result of a good abstraction is that the caller stops knowing how things are done.
Walk through what has happened to the fine calculation over the course:
| Moment | Who knows how it is computed |
|---|---|
| Module 1 | main: the formula is written there |
| Module 2 | main: the formula, the cap and the classification, inside a switch |
| Lesson 03-03 | Loan: main calls calculateFine() |
| Lesson 03-05 | Material and its subclasses: each format supplies its rate |
| Lesson 03-07 | Only the domain: main cannot even touch the result |
| Now | main does not even mention the word "fine" except to display it |
The definitive test that the what and the how are separated is this: can you change the implementation without touching the callers? If Nexus Software decides tomorrow that the rate should be progressive —€0.25 for the first seven days and €0.50 from the eighth on—, how many files have to be touched?
// New rule, only in Material
@Override
public double calculateFine(int elapsedDays) {
int late = calculateDaysLate(elapsedDays);
int minorBand = Math.min(late, MINOR_THRESHOLD);
int severeBand = Math.max(0, late - MINOR_THRESHOLD);
double amount = minorBand * getDailyRate()
+ severeBand * getDailyRate() * 2;
return Math.min(amount, MAX_FINE);
}One file. Neither main, nor the receipt, nor Loan finds out. That is what abstraction buys, and it is the reason its cost in indirection is worth paying.
- Minimal public API and the cost of exposing too much
A class's public API is everything others can use: public methods and fields, and its constructors. It is its promise to the outside world.
And that promise has a cost that almost nobody calculates while writing the code:
| Cost | Explanation |
|---|---|
| Compatibility | Every public method is a commitment: if you delete it or change its signature, you break all its users |
| Cognitive load | A class with 25 public methods forces you to read 25 names to know which one to use |
| Error surface | Every public method is a route by which someone can misuse the class |
| Lost freedom | You cannot change what you have promised; only the private part is free |
The golden rule:
It is easy to add a public method later; it is extremely expensive to remove one. When in doubt, leave it
private.
Applied to Loan, let us review its current API with a severe eye:
| Member | Public? | Reason |
|---|---|---|
registerReturn(int) |
Yes | It is the use case |
calculateFine(), calculateDaysLate(), classifySeverity() |
Yes | Queries the presentation needs |
getReference(), getMaterialTitle(), getEmployeeName() |
Yes | They identify the loan on screen |
isReturned(), isOverdue(), getDueDay() |
Yes | State queried in listings |
getAppliedFine() |
Yes | A fact recorded after the return |
setElapsedDays(int) |
No → private |
It is an internal detail of registerReturn |
getEmployeeDoNotUse() |
No → remove | It allows mutating the employee through the back door |
getMaterial() |
Debatable | Needed in some listings; can be replaced by delegations |
getLoanDay(), daysRemaining() |
Yes | They are shown in the summary |
employeeAtLimit() |
Yes | A legitimate business query |
canBeLent(Material, Employee) |
Yes | Pre-check for the use case |
With that pruning, Loan goes from more than twenty public members to fourteen, and none of them lets you unbalance the system. Exercise 1 asks you to take it all the way.
- Leaky abstractions
A leaky abstraction is one that promises to hide a detail but forces you to know it anyway. The law stated by Joel Spolsky says, with some black humour, that all non-trivial abstractions leak: the point is not to avoid it entirely, but not to make it worse.
Examples you have already seen or will see:
| Leak | How it shows |
|---|---|
Loan.getMaterial() returns the mutable object |
Whoever receives it can call returnItem() bypassing the loan |
A getIncidents() with no defensive copy |
The caller discovers their array is the internal one |
A method called save() that sometimes fails on the network |
The "save" abstraction cannot hide that there is a network underneath (module 9) |
String.substring and memory usage |
In old Java versions, a substring retained the whole original array |
How to reduce leaks:
- Return immutable types or copies instead of internal state.
- Do not expose implementation types in the public signature. If tomorrow you want to change an array for a list, your signature should not force you to break anyone.
- Document what leaks when you cannot avoid it. A documented leak is a contract; a silent leak is a trap.
- Over-abstraction and YAGNI
The opposite vice exists too, and in the hands of somebody who has just discovered OOP it is the more frequent one: abstracting what nobody asked for.
Symptoms of over-abstraction:
- Five-level hierarchies for three concrete cases.
- Classes called
AbstractBaseGenericProcessorFactory. - Configuration parameters that are always the same value.
- Extension points "in case one day" that are never used.
- One interface per class, with a single implementation.
The principle that contains it is called YAGNI: You Aren't Gonna Need It. Formulated as a rule:
Do not add flexibility until you have two real cases that require it. With one, it is guesswork.
Applied to BiblioTech, an honest contrast:
| Abstraction | Justified? | Why |
|---|---|---|
Material with three subclasses |
Yes | There are three real formats with different rules |
Separating domain from presentation |
Yes | Module 12 will bring a web interface |
An overridable getLoanDays() |
Yes | Each format has its own term, verified |
A PhysicalMaterial / DigitalMaterial hierarchy |
Not yet | There is no digital material in the catalog |
| A rate system configurable from a file | No | There is one rate per format and it does not change |
A Lendable interface with a single implementer |
No | It contributes nothing until there is a second |
And a warning about balance: over-abstracting is an expensive but visible mistake; under-abstracting produces module 2's two-hundred-line main, which is expensive too. The professional answer is not to pick an extreme, but to refactor when the second case appears. That is exactly what you have done: Material did not exist until magazines and DVDs turned up.
- The missing mechanisms: interfaces and abstract classes
This whole lesson has talked about abstraction as a discipline: what to model, what to expose, how to separate levels. But Java also offers two language mechanisms designed specifically to materialise it, and both arrive in the next module:
| Mechanism | What it contributes | Lesson |
|---|---|---|
| Interfaces | They declare what can be done without saying how; a class can implement several | 04-01 |
| Abstract classes | Classes that cannot be instantiated and that can declare bodiless methods, forcing subclasses to implement them | 04-02 |
Two concrete shortcomings of the current design that those mechanisms will solve:
First: Material can be instantiated. Today nothing stops you writing new Material("Something", "REF-1", true), and that means nothing: the library has books, magazines and DVDs, not "generic materials". An abstract class forbids it at compiler level.
Second: getType() returns "Material" by default. If somebody creates a new subclass and forgets to override it, "Material" will appear in the listings without anyone detecting it. An abstract method turns that omission into a compile error:
// A preview of lesson 04-02
public abstract class Material {
public abstract String getType(); // no body: forces implementation
}Take this away: in module 4 you will learn how they are written; in this lesson you have learned why they exist. The order matters, because whoever learns interface without having understood abstraction ends up creating one interface per class without knowing what for.
Common Mistakes and Tips
- Confusing abstraction with encapsulation. Abstraction is what I expose; encapsulation is how I hide it. The table in section 2 separates them.
- Mixing levels in a method. Business rules and
printfin the same block is the most frequent mistake in beginners' code, and the most expensive to undo. - Putting presentation in the domain classes. A
Bookthat knows how to print its card on the console cannot be reused on a website. Presentation goes in another package. - Making things public "just in case". Every public method is a permanent commitment. Start
privateand raise it only when somebody genuinely needs it. - Generic names.
Manager,Processor,Helper,Data,Info,Util. If you cannot give a class a concrete name, you probably do not yet know what it is. - Documenting the how instead of the what. A Javadoc saying "walks the array and sums" becomes obsolete the moment you change the implementation. Document the contract: what it receives, what it returns, what it guarantees.
- Abstracting without a second case. YAGNI. Wait for the second format, the second database, the second file layout to show up.
- Tip: describe the method in one sentence. If you need "and" or "also", the method does two things and probably mixes levels.
- Tip: read your code as if you were the librarian. If
l.registerReturn(20)is understandable anddm.process(i, 20)is not, you know which one is well named. - Tip: the change test. For every design decision, ask yourself what would have to be touched if the rule changed. The fewer files, the better the abstraction.
Exercises
Exercise 1: redesign Loan's public API
This is the lesson's central exercise. Take the Loan class as it ended up in 03-07 and reduce its public surface to the necessary minimum.
- List all its current public members.
- For each one, decide: keep, make
privateor remove, justifying the decision in one sentence. - Remove
getEmployeeDoNotUse()and replace its uses with appropriate delegations. - Write the complete Javadoc, with preconditions and postconditions, of the three methods you consider the class's core.
- Check that
BiblioTechAppandConsoleReceiptstill work with the reduced API.
Exercise 2: separate abstraction levels
Refactor this method by distributing each line to its level. It should end up with: the rules in the domain, the presentation in ConsoleReceipt and the use case reduced to three or four readable lines.
public static void processFullLoan(Material m, Employee e, int day, Scanner sc) {
System.out.println("--- NEW LOAN ---");
if (m.isAvailable() == false) {
System.out.println("ERROR: not available");
return;
}
if (e.getTotalLoans() >= 3) {
System.out.println("ERROR: limit reached");
return;
}
System.out.print("Confirm (yes/no): ");
String r = sc.nextLine().trim();
if (!r.equalsIgnoreCase("yes")) {
System.out.println("Cancelled");
return;
}
Loan l = new Loan(m, e, day);
int due = day + m.getLoanDays();
System.out.println("OK. Due on day " + due
+ ". Late rate: " + m.getDailyRate() + " EUR/day"
+ ". Cap: " + 20.0 + " EUR");
}Exercise 3: judge abstractions
For each proposal from a colleague, decide whether it is justified, whether it is over-abstraction (YAGNI) or whether it is a leaky abstraction. Justify it in two or three sentences and, where appropriate, propose the alternative.
- Creating
PhysicalMaterialandDigitalMaterialas intermediate levels betweenMaterialand the three current classes. - Adding to
Materiala methodpublic String[] getInternalFields()returning the raw state "for debugging". - Extracting the four business constants to a
BusinessRulesclass withstaticmethods. - Adding to
Loanaboolean silentModeparameter inregisterReturnso as not to print warnings.
Solutions
Solution 1
1 and 2. Member-by-member review:
| Member | Decision | Justification |
|---|---|---|
Loan(Material, Employee, int, int) |
Keep | Canonical constructor |
Loan(Material, Employee, int) |
Keep | Usual entry, with zero days |
getReference() |
Keep | Identifies the loan on screen and in notices |
getMaterialTitle() |
Keep | Delegation compliant with Demeter |
getEmployeeName() |
Keep | Delegation compliant with Demeter |
getMaterial() |
Remove | Exposes a mutable collaborator; replaced by delegations |
getEmployeeDoNotUse() |
Remove | Allows mutating the employee bypassing the loan |
getLoanDay() |
Keep | Shown in the summary |
getDueDay() |
Keep | Key data for the user |
getElapsedDays() |
Keep | Shown and queried |
setElapsedDays(int) |
To private |
Internal detail of registerReturn |
isReturned() |
Keep | State queried in listings |
isOverdue() |
Keep | Business query |
daysRemaining() |
Keep | Shown in due-date notices |
calculateDaysLate() |
Keep | The presentation needs it |
calculateFine() |
Keep | Query made before the return |
classifySeverity() |
Keep | Shown on the receipt |
getAppliedFine() |
Keep | A fact recorded after returning |
getIncidents() |
Keep | With a defensive copy |
recordIncident(String) |
Keep | A legitimate business operation |
employeeAtLimit() |
Keep | Replaces navigating to the employee |
getLoansCreated() |
Keep | Session statistic |
canBeLent(Material, Employee) |
Keep | Pre-check for the use case |
3. Replacing the two removed getters. Where l.getMaterial().getType() or l.getEmployeeDoNotUse().getIdentifier() used to be written, delegations are added:
public String getMaterialType() { return material.getType(); }
public String getMaterialReference() { return material.getReference(); }
public String getEmployeeIdentifier() { return employee.getIdentifier(); }They are three trivial methods, and that is the usual objection: "I have swapped one getter for three". The answer is that now the class controls what can be known about the employee, and nobody can call registerLoan() behind its back. One open door has been swapped for three barred windows.
4. Javadoc of the three core methods:
/**
* Registers the return of the material and applies the corresponding fine.
*
* <p>Preconditions: {@code elapsedDays >= 0}. If the loan had already been
* returned, the call has no effect.
*
* <p>Postconditions: {@code isReturned()} is {@code true}; the material
* becomes available again; the employee frees up an active loan; the returned
* value lies in the interval [0, {@code Material.MAX_FINE}].
*
* @param elapsedDays days since handover, never negative
* @return applied fine in euros
*/
public double registerReturn(int elapsedDays) { ... }
/**
* Calculates the fine that would apply if the material were returned now,
* according to the format's own term and rate.
*
* <p>Preconditions: none. It can be called at any time.
* <p>Postconditions: it does not modify the loan's state; the result lies
* in [0, {@code Material.MAX_FINE}].
*
* @return amount in euros
*/
public double calculateFine() { ... }
/**
* Says whether it is possible to register a loan of the given material to the
* given employee, without creating it.
*
* <p>Preconditions: none; it accepts null arguments and answers
* {@code false}.
* <p>Postconditions: it modifies no object.
*
* @param material the requested material
* @param employee the requesting employee
* @return {@code true} if the material is available and the employee has not
* reached {@code Employee.MAX_CONCURRENT_LOANS}
*/
public static boolean canBeLent(Material material, Employee employee) { ... }Notice a detail in the third one: documenting "accepts nulls and answers false" is a design decision. Without it, every caller would have to check on their own, or discover the behaviour through NullPointerExceptions.
5. ConsoleReceipt already used only delegations (getMaterialTitle(), getEmployeeName()), so it still compiles unchanged. That is the sign the pruning was right: what was removed was not used by anyone legitimate.
Solution 2
The original method mixes four levels: interaction with the user, rule validation, creation of the loan and presentation of results. It is distributed like this.
Domain. The combined check already exists (Loan.canBeLent) and the due day is computed by the constructor. No new rules need adding, only a method that explains why it cannot be lent:
/**
* @return the reason why it cannot be lent, or an empty string if it can
*/
public static String rejectionReason(Material material, Employee employee) {
if (material == null || employee == null) return "Incomplete data";
if (!material.isAvailable()) return "The material is not available";
if (!employee.canBorrow()) return "The employee has reached their limit";
return "";
}Presentation. All the text, in ConsoleReceipt:
public void printLoanHeader() {
System.out.println("--- NEW LOAN ---");
}
public void printRejection(String reason) {
System.out.println("ERROR: " + reason);
}
public void printConditions(Material m) {
System.out.printf(" Term: %d days | Rate: %.2f EUR/day | Cap: %.2f EUR%n",
m.getLoanDays(), m.getDailyRate(), Material.MAX_FINE);
}
public void printLoanRegistered(Loan l) {
System.out.printf("OK. %s registered. Due on day %d.%n",
l.getReference(), l.getDueDay());
}Interaction with the user. That is presentation too, so the confirmation is isolated:
/** @return true if the user confirms the operation. */
public boolean confirm(Scanner sc) {
System.out.print("Confirm (yes/no): ");
return sc.nextLine().trim().equalsIgnoreCase("yes");
}Use case. It coordinates and nothing else:
public static void processFullLoan(Material m, Employee e, int day, Scanner sc) {
receipt.printLoanHeader();
String reason = Loan.rejectionReason(m, e);
if (!reason.isEmpty()) {
receipt.printRejection(reason);
return;
}
receipt.printConditions(m);
if (!receipt.confirm(sc)) {
System.out.println("Cancelled");
return;
}
receipt.printLoanRegistered(new Loan(m, e, day));
}Improvements achieved, beyond the length:
| Original problem | How it is resolved |
|---|---|
m.isAvailable() == false |
!reason.isEmpty(), and the reason comes with text |
The literal 3 duplicating the limit |
Supplied by Employee.MAX_CONCURRENT_LOANS |
The literal 20.0 duplicating the cap |
Material.MAX_FINE |
day + m.getLoanDays() recomputed outside |
Computed by the constructor; read with getDueDay() |
| Error messages scattered around | All in the presentation class |
| Impossible to change channel | Replace ConsoleReceipt with another class |
Solution 3
1. PhysicalMaterial / DigitalMaterial — over-abstraction (YAGNI). Today the catalog's three materials are all physical, so DigitalMaterial would have no subclass and PhysicalMaterial would contribute neither a field nor a method that Material does not already have. It would add a level to the hierarchy —exactly what 03-05 advises against— in exchange for nothing. Alternative: wait. The day an on-demand video course arrives, with its own access rules, the intermediate level is introduced with a real case in hand.
2. getInternalFields() — a leaky abstraction, and a serious one. A method returning "the raw state" publishes the internal representation as part of the API: as soon as somebody uses it in production "just for a log", you will no longer be able to change the fields without breaking it. It also invites parsing the output instead of using the methods. Alternative: a properly overridden toString(), which is exactly what this is for and is studied in the next lesson. Debugging is done with the debugger (02-05), which sees private fields without needing to expose them.
3. A BusinessRules class with the constants — it depends, and that is why it is the most interesting. As formulated, it is debatable: the constants already live in Material, the class that uses them, and moving them into a separate container would take them away from their meaning; besides, getLoanDays() and getDailyRate() are polymorphic per format, so a single class could not represent them without going back to a switch on the type, that is, undoing what was achieved in 03-06. It would be justified if those values had to be read from a configuration file or vary by site, because then there would be a second real case. Today there is not. Verdict: YAGNI, with a review date.
4. A boolean silentMode parameter — wrong for two reasons. The first is the one from the good-practice section of 03-03: a boolean parameter at the call site says nothing (registerReturn(20, true) is unreadable). The second is deeper: its existence proves the abstraction is broken, because a domain method should not print anything, and therefore there should be nothing to silence. Alternative: have registerReturn write nothing to the console at all and return the information —it does, it returns the fine—, letting the presentation layer decide what to show and when. "Silent mode" disappears because the problem disappears.
A pattern worth recognising in all four answers: proposals 2 and 4 are patches over symptoms; once the abstraction levels are separated, neither of them has any reason to exist.
Conclusion
You have completed the four pillars. Abstraction is not a keyword you type, but the discipline of keeping what is essential to the problem, and that is why the same book is modelled differently in a library, a shop and a printing house. You know how to distinguish it from encapsulation —what I expose versus how I hide it— and why you need both. You have learned to detect and correct the mixing of abstraction levels, the vice that turned your module 2 main into an unreadable block, separating the business rules into the domain, the presentation into its own package and leaving the use case at two lines. You know how to write contracts in Javadoc with preconditions, postconditions and invariants, and to choose consciously between validating defensively or documenting and trusting. You model with the vocabulary of the domain, so that a Nexus Software librarian would recognise your code. You have verified that separating the what from the how pays for itself: changing the fine policy to a progressive rate affects one file. And you have judgement about both extremes: the minimal public API versus the permanent cost of exposing too much, leaky abstractions and the YAGNI that reins in hierarchies invented for cases nobody asked for.
You also know what is missing: the two mechanisms with which Java materialises all of this —interfaces (04-01) and abstract classes (04-02)— arrive in the next module, and now you will know what they are for, which is exactly what is missing from anyone who learns them too early.
One lesson remains to close the module, and it is the one that makes your objects behave as first-class citizens of the language. Right now, printing a Book produces an unreadable Book@1b6d3586, and two copies with the same ISBN are considered different because equals compares memory addresses. In The Object Class: equals, hashCode and toString you will learn the role of the root of the whole hierarchy, the complete equals contract with its five properties, why hashCode must always be overridden alongside it and what breaks if you do not. And you will keep the promise that closes module 2: the three loose variables highestDelay, highestDelayEmployee and highestDelayBook will finally become a single coherent object.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
