Course contents

Refactor drill — apply all five

Sign in to run this lab and save your progress

Learn

Before you start

This is the closer of the SOLID module. You've drilled each principle on its own; now you use all five at once on a realistic refactor.

Why the "drill" format

Because this is what a real design interview asks you to do.

Interviewer: "Here's an existing system. Refactor it." You: (hold all five principles in your head, apply each one in turn, produce a clean design.)

If you've only practiced the principles one at a time, this scenario melts your brain. If you've practiced them together even once, it doesn't.

Suggested order of attack

Do them in this order — each step makes the next easier:

  1. SRP first. Split the god class by job. Ignore OCP / LSP / etc. for now. Just move the jobs into their own classes. Once done, you'll have Report, some formatters, some storage thing, some notifier thing.

  2. OCP second. Anywhere you see if fmt == "csv": elif fmt == "json":, replace it with a base class + subclasses. The if/elif goes away.

  3. LSP third. Check every subclass hierarchy you introduced. Does every child honestly keep the parent's promises? For this exercise, everything should be fine — but the habit of checking matters.

  4. ISP fourth. Look at each interface. Is it as small as it can be while still being useful? For this exercise, the interfaces are already small (each has one method) — again, the habit is what matters.

  5. DIP last. Rewrite ReportService (the top-level coordinator) to take its dependencies as constructor arguments. Nothing gets built inside except local values.

Following this order, the whole refactor is maybe 60-80 lines. Trying to do all five at once without a plan is 300 lines of pain.

What the strict tests are checking

  • test_report_has_no_formatting_persistence_delivery — SRP: the data class stays a data class.
  • test_new_formatter_added_without_touching_service — OCP: a new formatter extends the system with ZERO changes to existing code.
  • test_service_does_not_hardcode_concretes — DIP: the coordinator only names the abstract types.
  • test_service_takes_all_three_deps — DIP: the constructor takes all three collaborators.
  • Every formatter / storage / notifier test checks abstract bases (LSP + ISP).

If any of these fail, you've brought back one of the smells the refactor was supposed to remove. Read the failure message and identify which principle is warning you.

After this lesson

You have the whole SOLID toolkit. The next module is design patterns — every pattern uses at least one SOLID principle, and most use several. You'll spot them naturally now.

Your task

You have every SOLID principle. Now use all five at once.

In main.py you'll find a ReportSystem god class that breaks every principle from S to D. Refactor it into a clean design that:

  1. SRP — split the jobs across focused classes.
  2. OCP — new output formats added by writing new classes, not by editing old ones.
  3. LSP — no child class breaks its parent's promises.
  4. ISP — no class implements methods it can't support.
  5. DIP — dependencies (formatter, storage, notifier) are passed in through the constructor.

The bad code you're rewriting

See main.py for the full mess. In summary:

class ReportSystem:
    def __init__(self, records):
        self.records = records

    def calculate_total(self): ...           # data
    def format_csv(self):                    # formatting
    def format_json(self):                   # more formatting (OCP smell)
    def save_to_disk(self, path, fmt):       # storage
                                              # if/elif on fmt (OCP smell)
    def email_report(self, to, fmt):         # delivery
                                              # smtplib.SMTP inside
    def send_sms_report(self, to, fmt):      # more delivery
                                              # (SRP + DIP smells)

The target shape

Build these classes:

  1. Report — holds records. Has total() and records (list). No formatting, no saving, no sending.
  2. Formatter interface + concrete formatters:
    • ReportFormatter(ABC) with abstract format(report).
    • CsvFormatter(ReportFormatter) — returns a CSV string. Format: "name,amount" header, then one "<name>,<amount>" line per record.
    • JsonFormatter(ReportFormatter) — returns a JSON string via json.dumps({"total": ..., "records": [...]}). Records list is [{"name": ..., "amount": ...}, ...].
  3. ReportStorage(ABC) with abstract save(name, content) returning a string. One implementation: InMemoryStorage(ReportStorage) — stores name→content in a dict called .files, returns "stored <name> ({len} bytes)".
  4. Notifier(ABC) with abstract send(to, message). Two concrete: EmailNotifier returning "emailed <to>: <message>" and SmsNotifier returning "texted <to>: <message>".
  5. ReportService — the top-level coordinator:
    • Constructor takes formatter: ReportFormatter, storage: ReportStorage, notifier: Notifier.
    • publish(report, name, to) — formats the report, saves it under name, notifies to with f"report ready: {name}", returns a dict: {"stored": <storage.save result>, "notified": <notifier.send result>}.
    • Doesn't create any formatter / storage / notifier inside.

Rules (checked by tests)

  • Report has NO format_*, save_*, email_*, or send_* methods.
  • ReportService.__init__ accepts all three dependencies and mentions none of these class names: CsvFormatter, JsonFormatter, InMemoryStorage, EmailNotifier, SmsNotifier.
  • Adding an XmlFormatter (the test defines one inline) works with ZERO changes to ReportService or the existing formatters.

Click Submit to run the tests. Green means you got all five right.

Starter files

Preview only — open the lab to edit and run.

files_dir
06-refactor-drill-starter