How do you apply the SOLID principles in Swift without over-engineering?

15.6.1 · L3 · Interview

Predict first

New discount kinds are added as new types, with no change to the checkout function. What does it return?

protocol Discount { func apply(_ total: Double) -> Double }
struct Percent: Discount { let p: Double; func apply(_ t: Double) -> Double { t * (1 - p) } }
struct Flat: Discount { let off: Double; func apply(_ t: Double) -> Double { max(0, t - off) } }
func checkout(_ total: Double, discounts: [Discount]) -> Double { discounts.reduce(total) { $1.apply($0) } }
print(checkout(100, discounts: [Percent(p: 0.1), Flat(off: 5)]))
  • 85.0
  • 95.0
  • 90.0

Mental model. SOLID is like building codes: sensible in a hospital, silly in a garden shed. The skill is knowing how much building you need.

SOLID is five heuristics about managing change. Single Responsibility: a type should have one reason to change. A view model that formats strings, calls the network, parses JSON and persists data has four; split along the lines where requirements change independently, not by method count. Open/Closed: add behaviour by adding code, not editing tested code. In Swift this is protocols and composition: a checkout that applies [Discount] works for Percent and Flat, and a new BuyOneGetOne is a new type with no change to checkout (a list of a 10% and a flat 5 discount on 100 gives 85). Liskov Substitution: a subtype must be usable wherever its supertype is, without surprises: a ReadOnlyFile that traps on write breaks the contract of a File protocol; prefer smaller protocols and value types over deep class hierarchies. Interface Segregation: callers should not depend on methods they do not use; many small protocols (Fetching, Persisting) beat one large one, and a consumer declares only what it needs. Dependency Inversion: high-level policy should not depend on low-level detail; both depend on an abstraction owned by the high-level side, which is what makes injection and testing possible.

Now the “without dogma”. Each principle has a price in indirection, and the price is only worth paying when the change it protects is likely. Do not create a protocol for every class (a protocol with one implementation and no tests or variants is noise); wait for the second implementation or the test that needs a fake (“rule of three”). Do not split a type into pieces so small that understanding one behaviour means reading six files. Prefer composition over inheritance and value types over reference types, which in Swift removes many Liskov problems by construction. Prefer duplication to the wrong abstraction: two similar pieces that change for different reasons should stay separate. Pair the principles with others that matter as much: YAGNI (do not build what you do not need), KISS, the Law of Demeter, and making illegal states unrepresentable with enums. A good review question is not “does this follow SOLID?” but “what would it take to change X, and is that proportionate to how often X changes?”. Use the principles to explain why a design hurts, not as a checklist that must be satisfied.

Ten percent off 100 is 90, then 5 off is 85. A new discount type needs no edit to checkout: open for extension, closed for modification.

Common traps

  • A protocol for every class “for testing”. It adds indirection without a second implementation or a real need; add it when you need the seam.
  • Splitting responsibilities so finely that logic is scattered. Group by reasons to change, not by size.

They might follow up with

  • How does Swift help with Liskov? Value types, protocols with small requirements, and composition reduce inheritance traps.
  • What is a good test of whether to abstract? The change you are protecting against must be likely and nameable.

Related questions

  • 15.2.2 How do you do dependency injection in Swift, and when do you need a container or framework?
  • 15.1.1 How do MVC, MVVM, MVP, VIPER, RIBs and Clean compare, and which would you choose?
  • 15.3.1 What is a feature module, how do you draw module boundaries, and how does access control enforce them?

How do you run a shared mobile platform for many apps and teams… →