Early Access: 87 spots left.

Claim
Low Level DesignLiskov SubstitutionLiskov Substitution: Refactor the Account Hierarchy

Course content

Liskov Substitution: Refactor the Account Hierarchy

Medium·Tagssolidliskov-substitutioninterface-segregationrefactoring

Problem Statement

The starter has three classes: - `Account` is the base class with `deposit`, `withdraw`, and `getBalance`. - `SavingsAccount extends Account` and inherits everything. - `FixedDepositAccount extends Account` and overrides `withdraw` to throw `UnsupportedOperationException`, because a fixed deposit cannot be withdrawn from before maturity. This violates the Liskov Substitution Principle. Any function that holds an `Account` reference and calls `withdraw` works for `SavingsAccount` but breaks for `FixedDepositAccount`. The base type promised a capability that one of its subtypes cannot honour. Refactor the design so that any `Account` reference is safe to use polymorphically. The fix is to move the withdraw capability out of the base class and into a separate interface. After your refactor, the design must expose this exact public API: ```java class Account { public Account(double balance); public double getBalance(); public void deposit(double amount); // No withdraw on the base class. } interface Withdrawable { void withdraw(double amount); } class SavingsAccount extends Account implements Withdrawable { ... } class FixedDepositAccount extends Account { ... } // does NOT implement Withdrawable ``` The `Withdrawable` interface is already declared in the starter. Your job is to: 1. Remove the `withdraw` method from `Account`. 2. Remove the broken `withdraw` override from `FixedDepositAccount`. 3. Make `SavingsAccount` implement `Withdrawable` and define `withdraw` correctly (subtract the amount, but reject withdrawals that exceed the balance with `IllegalStateException`). The validator runs five checks: 1. `SavingsAccount` supports the full lifecycle: deposit and withdraw work correctly, and overdrawing throws `IllegalStateException`. 2. `FixedDepositAccount` does not expose a `withdraw` method at all. The validator uses reflection to confirm that calling `getMethod("withdraw", double.class)` on `FixedDepositAccount` throws `NoSuchMethodException`. This forces both the removal of `withdraw` from `Account` and the removal of the override from `FixedDepositAccount`. 3. `SavingsAccount` implements `Withdrawable`. The validator uses reflection to confirm `Withdrawable.class.isAssignableFrom(SavingsAccount.class)` returns true. 4. `Account` does not declare a `withdraw` method. Reflection check, mirrors the requirement that withdraw belongs on the capability interface, not on the base class. 5. A `SavingsAccount` cast to `Withdrawable` behaves correctly when `withdraw` is invoked through the interface reference. This is the LSP test in its purest form: substituting a subtype for the abstraction does not change the contract. The smelly starter passes only check 1 out of the box. The other four require the actual refactor.

Examples

Example 1
Input
Withdrawable w = new SavingsAccount(1000); w.withdraw(300); ((SavingsAccount) w).getBalance()
Output
"700"
Why
A SavingsAccount can be substituted for any Withdrawable reference. The withdraw call subtracts 300 from the balance.
Example 2
Input
FixedDepositAccount fd = new FixedDepositAccount(1000); fd.deposit(500); fd.getBalance()
Output
"1500"
Why
Fixed deposits support deposit and getBalance through the Account base. They do not expose withdraw at all.
Example 3
Input
FixedDepositAccount fd = new FixedDepositAccount(1000); fd.withdraw(100)
Output
"compile error"
Why
After the refactor, FixedDepositAccount does not have a withdraw method. The compiler rejects the call, which is exactly the protection LSP is meant to provide.

Constraints

  • Refactor must keep the four declarations named: Account, SavingsAccount, FixedDepositAccount, Withdrawable.
  • Account must not declare a withdraw method.
  • FixedDepositAccount must not declare or inherit a withdraw method.
  • SavingsAccount must implement Withdrawable.
  • SavingsAccount.withdraw must throw IllegalStateException when the requested amount exceeds the current balance.

Hints

Stuck? Reveal a nudge toward the right pattern, one step at a time.

Hint 1
Start by deleting the withdraw method from Account. The point of the refactor is that Account should not promise something one of its subtypes cannot deliver.
Hint 2
Delete the broken withdraw override from FixedDepositAccount as well. Fixed deposits do not have a withdraw operation, so the class should not have a withdraw method at all.
Hint 3
Make SavingsAccount implement Withdrawable. Move the working withdraw logic (the same overdraft check the starter Account had) into SavingsAccount.
Hint 4
The validator uses reflection on FixedDepositAccount.class.getMethod("withdraw", double.class). getMethod walks the inheritance chain, so this throws NoSuchMethodException only when neither FixedDepositAccount nor Account declares a withdraw method. That is the architectural property you are trying to establish.
Hint 5
Resist the temptation to make FixedDepositAccount.withdraw a no-op or to weaken its exception. The fix is structural: withdraw should not be on FixedDepositAccount's API surface at all.