A short cheat-sheet with Playground (OOD-Principles-In-Swift-uk.playground.zip).
👷 Project maintained by: @oktawian (Oktawian Chojnacki)
Клас повинен мати одну, і тільки одну, причину для зміни.
Точніше формулювання: модуль повинен відповідати перед одним, і тільки одним, актором (зацікавленою стороною). SRP — це не про «робити одну річ», а про групування разом того, що змінюється з однакових причин, і відокремлення того, що змінюється з різних причин. Коли клас обслуговує кількох акторів, зміни, запитані одним актором, можуть порушити очікування іншого.
Приклад:
protocol Openable {
mutating func open()
}
protocol Closeable {
mutating func close()
}
// Я — двері. Маю інкапсульований стан, який можна змінити за допомогою методів.
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
}
}
// Я відповідаю лише за відкривання, не знаю що всередині і як закривати.
final class DoorOpener {
private var door: Openable
init(door: Openable) {
self.door = door
}
func execute() {
door.open()
}
}
// Я відповідаю лише за закривання, не знаю що всередині і як відкривати.
final class DoorCloser {
private var door: Closeable
init(door: Closeable) {
self.door = door
}
func execute() {
door.close()
}
}
let door = PodBayDoor()
// ⚠️ Тільки `DoorOpener` відповідає за відкривання дверей.
let doorOpener = DoorOpener(door: door)
doorOpener.execute()
// ⚠️ Якщо при закриванні дверей потрібно виконати додаткову операцію,
// наприклад, увімкнути сигналізацію, не потрібно змінювати клас `DoorOpener`.
let doorCloser = DoorCloser(door: door)
doorCloser.execute()
Має бути можливо розширити поведінку класу без його модифікації.
Програмні сутності (класи, модулі, функції) повинні бути відкриті для розширення, але закриті для модифікації. Ключове спостереження полягає в тому, що коли одна зміна каскадно поширюється через залежні модулі, проєкт є крихким. Спираючись на абстракції (протоколи), нову поведінку можна додати, написавши новий код — без зміни наявного, працюючого коду.
Приклад:
protocol Shooting {
func shoot() -> String
}
// Я — лазерний промінь. Я вмію стріляти.
final class LaserBeam: Shooting {
func shoot() -> String {
return "Ziiiiiip!"
}
}
// Я маю зброю, і повірте, я можу вистрілити з усього одразу. Бум! Бум! Бум!
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()
// Я — ракетна установка. Я вмію запускати ракету.
// ⚠️ Щоб додати підтримку ракетної установки, не потрібно змінювати нічого в існуючих класах.
final class RocketLauncher: Shooting {
func shoot() -> String {
return "Whoosh!"
}
}
let rocket = RocketLauncher()
weapons = WeaponsComposite(weapons: [laser, rocket])
weapons.shoot()
Похідні класи повинні бути підставними замість базових класів.
Підтипи повинні дотримуватися поведінкового контракту своїх надтипів: вони не
повинні посилювати передумови, послаблювати постумови або порушувати інваріанти.
Викликаючий код, який працює з базовим типом, повинен мати можливість
використовувати будь-який підтип, не знаючи про це, і програма повинна
продовжувати працювати правильно. Порушення цього принципу призводять до крихких
ієрархій, де в клієнтському коді з’являються перевірки типу if/else.
Приклад:
let requestKey: String = "NSURLRequestKey"
// Я — підклас NSError. Я надаю додаткову функціональність, але не порушую оригінальну.
class RequestError: NSError {
var request: NSURLRequest? {
return self.userInfo[requestKey] as? NSURLRequest
}
}
// Мені не вдається отримати дані, і я поверну RequestError.
func fetchData(request: NSURLRequest) -> (data: NSData?, error: RequestError?) {
let userInfo: [String:Any] = [requestKey : request]
return (nil, RequestError(domain:"DOMAIN", code:0, userInfo: userInfo))
}
// Я не знаю що таке RequestError і поверну NSError.
func willReturnObjectOrError() -> (object: AnyObject?, error: NSError?) {
let request = NSURLRequest()
let result = fetchData(request: request)
return (result.data, result.error)
}
let result = willReturnObjectOrError()
// OK. З моєї точки зору це ідеальний екземпляр NSError.
let error: Int? = result.error?.code
// ⚠️ Але зачекайте! Що це? Це також RequestError! Чудово!
if let requestError = result.error as? RequestError {
requestError.request
}
Створюйте дрібнозернисті інтерфейси, адаптовані до конкретного клієнта.
Жоден клієнт не повинен бути змушений залежати від методів, які він не використовує. Коли інтерфейс надмірно розростається, його клієнти стають зв’язаними з методами, які вони ніколи не викликають — а зміни в цих непов’язаних методах можуть змусити клієнтів перекомпілювати або повторно розгортати. Розділення громіздких інтерфейсів на менші, спеціалізовані протоколи підтримує залежності вузькими та зв’язними.
Приклад:
// Я маю місце для посадки.
protocol LandingSiteHaving {
var landingSite: String { get }
}
// Я можу приземлитися на об'єкти LandingSiteHaving.
protocol Landing {
func land(on: LandingSiteHaving) -> String
}
// Я маю вантаж.
protocol PayloadHaving {
var payload: String { get }
}
// Я можу забрати вантаж з транспортного засобу (наприклад, за допомогою Canadarm).
protocol PayloadFetching {
func fetchPayload(vehicle: PayloadHaving) -> String
}
final class InternationalSpaceStation: PayloadFetching {
// ⚠ Космічна станція нічого не знає про здатність SpaceXCRS8 до посадки.
func fetchPayload(vehicle: PayloadHaving) -> String {
return "Deployed \(vehicle.payload) at April 10, 2016, 11:23 UTC"
}
}
// Я — баржа, у мене є місце для посадки (ну, ви розумієте ідею).
final class OfCourseIStillLoveYouBarge: LandingSiteHaving {
let landingSite = "a barge on the Atlantic Ocean"
}
// Я маю вантаж і можу приземлитися на об'єкти з місцем для посадки.
// Я дуже обмежений космічний апарат, я знаю.
final class SpaceXCRS8: Landing, PayloadHaving {
let payload = "BEAM and some Cube Sats"
// ⚠️ CRS8 знає лише інформацію про місце посадки.
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)
Залежність повинна бути від абстракцій, а не від конкретних реалізацій.
Дві формальні правила визначають цей принцип: (1) Модулі високого рівня не повинні залежати від модулів низького рівня — обидва повинні залежати від абстракцій. (2) Абстракції не повинні залежати від деталей — деталі повинні залежати від абстракцій. Інвертуючи залежність вихідного коду так, щоб вона вказувала на політики, а не на механізми, бізнес-логіка високого рівня стає несприйнятливою до змін в інфраструктурі та деталях реалізації.
Приклад:
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
// ⚠️ Еммет Браун отримує пристрій `TimeTraveling`, а не конкретний клас `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