В этой статье мы обсудим, что такое принципы SOLID и зачем они вам понадобятся как iOS-разработчику.
Согласно Википедии:
В разработке программного обеспечения SOLID — это мнемоническая аббревиатура пяти принципов проектирования, призванных сделать проекты программного обеспечения более понятными, чистыми, гибкими и удобными в поддержке.
Принципы SOLID — это набор передовых практик, которым нужно следовать при создании кода.
Эти пять принципов помогают нам понять необходимость определенных шаблонов проектирования и архитектуры программного обеспечения в целом, чтобы сделать проекты более понятными, гибкими и удобными. Поэтому я считаю, что это тема, которую должны изучить все программисты, включая iOS-разработчиков.
Принцип единственной ответственности
Роберт Дж. Мартин (он же дядя Боб) выражает этот принцип следующим образом:
Для каждого класса должно быть определено единственное назначение.
Пример
protocol Openable { mutating func open() }protocol Closeable { mutating func close() }
Структура SimpleDoor имеет инкапсулированное состояние, и вы можете изменить его с помощью ее методов:
struct SimpleDoor: Openable, Closeable { private enum State { case open case closed } private var state: State = .closed mutating func open() { state = .open } mutating func close() { state = .closed } }
Класс DoorOpener отвечает за открытие двери и не знает, что внутри и как закрыть дверь:
final class DoorOpener { private var door: Openable init(door: Openable) { self.door = door } func execute() { door.open() } }
А класс DoorCloser отвечает за закрытие двери и снова не имеет представления о том, что внутри и как открыть дверь:
final class DoorCloser { private var door: Closeable init(door: Closeable) { self.door = door } func execute() { door.close() } } let door = SimpleDoor()
Только класс DoorOpener отвечает за открытие двери:
let doorOpener = DoorOpener(door: door) doorOpener.execute()
Поэтому, если при закрытии двери необходимо выполнить другую операцию, например, включить сигнализацию, вам не нужно менять класс DoorOpener:
let doorCloser = DoorCloser(door: door) doorCloser.execute()
Принцип открытости/закрытости
Программные сущности должны быть открыты для расширения, но закрыты для модификации.
Пример
protocol Shooting { func shoot() -> String }
Этот класс имеет метод shot() и, конечно же, может стрелять:
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()
А здесь у нас есть класс Rocket Launcher, и он может стрелять ракетами. Чтобы добавить поддержку ракетных установок в наш класс Weapons Composite, нам не нужно ничего менять в существующих классах:
final class RocketLauncher: Shooting { func shoot() -> String { return "Whoosh!" } } let rocket = RocketLauncher()weapons = WeaponsComposite(weapons: [laser, rocket]) weapons.shoot()
Принцип подстановки Лисков
Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы.
Или, проще говоря, производные классы должны без проблем заменять свои базовые классы.
Пример
RequestError является подклассом NSError и предоставляет дополнительные функции, но не влияет на исходные функции.
let requestKey: String = "NSURLRequestKey" 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()
С моей точки зрения, это идеальный экземпляр 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). Обратите внимание, что космическая станция не имеет представления о посадочных возможностях SpaceXCRS8:
protocol PayloadFetching { func fetchPayload(vehicle: PayloadHaving) -> String } final class InternationalSpaceStation: PayloadFetching { 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" }
Этот класс имеет полезную нагрузку и может приземляться на объекты, имеющие место посадки. Таким образом, CRS8 знает только информацию о посадочной площадке:
final class SpaceXCRS8: Landing, PayloadHaving { let payload = "BEAM and some Cube Sats" 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)
Принцип инверсии зависимостей
Модули высокого уровня не должен зависеть от модуля низкого уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей.
Другими словами, ваши сущности должны зависеть от абстракций, а не от чего-то конкретного.
Пример
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" } }
EmmetBrown получает DeLorean как устройство TimeTraveling, а не конкретный класс DeLorean:
final class EmmettBrown { private let timeMachine: TimeTraveling` 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)
Заключение
В этой статье мы начали с определения принципов SOLID, а затем получили четкое представление о причинах и способах реализации каждого принципа.
Я предлагаю помнить об этих принципах при проектировании, написании и рефакторинге вашего программного обеспечения, чтобы ваш код был намного более чистым, расширяемым и поддерживаемым.