2.2.8 · D4Design Principles

Exercises — SOLID — Dependency Inversion Principle

2,788 words13 min readBack to topic

This page is a self-testing ladder. Each rung raises the difficulty: L1 Recognition (spot it), L2 Application (apply it), L3 Analysis (measure and dissect it), L4 Synthesis (build with it), L5 Mastery (defend it under pressure). Try each problem before opening its solution.

New parent context: the DIP topic note. If a symbol below is unfamiliar, that note built it first — but every number here is re-derived in place.


Level 1 — Recognition

Exercise 1.1 — Spot the concrete dependency

Here is a snippet. Point to the exact line that violates DIP, and name what depends on what.

class ReportService {
    PdfWriter writer = new PdfWriter();  // line A
    void run(){ writer.write("hello"); } // line B
}
Recall Solution

Line A violates DIP. ReportService is a high-level module (policy: "produce a report"). PdfWriter is a low-level module (mechanical detail: bytes to a PDF). The word new PdfWriter() welds the policy directly to a concrete detail.

The dependency arrow points: ReportService PdfWriter (concrete). DIP wants both to point at an abstraction instead. Line B is fine in spirit — calling write is what we want; the sin is who created the object and what type the field is.

Exercise 1.2 — Which is high-level?

Given BillingPolicy and StripeApiClient, which is the high-level (policy) module and which is the low-level (detail) module?

Recall Solution
  • High-level = BillingPolicy — it decides what should happen (charge the customer, apply discounts). It is the valuable, slowly-changing business asset.
  • Low-level = StripeApiClient — it is a swappable mechanism (how bytes reach Stripe's servers). It could be replaced by PaypalApiClient tomorrow.

Rule of thumb: the module whose deletion would change what the product does is high-level; the module whose deletion only changes how a step is carried out is low-level.

Exercise 1.3 — Arrow direction

In a DIP-correct design with an interface Repository, a class SqlRepository, and a class UserService, draw the three dependency arrows.

Recall Solution

Figure — SOLID — Dependency Inversion Principle

  • UserService Repository (policy depends on the abstraction).
  • SqlRepository Repository (detail depends on — implements — the abstraction).
  • Repository depends on nothing (an interface is a pure contract, no implementation).

Notice both real classes point at the interface. That "both arrows converging on the abstraction" shape is DIP.


Level 2 — Application

Exercise 2.1 — Refactor to obey DIP

Rewrite this to satisfy DIP. State every change you make and why.

class WeatherApp {
    OpenWeatherClient client = new OpenWeatherClient();
    double todayTemp(){ return client.fetchTemp(); }
}
Recall Solution

Step 1 — introduce an abstraction the policy owns.

interface WeatherSource { double fetchTemp(); }

Why: gives WeatherApp a stable contract to depend on instead of a concrete class.

Step 2 — make the detail implement it (invert its arrow up).

class OpenWeatherClient implements WeatherSource { ... }

Why: the concrete's dependency arrow now points up at the contract.

Step 3 — inject the concrete; never new it inside.

class WeatherApp {
    private final WeatherSource src;
    WeatherApp(WeatherSource s){ this.src = s; }
    double todayTemp(){ return src.fetchTemp(); }
}

Why: removes the last source-code dependency on OpenWeatherClient. WeatherApp.java now compiles knowing only WeatherSource.

Exercise 2.2 — Cost of adding a variant

In the DIP version above, you must add a MockWeatherSource for tests. How many existing files must you edit? In the original (non-DIP) version, how many?

Recall Solution
  • DIP version: 0 existing files. You create one new file MockWeatherSource implements WeatherSource and inject it in the test. WeatherApp is untouched.
  • Non-DIP version: 1 existing file (WeatherApp itself), because the concrete OpenWeatherClient is hard-wired inside — you'd have to change the field and constructor.

The count dropping from 1 → 0 on the core policy file is the concrete payoff of inversion: change becomes additive (add a leaf) instead of invasive (edit the core).

Exercise 2.3 — Where does the interface live?

You have package core.billing (policy) and package infra.stripe (detail). In which package should PaymentGateway (the interface) live? Which package ends up depending on which?

Recall Solution

The interface PaymentGateway lives in core.billing (with the policy that owns it).

Then infra.stripe core.billing (the detail package depends on the policy package). This is the truly inverted direction. Had you put the interface in infra.stripe, then core.billing infra.stripe, and your valuable policy would again depend on an infrastructure package — the very thing DIP forbids.


Level 3 — Analysis

For L3 we quantify dependency direction with Robert C. Martin's metrics. Two counts for a module:

Exercise 3.1 — Compute instability

Module M is depended on by 8 classes and itself depends on 2 classes. Find . Is it stable or unstable?

Recall Solution

Here (8 arrows in), (2 arrows out). is close to , so M is fairly stable — many depend on it, it depends on little. Good: stable modules are exactly where you want other code to point.

Exercise 3.2 — Instability before vs after DIP

OrderService originally depends on 3 concrete details and is depended on by 5 controllers (). After DIP you replace the 3 concretes with 1 abstraction it owns, so now (it depends only on that interface) with unchanged. Compute before and after and interpret.

Recall Solution

Before: . After: .

Instability dropped (): OrderService became more stable because it now depends on far fewer things. DIP literally moved a high-value module toward — toward being a rock others can safely build on.

Exercise 3.3 — Distance from the main sequence

A module has and . The "distance from main sequence" is Compute . Is this module well-placed?

Recall Solution

Figure — SOLID — Dependency Inversion Principle
is tiny (ideal is ). This module is stable and abstract — the bottom-right / top-left ideal for a DIP contract package: everyone depends on it ( low) and it exposes mostly interfaces ( high), so it is stable and extensible. It sits almost exactly on the main sequence (see the red dot in the figure).


Level 4 — Synthesis

Exercise 4.1 — Design a plug-in payment system

Design (in pseudocode + arrows) a checkout that supports Stripe today and PayPal next month, such that adding PayPal touches zero existing core files. Name the abstraction, say which package owns it, and show injection.

Recall Solution
// package core.checkout  (policy owns the contract)
interface PaymentGateway { Receipt charge(Money m); }

class CheckoutService {
    private final PaymentGateway gateway;
    CheckoutService(PaymentGateway g){ this.gateway = g; }   // injected
    Receipt buy(Cart c){ return gateway.charge(c.total()); }
}

// package infra.stripe   (detail points UP into core)
class StripeGateway implements PaymentGateway { ... }

// NEXT MONTH — new file only, zero core edits:
// package infra.paypal
class PaypalGateway implements PaymentGateway { ... }

Arrows: CheckoutService → PaymentGateway, StripeGateway → PaymentGateway, PaypalGateway → PaymentGateway. Package arrows: infra.stripe → core.checkout, infra.paypal → core.checkout. Adding PayPal = 1 new file + 1 injection wiring line; 0 edits to CheckoutService.

Exercise 4.2 — Combine DIP with a factory

Your CheckoutService should not know which gateway to build, yet something must choose. Introduce a mechanism that picks the gateway by a config string "stripe"/"paypal" without CheckoutService ever mentioning a concrete. Sketch it and count the concrete class names visible inside CheckoutService.

Recall Solution

Use a factory (see Factory Pattern) at the composition edge:

class GatewayFactory {
    PaymentGateway create(String cfg){
        if (cfg.equals("stripe")) return new StripeGateway();
        return new PaypalGateway();
    }
}
// wiring (main / IoC container):
PaymentGateway g = new GatewayFactory().create(config);
CheckoutService svc = new CheckoutService(g);   // injected abstraction

Concrete gateway names visible inside CheckoutService: 0. All new StripeGateway() / new PaypalGateway() live in the factory at the outer edge. CheckoutService still sees only PaymentGateway. This is DIP (goal) delivered by Dependency Injection + Factory Pattern (techniques). An IoC container would automate the same wiring.

Exercise 4.3 — Relate to the neighbours

For each: does obeying DIP here also serve OCP? And how does the abstraction relate to Strategy and to Ports & Adapters?

Recall Solution
  • OCP: Yes. Adding PaypalGateway without editing CheckoutService means the module is open for extension, closed for modification — DIP's inverted arrow is precisely what enables OCP.
  • Strategy: PaymentGateway with interchangeable StripeGateway/PaypalGateway is the Strategy pattern — swappable algorithms behind one interface. DIP explains why the arrow inverts; Strategy is a shape that realizes it at runtime.
  • Ports & Adapters: PaymentGateway is a port owned by the core; StripeGateway is an adapter in the infrastructure ring. Hexagonal architecture is DIP applied systematically to every external boundary.

Level 5 — Mastery

Exercise 5.1 — DI without DIP (the tricky counterexample)

Produce a snippet that uses Dependency Injection yet still violates DIP. Explain precisely why.

Recall Solution
class ReportService {
    private final PdfWriter writer;                  // concrete TYPE
    ReportService(PdfWriter w){ this.writer = w; }   // injected!
}

This injects writer (classic DI — no new inside), yet the declared parameter/field type is the concrete PdfWriter. So ReportService.java still has a compile-time dependency on a low-level detail. DI is satisfied; DIP is not. Fix: change the type to an interface DocumentWriter. This proves DIP (goal: depend on abstractions) ≠ DI (technique: pass objects in).

Exercise 5.2 — DIP without classic DI

Now satisfy DIP without constructor/setter injection. What mechanism do you use, and is it still DIP-clean?

Recall Solution

Use a service locator / factory lookup returning the abstraction:

class ReportService {
    private final DocumentWriter writer =
        Registry.get(DocumentWriter.class);   // returns an abstraction
    void run(){ writer.write("hi"); }
}

ReportService depends only on DocumentWriter (the abstraction) and on Registry (a lookup utility) — never on a concrete writer. So DIP holds even though we didn't inject through the constructor. (Trade-off: the service-locator hides dependencies and is generally considered inferior to DI for testability — but it is DIP-compliant. Goal met by a different technique.)

Exercise 5.3 — Prove the change-cost claim numerically

Claim: "Under DIP, supporting payment providers requires editing the core CheckoutService file only once ever (the initial abstraction), regardless of ; without DIP it requires editing it times." Model the total core-edits as a function of for both, and give the values at .

Recall Solution

Let = core edits with DIP, = core edits without DIP.

With DIP: you edit the core once to introduce the abstraction, then every provider is a new leaf file + wiring (outside the core). So Without DIP: each provider is hard-wired into the core, so each addition edits the core once: At : and . The saving is core edits avoided, and the ratio grows without bound as increases (). This is why DIP turns " invasive changes" into " core change + additive leaves."


Active Recall