
Kompozyt w języku Swift
Kompozyt to strukturalny wzorzec projektowy umożliwiający komponowanie struktury drzewiastej z obiektów i traktowanie jej jak pojedynczy obiekt.
Kompozyt stał się dość popularnym rozwiązaniem wielu problemów gdzie w grę wchodzi struktura drzewa. Zaletą tego wzorca jest możliwość uruchamiania metod rekurencyjnie na wszystkich elementach struktury i sumowanie wyników ich działania.
Złożoność:
Popularność:
Przykłady użycia: Wzorzec Kompozyt jest dość powszechny w kodzie Swift. Często stosuje się go do modelowania hierarchii komponentów interfejsu użytkownika lub kodu który działa na grafach.
Identyfikacja: Jeśli klasy wszystkich obiektów w drzewie należą do jednej hierarchii to najprawdopodobniej mamy do czynienia z kompozytem. Jeśli dodatkowo metody tych klas delegują zadania obiektom-dzieciom wchodzącym w skład tego drzewa i robią to za pośrednictwem klasy bazowej lub bazowego interfejsu hierarchii, to na pewno jest to kompozyt.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Kompozyt ze szczególnym naciskiem na następujące kwestie:
- Z jakich składa się klas?
- Jakie role pełnią te klasy?
- W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć następujący przykład, oparty na prawdziwym przypadku użycia Swift.
Example.swift: Przykład koncepcyjny
import XCTest /// The base Component class declares common operations for both simple and /// complex objects of a composition. protocol Component { /// The base Component may optionally declare methods for setting and /// accessing a parent of the component in a tree structure. It can also /// provide some default implementation for these methods. var parent: Component? { get set } /// In some cases, it would be beneficial to define the child-management /// operations right in the base Component class. This way, you won't need /// to expose any concrete component classes to the client code, even during /// the object tree assembly. The downside is that these methods will be /// empty for the leaf-level components. func add(component: Component) func remove(component: Component) /// You can provide a method that lets the client code figure out whether a /// component can bear children. func isComposite() -> Bool /// The base Component may implement some default behavior or leave it to /// concrete classes. func operation() -> String } extension Component { func add(component: Component) {} func remove(component: Component) {} func isComposite() -> Bool { return false } } /// The Leaf class represents the end objects of a composition. A leaf can't /// have any children. /// /// Usually, it's the Leaf objects that do the actual work, whereas Composite /// objects only delegate to their sub-components. class Leaf: Component { var parent: Component? func operation() -> String { return "Leaf" } } /// The Composite class represents the complex components that may have /// children. Usually, the Composite objects delegate the actual work to their /// children and then "sum-up" the result. class Composite: Component { var parent: Component? /// This fields contains the conponent subtree. private var children = [Component]() /// A composite object can add or remove other components (both simple or /// complex) to or from its child list. func add(component: Component) { var item = component item.parent = self children.append(item) } func remove(component: Component) { // ... } func isComposite() -> Bool { return true } /// The Composite executes its primary logic in a particular way. It /// traverses recursively through all its children, collecting and summing /// their results. Since the composite's children pass these calls to their /// children and so forth, the whole object tree is traversed as a result. func operation() -> String { let result = children.map({ $0.operation() }) return "Branch(" + result.joined(separator: " ") + ")" } } class Client { /// The client code works with all of the components via the base interface. static func someClientCode(component: Component) { print("Result: " + component.operation()) } /// Thanks to the fact that the child-management operations are also /// declared in the base Component class, the client code can work with both /// simple or complex components. static func moreComplexClientCode(leftComponent: Component, rightComponent: Component) { if leftComponent.isComposite() { leftComponent.add(component: rightComponent) } print("Result: " + leftComponent.operation()) } } /// Let's see how it all comes together. class CompositeConceptual: XCTestCase { func testCompositeConceptual() { /// This way the client code can support the simple leaf components... print("Client: I've got a simple component:") Client.someClientCode(component: Leaf()) /// ...as well as the complex composites. 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: Wynik działania
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)
Przykład z prawdziwego życia
Example.swift: Przykład z prawdziwego życia
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: Wynik działania
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