Композиция vs Наследование
Суть
Наследование — отношение is-a («кнопка является view»): подкласс получает всё поведение родителя. Композиция — отношение has-a («экран имеет логгер»): объект собирается из частей и делегирует им работу. Общее правило: «предпочитайте композицию наследованию», наследуйтесь, когда действительно нужно расширить фреймворк (UIViewController) или модель предметной области честно иерархична.
Пример
// Inheritance: "is-a". A deep hierarchy is rigid.
class BaseViewController {}
class LoggingViewController: BaseViewController {}
// Composition: "has-a". Behaviour is injected and replaceable.
protocol Logger { func log(_ message: String) }
struct ConsoleLogger: Logger { func log(_ message: String) { print(message) } }
struct SilentLogger: Logger { func log(_ message: String) {} }
final class ProfileViewModel {
private let logger: any Logger
init(logger: any Logger = ConsoleLogger()) { self.logger = logger }
func load() { logger.log("loading profile") }
}
ProfileViewModel().load() // prints
ProfileViewModel(logger: SilentLogger()).load() // prints nothing
Чтобы добавить логирование через наследование, нужен LoggingViewController, потом LoggingAnalyticsViewController... Комбинаторный взрыв. Композиция позволяет собрать любой набор поведений и подменить его в тестах.
Почему композиция обычно лучше
- Меньше связанность: зависимость — это протокол, а не конкретный родитель.
- Поведение можно менять в рантайме (подставить другой
Logger). - Нет проблемы хрупкого базового класса и единственного слота наследования.
- Работает с
struct/enum/actor, которые вообще не наследуются.
Когда наследование уместно
- Подкласс фреймворка:
UIView,UIViewController,NSOperation. - Нужен полиморфизм подтипов с общим состоянием, а протокол с
extensionдаёт только поведение, не хранимые свойства.