Как правильно использовать расширения класса в 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
Я ненавижу это. Это добавляет дополнительную сложность и затрудняет использование расширений, из-за чего неясно, чего ожидать от людей, для которых используются расширения.
Если вы используете расширение для соответствия протокола, хорошо, я вижу это, но почему бы просто не прокомментировать ваш код? Как это лучше? Я этого не вижу.