Ортогональность в Swift

Ортогональность в Swift

Vasiliy Usov

Каждый из нас рано или поздно задумывается о качестве своего кода. И на помощь в решении этого вопроса приходят архитектурные паттерны и различные методики, вроде принципов SOLID. И такое желание вполне естественно: без этого не берут в профессиональные программисты на большие зарплаты, в каждой второй вакансии на HH требуют знание знание правил, описанных дядей Бобом (aka Джордж Мартин, автор книги «Чистая архитектура»).

И вот вы познаете MVС, MVP и другие архитектуры, пытаетесь разделять функционал приложения между компонентами, создаете относительно независимые элементы кода, аббревиатура SOLID больше не пустой набор звуков, вы даже имеете представление о законе Деметры.

Но в процессе изучения матчасти вы рано или поздно в книгах или на stackoverflow встречаетесь с понятием Ортогональности. И не до конца понимаете, что оно значит. А ведь именно ортогональность лежит в основе всего материала, который вы изучали.

Понятие ортогональности пришло в программирование из геометрии, где оно используется для описания двух независимых перпендикулярных векторов. В нашем же случае система называется ортогональной, если выполняются два требования:

1. Loose coupling - слабое сцепление - отдельные компоненты вашей программы зависят друг от друга в минимально возможной степени.

2. High cohesion - сильная связность - каждый отдельный компонент программы выполняет одну конкретную задачу.

Это два простых требования, на которых строится весь смысл применения архитектурных паттернов и различных принципов разработки программного обеспечения.

Самая главная заслуга ортогональности - простое и легкое внесение изменений в код. Модифицируя один компонент ортогональной системы другие даже не заметят этого!

Вот вы и узнали про ортогональность, и теперь не будете впадать в ступор, встретив это понятие. И на этом казалось бы можно закончить, но я бы хотел немного рассказать о том, что такое ортогональность для Swift.

Swift позиционирует себя не просто как объектно-ориентированный, а как «первый на земном шаре» протокол-ориентированный язык. Все три принципа ООП: наследование, инкапсуляция, полиморфизм, разработаны для того, чтобы вы создавали качественный и независимый код. Но вот что интересно, с наследованием в этом вопросе кое-что не так. Наследование ухудшает ортогональность. Оно, в большинстве случаев, которые я видел, ухудшает показатели coupling и cohesion.

Спросите себя, для чего вы используете наследование? Я вижу всего два возможных варианта:

  1. Для уменьшения количества кода. Например, в своей программе вам нужны классы Cat и Dog. И казалось бы вполне логичным построить их на основе класса Animal, который по сути не будет использоваться в программе, а служит лишь каркасом для Cat и Dog.
  2. Для создания множества отдельных типов данных, каждый из которых будет использован в коде программы. Например, вы создаете класс Vehicle и на его основе класс Truck. Один используете на экране, выводящем все возможные типы автомашин, а второй на карточке грузовика.

Но что, в первом, что во втором случае, вы роете своей могилу ортогональности своего программного кода.

Используя наследование, вы связываете классы жесткой сцепкой. И если вдруг вам потребуется внести правки в базовый класс, то пострадают и все дочерние. А если дочерние классы используются в «независимых» компонентах программы, то эти компоненты уже не являются независимыми! Более того, каждый дочерний класс содержит всю логику всех родителей вверх по цепочке, а это приводит к излишней функциональности типов данных.

Но что это значит? На нельзя использовать наследование? А чем же нам заменить его?

Нет, отказываться от него не стоит. Но использование наследования стоит максимально ограничить.

Далее я привожу два примера, когда использование наследования оправдано (это не полный список, а лишь примеры):

  1. Если вы работает с UIKit, или другим фреймворков, построенном на классах, то у вас нет другого выхода при необходимости создавать свои реализации с помощью наследования. Но помните, в этом случае вы ваш код будет работать, основываясь на вере в том, что разработчик фреймворка не внесет изменения в его API. В ином случае ваш код перестанет работать.
  2. Вы создаете новый класс на базе старого, на при этом расширяете его возможности с помощью миксинов (в Swift они представлены посредством protocol extension).

И именно протокол-ориентированные возможности Swift позволяют решить все проблемы наследования.

Протокол - это контракт, договор, который должны выполнять подписавшиеся стороны. Протокол, в отличии от родительского класса, не определяет реализацию, он сосредоточен на требованиях. Подписывая класс или структуру на протокол вы всегда будете уверены, что API для доступа к нему не изменится, а каждые класс или структура самостоятельно определят реализацию описанных требований. Если вам нужно внести новые требования, то вы не изменяете старый протокол, а создаете новый. И подписываете заинтересованные стороны уже на него.

Протоколы в Swift позволяют определить реализацию некоторых требований по-умолчанию с помощью расширений (extension). Но такие реализации - это не самостоятельные сущности, а точно такие же требования, как и сам протокол.

В программировании такие независимые функциональные единицы, определяющие реализацию, называются миксинами. И они позволят вам по-новому раскрыть для себя широкие возможности языка Swift.

Рассмотрим один пример.

Вы создали протокол Encryptable, определяющий требований к наличию методов decode и encode в принимающем его типе.


protocol Encryptable {

  func encode(value: String) -> String

  func decode(value: String) -> String

}


class UserPassword: Encryptable{

  func encode(value: String) -> String {

    // шифровка значения

    return ""

  }

  func decode(value: String) -> String {

    // расшифровка значения

    return ""

  }

}


В разных частях программы требуется, чтобы значение могло быть закодировано с помощью различных алгоритмов, например AES и Blowfish.

Как нам поступить в этом случае?

Мы можем создать на основе Encryptable два протокола: EncryptableAES и EncryptableBlowfish, и для каждого из них указать собственную реализацию методов шифрования/дешифрования.


protocol EncryptableAES: Encryptable {}

extension EncryptableAES {

  func encode() -> String {

    // ...

  }

  func decode() -> String {

    // ...

  }

}


protocol EncryptableBlowfish: Encryptable {}

extension EncryptableBlowfish {

  func encode() -> String {

    // ...

  }

  func decode() -> String {

    // ...

  }

}


Теперь, когда нам нужен тип, способный шифровать значения с помощью одного из указанных алгоритмов, мы просто подпишем его на соответсвующий протокол.


class MyValue: EncryptableBlowfish { //... }


Но при необходимости мы можем наделить уже существующий тип возможностями шифрования, написав расширение для него


extension UserData: EncryptableBlowfish {}


Ну или мы можем создать дочерний тип, используя наследование, расширив его функциональность за счет созданных миксинов:


class Value {

var value: String

}

class SecretValue: Value, EncryptableAES {}


И это лишь несколько из множества примеров использования протоколов в Swift. При этом есть и другие способы избегать излишнего наследования, к примеру использовать супер-крутой паттер Делегирование. Но об этом поговорим как-нибудь в следующий раз.

Не торопитесь начинать повсеместно отказываться от наследования, полное понимание и осознание проблематики придет вам со временем в ходе работы над проектами. Вы обязательно начнете искать способы создания более ортогональных систем, но всему свое время.


Источник: http://swiftme.ru

Report Page