Как правильно использовать расширения класса в Swift?

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

В последнее время, однако, я видел, что Apple предлагает использовать расширения в еще большей степени, например, реализация протоколов как отдельных расширений.

То есть, если у вас есть класс A, реализующий протокол B, вы получите такой дизайн:

class A {
    // Initializers, stored properties etc.
}

extension A: B {
    // Protocol implementation
}

Когда вы входите в эту кроличью нору, я начал видеть больше кода на основе расширений, например:

fileprivate extension A {
    // Private, calculated properties
}

fileprivate extension A {
    // Private functions
}

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

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

Однако этот дизайн также разделяет хранимые и несохраненные свойства, публичные и приватные функции, что усложняет "логику" класса (я знаю, пишу меньшие классы). Это, вместе с проблемами подклассов, заставляет меня немного остановиться на крыльце страны чудес расширения.

Хотелось бы услышать, как сообщество Swift в мире смотрит на расширения. Как вы думаете? Есть ли серебряная пуля?

Ответ 1

Конечно, это только мое мнение, так что примите то, что я напишу спокойно.

В настоящее время я использую extension-approach в своих проектах по нескольким причинам:

  • Код намного более чистый: мои классы никогда не превышают 150 строк, а разделение по расширениям делает мой код более читабельным и разделенным обязанностями

Обычно так выглядит класс:

final class A {
    // Here the public and private stored properties
}

extension A {
    // Here the public methods and public non-stored properties
}

fileprivate extension A {
    // here my private methods
}

Расширений может быть больше одного, конечно, это зависит от того, что делает ваш класс. Это просто полезно для организации вашего кода и чтения его с верхней панели XCode

extension description

  • Это напоминает мне, что Swift является языком протоколано-ориентированного программирования, а не языком ООП. Вы ничего не можете сделать с протоколом и расширениями протокола. И я предпочитаю использовать протоколы для добавления уровня безопасности в мои классы/структуру. Например, я обычно пишу свои модели следующим образом:

    protocol User {
        var uid: String { get }
        var name: String { get }
    }
    
    final class UserModel: User {
        var uid: String
        var name: String
    
        init(uid: String, name: String) {
            self.uid = uid
            self.name = name
        }
    }
    

Таким образом, вы все еще можете редактировать свои значения uid и name внутри класса UserModel, но вы не можете снаружи, так как вы будете обрабатывать только тип протокола User.

Ответ 2

Я использую аналогичный подход, который можно описать в одном предложении:

Сортировка обязанностей типа в расширениях

Это примеры для аспектов, которые я вкладываю в отдельные расширения:

  • Основной интерфейс типа, как видно из клиента.
  • Соответствие протокола (т.е. протокол делегатов, часто закрытый).
  • Сериализация (например, все NSCoding).
  • Части типов, которые живут в фоновом потоке, например, обратные вызовы сети.

Иногда, когда сложность одного аспекта возрастает, я даже разделяю реализацию типа более чем на один файл.

Вот некоторые подробности, описывающие, как я сортирую код, связанный с реализацией:

  • Основное внимание уделяется функциональному членству.
  • Сохранять закрытые и закрытые реализации, но разделенные.
  • Не разделяйте между var и func.
  • Сохраняйте все аспекты реализации функциональности вместе: вложенные типы, инициализаторы, соответствие протокола и т.д.

Преимущество

Основная причина отдельных аспектов типа - облегчить чтение и понимание.

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

Есть еще одно преимущество: контроль доступа упрощает не называть что-то непреднамеренно. Метод, который должен быть вызван из фонового потока, может быть объявлен private в расширении "background". Теперь его просто нельзя вызвать из другого места.

Текущие ограничения

Swift 3 накладывает определенные ограничения на этот стиль. Есть несколько вещей, которые могут жить только в реализации основного типа:

  • сохраненные свойства
  • переопределение func/var
  • overidable func/var
  • обязательные (назначенные) инициализаторы

Эти ограничения (по крайней мере, первые три) связаны с необходимостью заранее знать макет данных объекта (и таблицу свидетелей для чистого Swift). Расширения потенциально могут быть загружены до конца во время выполнения (через фреймворки, плагины, dlopen,...) и изменение макета типа после того, как экземпляры были созданы, заблокировали бы их ABI.

Небольшое предложение для команды Swift:)

Все коды из одного модуля гарантированно будут доступны одновременно. Ограничения, которые предотвращают полное разделение функциональных аспектов, можно обойти, если бы компилятор Swift позволял "компоновать" типы внутри одного модуля. С составами типов я имею в виду, что компилятор будет собирать все объявления, которые определяют макет типа из всех файлов в модуле. Как и в других аспектах языка, он автоматически найдет зависимости между файлами.

Это позволило бы реально написать "ориентированные на аспект" расширения. Отсутствие объявления хранимых свойств или переопределений в основной декларации позволило бы лучше контролировать доступ и разделять проблемы.

Ответ 3

Я ненавижу это. Это добавляет дополнительную сложность и затрудняет использование расширений, из-за чего неясно, чего ожидать от людей, для которых используются расширения.

Если вы используете расширение для соответствия протокола, хорошо, я вижу это, но почему бы просто не прокомментировать ваш код? Как это лучше? Я этого не вижу.