| layout | title | lang | permalink |
|---|---|---|---|
default |
The Principles of OOD in Swift 5 |
en |
/ |
A short cheat-sheet with Playground (OOD-Principles-In-Swift.playground.zip).
👷 Project maintained by: @oktawian (Oktawian Chojnacki)
- The Single Responsibility Principle
- The Open Closed Principle
- The Liskov Substitution Principle
- The Interface Segregation Principle
- The Dependency Inversion Principle
A class should have one, and only one, reason to change.
A refined formulation: a module should be responsible to one, and only one, actor (stakeholder). SRP is not about "doing one thing" — it is about gathering together the things that change for the same reasons and separating those that change for different reasons. When a class serves multiple actors, changes requested by one actor risk breaking the expectations of another.
Example:
protocol Openable {
mutating func open()
}
protocol Closeable {
mutating func close()
}
// I'm the door. I have an encapsulated state and you can change it using methods.
struct PodBayDoor: Openable, Closeable {
private enum State {
case open
case closed
}
private var state: State = .closed
mutating func open() {
state = .open
}
mutating func close() {
state = .closed
}
}
// I'm only responsible for opening, no idea what's inside or how to close.
final class DoorOpener {
private var door: Openable
init(door: Openable) {
self.door = door
}
func execute() {
door.open()
}
}
// I'm only responsible for closing, no idea what's inside or how to open.
final class DoorCloser {
private var door: Closeable
init(door: Closeable) {
self.door = door
}
func execute() {
door.close()
}
}
let door = PodBayDoor()
// ⚠️ Only the `DoorOpener` is responsible for opening the door.
let doorOpener = DoorOpener(door: door)
doorOpener.execute()
// ⚠️ If another operation should be made upon closing the door,
// like switching on the alarm, you don't have to change the `DoorOpener` class.
let doorCloser = DoorCloser(door: door)
doorCloser.execute()You should be able to extend a class's behavior, without modifying it.
Software entities (classes, modules, functions) should be open for extension but closed for modification. The key insight is that when a single change ripples through dependent modules, the design is fragile. By relying on abstractions (protocols), new behavior can be added by writing new code — not by changing existing, working code.
Example:
protocol Shooting {
func shoot() -> String
}
// I'm a laser beam. I can shoot.
final class LaserBeam: Shooting {
func shoot() -> String {
return "Ziiiiiip!"
}
}
// I have weapons and trust me I can fire them all at once. Boom! Boom! Boom!
final class WeaponsComposite {
let weapons: [Shooting]
init(weapons: [Shooting]) {
self.weapons = weapons
}
func shoot() -> [String] {
return weapons.map { $0.shoot() }
}
}
let laser = LaserBeam()
var weapons = WeaponsComposite(weapons: [laser])
weapons.shoot()
// I'm a rocket launcher. I can shoot a rocket.
// ⚠️ To add rocket launcher support I don't need to change anything in existing classes.
final class RocketLauncher: Shooting {
func shoot() -> String {
return "Whoosh!"
}
}
let rocket = RocketLauncher()
weapons = WeaponsComposite(weapons: [laser, rocket])
weapons.shoot()Derived classes must be substitutable for their base classes.
Subtypes must honor the behavioral contract of their supertypes: they must not
strengthen preconditions, weaken postconditions, or violate invariants. A caller
that works with a base type must be able to use any subtype without knowing it,
and the program should still behave correctly. Violations of this principle lead
to fragile hierarchies where if/else type-checks creep into client code.
Example:
let requestKey: String = "NSURLRequestKey"
// I'm a NSError subclass. I provide additional functionality but don't mess with original ones.
class RequestError: NSError {
var request: NSURLRequest? {
return self.userInfo[requestKey] as? NSURLRequest
}
}
// I fail to fetch data and will return RequestError.
func fetchData(request: NSURLRequest) -> (data: NSData?, error: RequestError?) {
let userInfo: [String:Any] = [requestKey : request]
return (nil, RequestError(domain:"DOMAIN", code:0, userInfo: userInfo))
}
// I don't know what RequestError is and will fail and return a NSError.
func willReturnObjectOrError() -> (object: AnyObject?, error: NSError?) {
let request = NSURLRequest()
let result = fetchData(request: request)
return (result.data, result.error)
}
let result = willReturnObjectOrError()
// Ok. This is a perfect NSError instance from my perspective.
let error: Int? = result.error?.code
// ⚠️ But hey! What's that? It's also a RequestError! Nice!
if let requestError = result.error as? RequestError {
requestError.request
}Make fine grained interfaces that are client specific.
No client should be forced to depend on methods it does not use. When an interface grows too large, its clients become coupled to methods they never call — and changes to those unrelated methods can force clients to recompile or redeploy. Splitting fat interfaces into smaller, role-specific protocols keeps dependencies narrow and cohesive.
Example:
// I have a landing site.
protocol LandingSiteHaving {
var landingSite: String { get }
}
// I can land on LandingSiteHaving objects.
protocol Landing {
func land(on: LandingSiteHaving) -> String
}
// I have payload.
protocol PayloadHaving {
var payload: String { get }
}
// I can fetch payload from vehicle (ex. via Canadarm).
protocol PayloadFetching {
func fetchPayload(vehicle: PayloadHaving) -> String
}
final class InternationalSpaceStation: PayloadFetching {
// ⚠ Space station has no idea about landing capabilities of SpaceXCRS8.
func fetchPayload(vehicle: PayloadHaving) -> String {
return "Deployed \(vehicle.payload) at April 10, 2016, 11:23 UTC"
}
}
// I'm a barge - I have landing site (well, you get the idea).
final class OfCourseIStillLoveYouBarge: LandingSiteHaving {
let landingSite = "a barge on the Atlantic Ocean"
}
// I have payload and can land on things having landing site.
// I'm a very limited Space Vehicle, I know.
final class SpaceXCRS8: Landing, PayloadHaving {
let payload = "BEAM and some Cube Sats"
// ⚠️ CRS8 knows only about the landing site information.
func land(on: LandingSiteHaving) -> String {
return "Landed on \(on.landingSite) at April 8, 2016 20:52 UTC"
}
}
let crs8 = SpaceXCRS8()
let barge = OfCourseIStillLoveYouBarge()
let spaceStation = InternationalSpaceStation()
spaceStation.fetchPayload(vehicle: crs8)
crs8.land(on: barge)Depend on abstractions, not on concretions.
Two formal rules define this principle: (1) High-level modules should not depend on low-level modules — both should depend on abstractions. (2) Abstractions should not depend on details — details should depend on abstractions. By inverting the source-code dependency so that it points toward policies rather than mechanisms, high-level business logic becomes immune to changes in infrastructure and implementation details.
Example:
protocol TimeTraveling {
func travelInTime(time: TimeInterval) -> String
}
final class DeLorean: TimeTraveling {
func travelInTime(time: TimeInterval) -> String {
return "Used Flux Capacitor and travelled in time by: \(time)s"
}
}
final class EmmettBrown {
private let timeMachine: TimeTraveling
// ⚠️ Emmet Brown is given a `TimeTraveling` device, not the concrete class `DeLorean`!
init(timeMachine: TimeTraveling) {
self.timeMachine = timeMachine
}
func travelInTime(time: TimeInterval) -> String {
return timeMachine.travelInTime(time: time)
}
}
let timeMachine = DeLorean()
let mastermind = EmmettBrown(timeMachine: timeMachine)
mastermind.travelInTime(time: -3600 * 8760)📖 Descriptions from: The Principles of OOD by Uncle Bob