Компоновщик — это структурный паттерн, который позволяет создавать дерево объектов и работать с ним так же, как и с единичным объектом.
Компоновщик давно стал синонимом всех задач, связанных с построением дерева объектов. Все операции компоновщика основаны на рекурсии и «суммировании» результатов на ветвях дерева.
Концептуальный пример
Этот пример показывает структуру паттерна Компоновщик , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest
/// Базовый класс Компонент объявляет общие операции как для простых, так и для
/// сложных объектов структуры.
protocol Component {
/// При необходимости базовый Компонент может объявить интерфейс для
/// установки и получения родителя компонента в древовидной структуре. Он
/// также может предоставить некоторую реализацию по умолчанию для этих
/// методов.
var parent: Component? { get set }
/// В некоторых случаях целесообразно определить операции управления
/// потомками прямо в базовом классе Компонент. Таким образом, вам не нужно
/// будет предоставлять конкретные классы компонентов клиентскому коду,
/// даже во время сборки дерева объектов. Недостаток такого подхода в том,
/// что эти методы будут пустыми для компонентов уровня листа.
func add(component: Component)
func remove(component: Component)
/// Вы можете предоставить метод, который позволит клиентскому коду понять,
/// может ли компонент иметь вложенные объекты.
func isComposite() -> Bool
/// Базовый Компонент может сам реализовать некоторое поведение по умолчанию
/// или поручить это конкретным классам.
func operation() -> String
}
extension Component {
func add(component: Component) {}
func remove(component: Component) {}
func isComposite() -> Bool {
return false
}
}
/// Класс Лист представляет собой конечные объекты структуры. Лист не может
/// иметь вложенных компонентов.
///
/// Обычно объекты Листьев выполняют фактическую работу, тогда как объекты
/// Контейнера лишь делегируют работу своим подкомпонентам.
class Leaf: Component {
var parent: Component?
func operation() -> String {
return "Leaf"
}
}
/// Класс Контейнер содержит сложные компоненты, которые могут иметь вложенные
/// компоненты. Обычно объекты Контейнеры делегируют фактическую работу своим
/// детям, а затем «суммируют» результат.
class Composite: Component {
var parent: Component?
/// Это поле содержит поддерево компонентов.
private var children = [Component]()
/// Объект контейнера может как добавлять компоненты в свой список вложенных
/// компонентов, так и удалять их, как простые, так и сложные.
func add(component: Component) {
var item = component
item.parent = self
children.append(item)
}
func remove(component: Component) {
// ...
}
func isComposite() -> Bool {
return true
}
/// Контейнер выполняет свою основную логику особым образом. Он проходит
/// рекурсивно через всех своих детей, собирая и суммируя их результаты.
/// Поскольку потомки контейнера передают эти вызовы своим потомкам и так
/// далее, в результате обходится всё дерево объектов.
func operation() -> String {
let result = children.map({ $0.operation() })
return "Branch(" + result.joined(separator: " ") + ")"
}
}
class Client {
/// Клиентский код работает со всеми компонентами через базовый интерфейс.
static func someClientCode(component: Component) {
print("Result: " + component.operation())
}
/// Благодаря тому, что операции управления потомками объявлены в базовом
/// классе Компонента, клиентский код может работать как с простыми, так и
/// со сложными компонентами.
static func moreComplexClientCode(leftComponent: Component, rightComponent: Component) {
if leftComponent.isComposite() {
leftComponent.add(component: rightComponent)
}
print("Result: " + leftComponent.operation())
}
}
/// Давайте посмотрим как всё это будет работать.
class CompositeConceptual: XCTestCase {
func testCompositeConceptual() {
/// Таким образом, клиентский код может поддерживать простые компоненты-
/// листья...
print("Client: I've got a simple component:")
Client.someClientCode(component: Leaf())
/// ...а также сложные контейнеры.
let tree = Composite()
let branch1 = Composite()
branch1.add(component: Leaf())
branch1.add(component: Leaf())
let branch2 = Composite()
branch2.add(component: Leaf())
branch2.add(component: Leaf())
tree.add(component: branch1)
tree.add(component: branch2)
print("\nClient: Now I've got a composite tree:")
Client.someClientCode(component: tree)
print("\nClient: I don't need to check the components classes even when managing the tree:")
Client.moreComplexClientCode(leftComponent: tree, rightComponent: Leaf())
}
}
Output.txt: Результат выполнения
Client: I've got a simple component:
Result: Leaf
Client: Now I've got a composite tree:
Result: Branch(Branch(Leaf Leaf) Branch(Leaf Leaf))
Client: I don't need to check the components classes even when managing the tree:
Result: Branch(Branch(Leaf Leaf) Branch(Leaf Leaf) Leaf)
Пример из реальной жизни
Example.swift: Пример из реальной жизни
import UIKit
import XCTest
protocol Component {
func accept<T: Theme>(theme: T)
}
extension Component where Self: UIViewController {
func accept<T: Theme>(theme: T) {
view.accept(theme: theme)
view.subviews.forEach({ $0.accept(theme: theme) })
}
}
extension UIView: Component {}
extension UIViewController: Component {}
extension Component where Self: UIView {
func accept<T: Theme>(theme: T) {
print("\t\(description): has applied \(theme.description)")
backgroundColor = theme.backgroundColor
}
}
extension Component where Self: UILabel {
func accept<T: LabelTheme>(theme: T) {
print("\t\(description): has applied \(theme.description)")
backgroundColor = theme.backgroundColor
textColor = theme.textColor
}
}
extension Component where Self: UIButton {
func accept<T: ButtonTheme>(theme: T) {
print("\t\(description): has applied \(theme.description)")
backgroundColor = theme.backgroundColor
setTitleColor(theme.textColor, for: .normal)
setTitleColor(theme.highlightedColor, for: .highlighted)
}
}
protocol Theme: CustomStringConvertible {
var backgroundColor: UIColor { get }
}
protocol ButtonTheme: Theme {
var textColor: UIColor { get }
var highlightedColor: UIColor { get }
/// other properties
}
protocol LabelTheme: Theme {
var textColor: UIColor { get }
/// other properties
}
/// Button Themes
struct DefaultButtonTheme: ButtonTheme {
var textColor = UIColor.red
var highlightedColor = UIColor.white
var backgroundColor = UIColor.orange
var description: String { return "Default Buttom Theme" }
}
struct NightButtonTheme: ButtonTheme {
var textColor = UIColor.white
var highlightedColor = UIColor.red
var backgroundColor = UIColor.black
var description: String { return "Night Buttom Theme" }
}
/// Label Themes
struct DefaultLabelTheme: LabelTheme {
var textColor = UIColor.red
var backgroundColor = UIColor.black
var description: String { return "Default Label Theme" }
}
struct NightLabelTheme: LabelTheme {
var textColor = UIColor.white
var backgroundColor = UIColor.black
var description: String { return "Night Label Theme" }
}
class CompositeRealWorld: XCTestCase {
func testCompositeRealWorld() {
print("\nClient: Applying 'default' theme for 'UIButton'")
apply(theme: DefaultButtonTheme(), for: UIButton())
print("\nClient: Applying 'night' theme for 'UIButton'")
apply(theme: NightButtonTheme(), for: UIButton())
print("\nClient: Let's use View Controller as a composite!")
/// Night theme
print("\nClient: Applying 'night button' theme for 'WelcomeViewController'...")
apply(theme: NightButtonTheme(), for: WelcomeViewController())
print()
print("\nClient: Applying 'night label' theme for 'WelcomeViewController'...")
apply(theme: NightLabelTheme(), for: WelcomeViewController())
print()
/// Default Theme
print("\nClient: Applying 'default button' theme for 'WelcomeViewController'...")
apply(theme: DefaultButtonTheme(), for: WelcomeViewController())
print()
print("\nClient: Applying 'default label' theme for 'WelcomeViewController'...")
apply(theme: DefaultLabelTheme(), for: WelcomeViewController())
print()
}
func apply<T: Theme>(theme: T, for component: Component) {
component.accept(theme: theme)
}
}
class WelcomeViewController: UIViewController {
class ContentView: UIView {
var titleLabel = UILabel()
var actionButton = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
setup()
}
func setup() {
addSubview(titleLabel)
addSubview(actionButton)
}
}
override func loadView() {
view = ContentView()
}
}
/// Let's override a description property for the better output
extension WelcomeViewController {
open override var description: String { return "WelcomeViewController" }
}
extension WelcomeViewController.ContentView {
override var description: String { return "ContentView" }
}
extension UIButton {
open override var description: String { return "UIButton" }
}
extension UILabel {
open override var description: String { return "UILabel" }
}
Output.txt: Результат выполнения
Client: Applying 'default' theme for 'UIButton'
UIButton: has applied Default Buttom Theme
Client: Applying 'night' theme for 'UIButton'
UIButton: has applied Night Buttom Theme
Client: Let's use View Controller as a composite!
Client: Applying 'night button' theme for 'WelcomeViewController'...
ContentView: has applied Night Buttom Theme
UILabel: has applied Night Buttom Theme
UIButton: has applied Night Buttom Theme