Layers of Logic

7.1

Encapsulation, Inheritance, Packages, and `super`

Protect an object's rules, then share a genuine parent type without losing control of construction.

Core28 min read4 exercises
01

Previously on

Phase VI built classes, constructors, references, and object state. You also saw that private alone cannot protect a mutable object returned through a getter.

This lesson turns those observations into two design tools. Encapsulation protects valid state. Inheritance represents a real type relationship and shares its implementation.

02

The problem

Consider a bank account with a public balance:

class BankAccount {
    public int balance;
}

Any caller can do this:

account.balance = -50_000;

The class has no place to reject a negative withdrawal, record an operation, or change how money is stored. Its rules are optional because callers can bypass them.

Now consider three registry units:

class GroundUnit {
    String name;
    int id;
    int readiness;
}

class AirUnit {
    String name;
    int id;
    int readiness;
    int maximumAltitude;
}

The repeated fields suggest a common concept. Copying them shares text, not a contract.

We need one tool to control access and another to express that an air unit is a unit.

03

The idea

Encapsulation keeps state changes behind valid operations

class BankAccount {
    private int balance;

    BankAccount(int openingBalance) {
        if (openingBalance < 0) {
            throw new IllegalArgumentException("negative opening balance");
        }
        balance = openingBalance;
    }

    int getBalance() {
        return balance;
    }

    void deposit(int amount) {
        requirePositive(amount);
        balance += amount;
    }

    void withdraw(int amount) {
        requirePositive(amount);
        if (amount > balance) {
            throw new IllegalArgumentException("insufficient funds");
        }
        balance -= amount;
    }

    private static void requirePositive(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
    }
}

Callers can ask for the current balance. They cannot assign it. Every change passes through an operation that understands the rule.

This is stronger than adding a setter:

void setBalance(int balance) {
    this.balance = balance;
}

That setter gives callers the old power with extra punctuation. deposit and withdraw describe allowed state transitions.

Encapsulation also lets the implementation change. The class could later store money in paise, add transaction logging, or make operations thread-safe.

Inheritance expresses a more specific type

class Unit {
    private final int id;
    private final String name;
    private int readiness;

    Unit(int id, String name, int readiness) {
        if (readiness < 0 || readiness > 100) {
            throw new IllegalArgumentException("readiness must be 0 to 100");
        }
        this.id = id;
        this.name = name;
        this.readiness = readiness;
    }

    int getId() {
        return id;
    }

    String getName() {
        return name;
    }

    int getReadiness() {
        return readiness;
    }

    void train(int points) {
        if (points < 0) {
            throw new IllegalArgumentException("negative training points");
        }
        readiness = Math.min(100, readiness + points);
    }
}

class AirUnit extends Unit {
    private final int maximumAltitude;

    AirUnit(int id, String name, int readiness, int maximumAltitude) {
        super(id, name, readiness);
        if (maximumAltitude <= 0) {
            throw new IllegalArgumentException("altitude must be positive");
        }
        this.maximumAltitude = maximumAltitude;
    }

    int getMaximumAltitude() {
        return maximumAltitude;
    }
}

AirUnit extends Unit declares an inheritance relationship.

An AirUnit receives the accessible behaviour of Unit and adds altitude. More importantly, every AirUnit is valid where a Unit is required.

The test is a sentence: an air unit is a unit. If that sentence is false, inheritance is probably the wrong relationship.

04

Under the hood

Going deeper

Access modifiers define who may name a member

Java has four member-access levels:

Declaration Code that may access the member
private code in the declaring class
no modifier code in the same package
protected code in the same package, plus subclasses under the protected inheritance rules
public code anywhere that can access the class

No modifier is called package-private access. Some material calls it default access, but there is no default keyword on the declaration.

protected includes the entire package. Across packages, it also gives subclasses controlled access to the inherited member.

Cross-package protected access has an extra rule. Subclass code cannot use an arbitrary superclass object as a back door into that object’s protected state.

Begin with the narrowest access that supports the design. Widen access when a real collaborator needs it.

private controls source access, not object layout

An AirUnit object is one object. Its state includes the fields declared by Unit and the field declared by AirUnit.

AirUnit object
  Unit state: id, name, readiness
  AirUnit state: maximumAltitude

Subclass source cannot write readiness because that field is private to Unit. The train method can update it because that method’s code belongs to Unit.

Calling scout.train(5) runs Unit code with the same AirUnit object as its receiver.

Private members are not inherited as directly accessible subclass members. Their storage and behaviour still contribute to the complete object.

A constructor builds the superclass part first

Constructors are not inherited. Every class defines how its own instances begin.

The first statement of a constructor must be one of these:

this(...);  // delegate to another constructor in the same class
super(...); // delegate to a constructor in the direct superclass

If you write neither, the compiler inserts super().

The chain must eventually reach a superclass constructor. Construction proceeds from Object down toward the requested class.

class Unit {
    Unit(int id) {
        System.out.println("Unit " + id);
    }
}

class AirUnit extends Unit {
    AirUnit(int id) {
        super(id);
        System.out.println("AirUnit " + id);
    }
}

new AirUnit(7) prints:

Unit 7
AirUnit 7

If Unit declares only Unit(int id), this constructor fails:

AirUnit() {
    // compiler inserts super(), but Unit() does not exist
}

The child must choose an available parent constructor explicitly.

Do not call overridable methods during construction

The superclass constructor runs before the subclass constructor body. Dynamic method dispatch can still reach a subclass override during that time.

class Parent {
    Parent() {
        printState();
    }

    void printState() {
        System.out.println("parent");
    }
}

class Child extends Parent {
    private String label = "ready";

    @Override
    void printState() {
        System.out.println(label);
    }
}

Creating Child can print null. The parent constructor calls the override before the child’s field initializer assigns "ready".

Constructors should initialise state. Avoid calling methods that subclasses can override.

super can select the parent implementation

A subclass may extend inherited behaviour:

class Unit {
    String summary() {
        return "unit";
    }
}

class AirUnit extends Unit {
    @Override
    String summary() {
        return super.summary() + ", air capable";
    }
}

super.summary() asks for the superclass implementation. It does not create a separate parent object.

this refers to the current object under ordinary method lookup. super starts lookup at the direct superclass.

Packages create named boundaries

A package declaration belongs at the top of a source file:

package registry.units;

public class Unit {
}

The class’s full name is now registry.units.Unit. Source files normally follow the same directory structure:

registry/
  units/
    Unit.java

Code in another package can import the type:

package registry.app;

import registry.units.Unit;

An import does not copy code or load a class. It lets source use Unit instead of writing the fully qualified name each time.

Types in java.lang, such as String and Math, are imported automatically. Types in the same package need no import.

Packages also support package-private collaboration. That boundary is useful for implementation details shared by several classes but hidden from the rest of the program.

Java classes have one direct superclass

A class may extend only one class:

class AmphibiousUnit extends GroundUnit, NavalUnit { } // invalid Java

Two implementation parents can create conflicts in inherited state, constructors, and method bodies. Java uses a single class chain to keep that relationship unambiguous.

A class can still promise several capabilities through interfaces. The next lesson shows that design.

Test the relationship, not the amount of reused code

Inheritance is not a general code-reuse command.

A Car has an Engine. A car is not an engine. This should use composition:

class Car {
    private final Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }
}

Inheritance also promises substitutability. Code written for Unit should remain correct when it receives an AirUnit.

If a child must reject ordinary parent operations or change their meaning, the hierarchy is lying. Composition provides reuse without making that type promise.

05

What it costs

Encapsulation adds method boundaries. Those boundaries pay for themselves when they preserve an invariant or hide a changeable implementation.

Inheritance couples a child to its parent’s API, behaviour, and construction rules. Deep hierarchies spread one object’s behaviour across many files.

Use inheritance for a stable is-a relationship. Use composition when you mainly want to reuse a collaborator.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Why is a class with a getter and setter for every field not automatically well encapsulated?

    Show the answer

    Encapsulation protects rules, not field syntax. An unrestricted setter may allow invalid state. A getter may leak a mutable internal object. Operations such as withdraw express intent and keep validation inside the class.

  2. A subclass constructor does not begin with an explicit this(...) or super(...). What does Java do?

    Show the answer

    The compiler inserts super(). That call must resolve to an accessible no-argument constructor in the direct superclass. If none exists, the subclass constructor does not compile.

  3. Are a superclass's private fields absent from a subclass object?

    Show the answer

    No. One subclass object contains the state declared throughout its class hierarchy. Subclass source cannot directly name private superclass fields, but superclass methods can still read and change that state on the same object.

  4. When should composition be preferred to inheritance?

    Show the answer

    Prefer composition when the relationship is "has a" or "uses a," or when the proposed child cannot honour the parent's behaviour. Inheritance declares that every child object may be used wherever the parent type is expected.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises105 pointsabout 115 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Close the Fields, Keep the Rules

Real work·25 min·20 points

checkedex-7-1-a

A constructor that validates, and a public field that lets anyone walk straight past it.

Close the fields. Then resist the reflex to add a getter and setter for each one, because that puts you exactly back where you started with more typing.

Add deposit and withdraw instead, and make them own the rules.

Then demonstrate the difference concretely. Write setBalance(getBalance() - 100) next to withdraw(100) and show what each one does with an amount larger than the balance. One of them can say no. That is the whole point of the section, in two lines.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Closed {
    static String tryDeposit(double opening, double amount)
    static String tryWithdraw(double opening, double amount)
    static double balanceAfterWithdraw(double opening, double amount)
    static double balanceIfSetterExisted(double opening, double amount)
}

Account keeps its constructor check and gains deposit and withdraw, both of which throw IllegalArgumentException when they refuse. The two try methods open an account, run one operation, and return "ok" or the exception's simple name. balanceAfterWithdraw returns what the account is left holding, refused or not. balanceIfSetterExisted runs the same request through LooseAccount, which stores whatever it is handed.

What your program must do

  • Make every field of Account private
  • Add deposit and withdraw that refuse invalid amounts
  • Show why a public setBalance puts you back where you started
  • Say which access level you chose for each member, and why
Closed.java
public class Closed {

    static class Account {
        String owner;                 // TODO: close these two
        double balance;

        Account(String owner, double balance) {
            if (balance < 0) throw new IllegalArgumentException("negative opening balance");
            this.owner = owner;
            this.balance = balance;
        }

        String getOwner()   { return owner; }
        double getBalance() { return balance; }

        // TODO: refuse anything that is not a positive amount
        void deposit(double amount) { }

        // TODO: refuse a non positive amount, and refuse an overdraft
        void withdraw(double amount) { }
    }

    // Leave this one exactly as it is. It is here to be compared against.
    static class LooseAccount {
        private double balance;
        LooseAccount(double balance) { this.balance = balance; }
        double getBalance() { return balance; }
        void setBalance(double balance) { this.balance = balance; }
    }

    // "ok" if the account allowed it, or the exception's simple name if it refused.
    static String tryDeposit(double opening, double amount)  { return ""; }  // TODO
    static String tryWithdraw(double opening, double amount) { return ""; }  // TODO

    // What the account is left holding after the request, allowed or refused.
    static double balanceAfterWithdraw(double opening, double amount) { return -1; }  // TODO

    // The same request through LooseAccount: setBalance(getBalance() - amount).
    static double balanceIfSetterExisted(double opening, double amount) { return -1; }  // TODO

    public static void main(String[] args) {
        // TODO: ask for 500 out of a balance of 100, both ways, and compare
    }
}
Hint 1
Making the fields private is only step one. If you then add setBalance, you have reopened the same hole with two extra method calls.
Hint 2
withdraw(double amount) can check that the amount is positive and that the balance covers it. setBalance cannot, because by the time it is called the caller has already decided the answer.
Hint 3almost the answer
The difference is who owns the rule. With a setter the caller computes the new balance and the object stores whatever it is given, so 100 minus 500 lands as -400. With withdraw the object computes it and can refuse, leaving 100. Encapsulation is about exposing operations, not fields.
What this is really testing

Whether you can turn a validating constructor into a rule that holds forever. A constructor that checks and a public field that does not is a lock on one door of a house with two.

B

Follow the Constructor Chain

Real work·25 min·25 points

checkedex-7-1-b

Three constructors in a chain. Predict the order of the printed lines before running.

Then break it. Remove super(name) from AirUnit. Something you did not write will fail, and the error will be reported on a class where nothing is missing.

Read that error properly. It is one of the more confusing messages in Java, and it stops being confusing the moment you know that Java inserts an invisible super() when you do not write one.

Then fix it two ways: put super(name) back, and separately, add a no-argument Unit() constructor. Both work. Say which one you would actually do, and why the second one might be a bad idea.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Chain {
    static List<String> buildOrder()
    static boolean compiles(boolean parentHasNoArgConstructor, boolean childCallsSuperExplicitly)
    static String whatJavaInserts()
    static String errorReportedOn()
    static String whatIsMissingIsIn()
}

Each constructor records its own class name in the shared log, so buildOrder returns the three names in the order the bodies ran. compiles is the rule, not the example: given whether the parent offers a no-argument constructor and whether the child writes its own super(...) call, say whether the child compiles. The last two return class names as plain strings.

What your program must do

  • Predict the order of the three names before running
  • Remove super(name) from AirUnit and record the exact compile error
  • Say which class the error is reported on, and which class actually caused it
  • Work out when an omitted super(...) is fine and when it is not
Chain.java
import java.util.*;

public class Chain {

    static final List<String> log = new ArrayList<>();

    static class Unit {
        protected String name;
        Unit(String name) {
            log.add("Unit");
            this.name = name;
        }
    }

    static class AirUnit extends Unit {
        private int maxAltitude;
        AirUnit(String name, int maxAltitude) {
            super(name);
            log.add("AirUnit");
            this.maxAltitude = maxAltitude;
        }
    }

    static class StealthAirUnit extends AirUnit {
        StealthAirUnit(String name) {
            super(name, 20000);
            log.add("StealthAirUnit");
        }
    }

    // Build one StealthAirUnit and return the names in the order the bodies ran.
    // PREDICT the order before you run it.
    static List<String> buildOrder() { return List.of(); }  // TODO

    // The rule, not this example. Does a child compile, given what its parent offers
    // and whether the child writes its own super(...) call?
    static boolean compiles(boolean parentHasNoArgConstructor, boolean childCallsSuperExplicitly) {
        return true;  // TODO
    }

    // What does Java put in front of a constructor that calls neither super nor this?
    static String whatJavaInserts() { return ""; }  // TODO

    // Remove super(name) from AirUnit and read the error.
    static String errorReportedOn()   { return ""; }  // TODO: which class is blamed
    static String whatIsMissingIsIn() { return ""; }  // TODO: which class is short of a constructor

    public static void main(String[] args) {
        System.out.println("building: " + buildOrder());

        // TODO: remove super(name) from AirUnit. Predict the error, then read it.
        // TODO: add a no-arg Unit() and try again. Predict what changes.
    }
}
Hint 1
The chain runs upwards first and then unwinds. Every constructor's real first act is calling its parent, so no child body runs until the topmost one has finished.
Hint 2
When you remove super(name), Java inserts super() for you. There is no no-argument Unit constructor, so that call cannot be resolved.
Hint 3almost the answer
So the rule has two ways to be satisfied. Either the child writes its own super(...) naming a constructor that exists, or the parent offers a no-argument constructor for the inserted call to land on. Only when neither is true does it fail, and the error is reported on AirUnit while the missing constructor belongs to Unit.
What this is really testing

Whether you know that super() is always called and always first. The error message when it goes wrong points at the wrong class, so knowing the rule saves real confusion.

C

Is-A or Has-A

Real work·25 min·25 points

checkedex-7-1-c

Six pairs of classes. For each one, decide whether inheritance, composition, or neither is right, and write down the reason.

Then do the famous one properly. Implement Square extends Rectangle and find the case where it misbehaves. Write the code that breaks it, do not just describe it. Watching a method that works on a Rectangle produce a wrong answer on a Square is the point.

Then build it again without inheritance, and show the problem is gone.

Finish by stating the substitution test in your own words. If your version mentions maths, try again. The problem with Square extends Rectangle is not mathematical, it is about a promise the parent makes that the child cannot keep.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class IsA {
    static int resizeAndArea(Rectangle r)
    static String relationship(int pair)
    static boolean squarePassesSubstitutionTest()
    static String whyItFails()
}

Rectangle has setWidth, setHeight and area. Square extends it and keeps itself square. SquareBySide does the same job by holding a Rectangle and exposing only a side. Leave resizeAndArea exactly as written, since the point is that it was compiled against Rectangle and never mentions Square. relationship answers pairs 1 to 6 with one lowercase word out of inheritance, composition or neither. whyItFails names the promise the parent makes that the child cannot keep.

What your program must do

  • Decide inheritance, composition or neither for all six, with a reason
  • Implement Square extends Rectangle and show a case where it misbehaves
  • Reimplement it without inheritance and show the problem is gone
  • State the substitution test in your own words
IsA.java
// Six proposed relationships. For each: inheritance, composition, or neither?
//
//  1. AirUnit        and Unit
//  2. Car            and Engine
//  3. Square         and Rectangle
//  4. Stack          and ArrayList
//  5. SavingsAccount and Account
//  6. Employee       and Person

public class IsA {

    static class Rectangle {
        private int width;
        private int height;
        void setWidth(int width)   { }  // TODO
        void setHeight(int height) { }  // TODO
        int area() { return 0; }        // TODO
    }

    // Number 3, with inheritance. Keep it square whatever anyone sets.
    static class Square extends Rectangle {
        // TODO: override both setters
    }

    // Number 3 again, without inheritance. It HAS a rectangle and only offers a side.
    static class SquareBySide {
        SquareBySide(int side) { }      // TODO
        void setSide(int side) { }      // TODO
        int area() { return 0; }        // TODO
    }

    // Leave this exactly as it is. It was written against Rectangle and has never
    // heard of Square, which is the entire point.
    static int resizeAndArea(Rectangle r) {
        r.setWidth(3);
        r.setHeight(4);
        return r.area();
    }

    // One lowercase word per pair: inheritance, composition or neither.
    static String relationship(int pair) { return ""; }  // TODO

    static boolean squarePassesSubstitutionTest() { return true; }  // TODO

    // Name the promise Rectangle makes that a square cannot keep.
    static String whyItFails() { return ""; }  // TODO

    public static void main(String[] args) {
        // TODO: run resizeAndArea on a Rectangle and on a Square, and compare
    }
}
Hint 1
Say the is-a sentence out loud for each pair. "A Car is an Engine" is clearly false, so number 2 is composition. "An AirUnit is a Unit" is true.
Hint 2
Number 3 is the famous one. Mathematically a square is a rectangle. In code, if Rectangle has setWidth and setHeight, then any method that sets width to 3 and height to 4 and expects an area of 12 will break when handed a Square.
Hint 3almost the answer
The substitution test: anywhere the parent is expected, the child must work without surprising anybody. Square fails it, not because the maths is wrong, but because the parent's interface promises a width and a height that move independently, and a square cannot keep that promise.
What this is really testing

Whether you can tell a genuine inheritance relationship from one that only saves typing. Getting this wrong produces code that compiles and lies, which the compiler cannot help you with.

D

The Registry Gets a Family

Hard·40 min·35 points·The Registry

checkedex-7-1-d

Build a small family of Registry units where the shared parts live in the parent and nothing else does.

The temptation is to push fields upwards so everything can reach them. Resist it. A maxAltitude on Unit means every ground unit carries a field that means nothing, and every reader has to work out which types it applies to.

The real test is the report loop at the end. It must walk a Unit[] with no instanceof and no casts anywhere.

If you find yourself needing one, that is a signal, not a failure. It means some behaviour that belongs to the unit is currently sitting in the loop instead. Move it into a method and the loop gets simpler, which is exactly what the next section is about.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class RegistryFamily {
    Unit(String name, String id, int readiness, boolean active)
    GroundUnit(String name, String id, int readiness, boolean active, int armourRating)
    AirUnit(String name, String id, int readiness, boolean active, int maxAltitude, boolean weatherClear)
    NavalUnit(String name, String id, int readiness, boolean active, int displacement)
    Unit: String getName(), String getId(), int getReadiness(), boolean isActive()
    Unit: boolean isDeployable(), String describe()
    RegistryFamily: static int readyCount(Unit[] units)
    RegistryFamily: static String report(Unit[] units)
}

The skeletons and the getters are given so the file compiles. What goes where is the exercise. Unit holds exactly the four shared fields and no more. Each child holds only its own. isDeployable is declared on Unit with the common rule and overridden by the types that differ. One test builds a unit type of its own and hands it to readyCount, so a loop that checks types instead of asking each unit will get the wrong answer.

What your program must do

  • Put every shared field in Unit, and nothing that is not shared
  • Store the constructor arguments, chaining every child to super
  • Give Unit the common deployability rule and override it only where it differs
  • Write readyCount over a Unit[] with no instanceof and no casts anywhere
RegistryFamily.java
// Shared by all:      name, id, readiness, active
// GroundUnit adds:    armourRating
// AirUnit adds:       maxAltitude, weatherClear
// NavalUnit adds:     displacement
//
// Deployability:      ground needs readiness >= 60
//                     air    needs readiness >= 60 AND clear weather
//                     naval  needs readiness >= 50
//
// The skeletons are here so the file compiles. Everything inside is yours.

class Unit {
    private final String name;
    private final String id;
    private final int readiness;
    private final boolean active;

    Unit(String name, String id, int readiness, boolean active) {
        // TODO: store all four
        this.name = null;
        this.id = null;
        this.readiness = 0;
        this.active = false;
    }

    String getName()     { return name; }
    String getId()       { return id; }
    int getReadiness()   { return readiness; }
    boolean isActive()   { return active; }

    // TODO: the rule that most units follow. The ones that differ override it.
    boolean isDeployable() { return false; }

    // TODO: something readable that includes the id and the name
    String describe() { return "TODO"; }
}

class GroundUnit extends Unit {
    private final int armourRating;

    GroundUnit(String name, String id, int readiness, boolean active, int armourRating) {
        super(name, id, readiness, active);
        this.armourRating = armourRating;
    }

    int getArmourRating() { return armourRating; }

    // TODO: does ground need its own rule, or is the one on Unit already right?
}

class AirUnit extends Unit {
    private final int maxAltitude;
    private final boolean weatherClear;

    AirUnit(String name, String id, int readiness, boolean active,
            int maxAltitude, boolean weatherClear) {
        super(name, id, readiness, active);
        this.maxAltitude = maxAltitude;
        this.weatherClear = weatherClear;
    }

    int getMaxAltitude()     { return maxAltitude; }
    boolean isWeatherClear() { return weatherClear; }

    // TODO: air is the only type with a second condition
}

class NavalUnit extends Unit {
    private final int displacement;

    NavalUnit(String name, String id, int readiness, boolean active, int displacement) {
        super(name, id, readiness, active);
        this.displacement = displacement;
    }

    int getDisplacement() { return displacement; }

    // TODO: naval sails ten points lower than everybody else
}

public class RegistryFamily {

    // TODO: count the deployable units. No instanceof and no casts.
    static int readyCount(Unit[] units) {
        return 0;
    }

    // TODO: one line per unit, then a summary line
    static String report(Unit[] units) {
        return "TODO";
    }

    public static void main(String[] args) {
        Unit[] units = {
            new GroundUnit("Anvil", "G-01", 74, true, 3),
            new AirUnit("Kestrel", "A-07", 88, true, 15000, false),
            new NavalUnit("Trident", "N-02", 55, true, 9000),
        };
        System.out.println(report(units));
    }
}
Hint 1
Only name, id, readiness and active go in Unit. If you find yourself putting maxAltitude there so that everything can share it, stop: three quarters of your units would carry a field that means nothing to them.
Hint 2
Ground and Unit want the same rule, so ground overrides nothing. Air and naval each need their own. That is what a parent is for, and it is why the common rule goes upstairs rather than being repeated three times.
Hint 3almost the answer
The count loop is one line: if (u.isDeployable()) ready++;. If you needed an instanceof, some behaviour that belongs on the unit is sitting in the loop instead.
What this is really testing

Whether you can design a small hierarchy where the shared parts genuinely belong in the parent. Putting too much in the parent is as wrong as putting too little, and both look fine until you add a fourth type.

08

After the credits

You can now answer four separate questions about a class:

  1. Which states are valid?
  2. Which operations may change that state?
  3. Which code may see each member?
  4. Is this class genuinely a more specific form of its parent?

The next lesson uses that parent type to hold many child implementations at once.

Threads you opened in this section