Composite Pattern에 대해 알아봅시다.
Composite 패턴은 복합체 패턴이라고도 불리는 만큼, Composite는 합성물이라는 의미를 가지고 있습니다.
실제 패턴에서도 그러한지 살펴봅시다.
1. 목적
객체를 트리 구조로 구성한 다음 개별 객체와 객체 그룹을 동일하게 다룰 수 있도록 하는 패턴입니다.
2. 문제 상황
객체를 트리 구조로 구성해야 한다는 점에서 어플리케이션의 핵심 모델을 트리구조로 표현할 수 있는 경우에만 패턴을 사용하는 의미가 있습니다.
Products와 Boxes 두 가지 객체가 있습니다.
Box에는 여러 개의 Products와 Boxes이 포함될 수 있습니다.
물론 다시 하위 Boxes에도 여러 개의 Products와 Boxes가 다시 포함될 수 있습니다.
그림을 거꾸로 보면 나무와 같은 모양입니다.
이러한 상황에서 모든 상자를 풀어 내부의 모든 제품을 살펴본 다음 가격의 합계를 구해야 한다면 어떨까요?
실제 박스였다면 비교적 간단하게 접근이 가능하지만,
프로그램의 세계에서는 Products와 Boxes 내부와 상자들의 중첩 수준 등의 세부 사항을 미리 알고 있어야 가능한 일입니다.
3. 해결책
Composite 패턴은 총가격을 계산하는 메서드를 포함한 인터페이스를 이용하여 문제를 해결합니다.
제품의 경우에는 단순히 제품의 가격을 반환하고,
상자의 경우에는 상자에 포함된 각 항목으로부터 가격을 전달받아 총 가격을 반환합니다.
어디선가 본 거 같은 로직인가요? 맞습니다.
DFS의 작동원리와 조금 비슷합니다.
[Algorithm] Depth-First Search(DFS)
이전에 BFS(Breadth-First Search)에 대해 공부했었습니다.오늘은 그 친구격인 Depth-First Search(깊이우선탐색)에 대해서 알아보도록하겠습니다.마찬가지로 DFS라는 더 많이 불립니다. 특징BFS와 마찬가지
littlemoom.tistory.com
Composite 패턴은 객체 트리의 모든 컴포넌트들에 대해 재귀적인 행동을 부여합니다.
이렇게 되면 상자 안의 물건이 상자인지 제품인지 알 필요 없이 모두 같은 방식의 인터페이스대로 작동하게 됩니다.
4. 구조
Product와 Box로 구조를 파악해 봅시다.
먼저 Component를 설계합니다. 여기서는 가격을 반환받는 함수가 포함되어야 합니다.
이후 Composite 설계를 위해서 id 프로토콜을 추가합니다.
// Component
protocol Item: Identifiable {
var id: UUID { get set }
// execute()
func getPrice() -> Double
}
두 번째로 Leaf에 대해 설계합니다. 여기서는 Product가 Leaf가 됩니다.
// Leaf
class Product: Item {
var id = UUID()
private var price: Double
init(price: Double) {
self.price = price
}
// execute()
func getPrice() -> Double {
return self.price
}
}
세 번째로 Composite는 Box입니다.
// Composite
class Box: Item {
var id: UUID = UUID()
// children: Component
private var items: [UUID: any Item] = [:]
// add(c: Component)
func addItem(i: Item) {
items[i.id] = i
}
// remove(c: Component)
func removeItem(i: Item) {
items.removeValue(forKey: i.id)
}
// getChildren(): Component[]
func getItems() -> [UUID: any Item] {
return self.items
}
// execute()
func getPrice() -> Double {
// Code
}
}
Composite의 execute()에 해당하는 Box의 getPrice()는 어떻게 설계해야 할까요?
Item들을 순회하면서 그저 가격을 반환받으면 됩니다.
func getPrice() -> Double {
return self.items.reduce(0.0) { total, item in
total + item.value.getPrice()
}
}
문제 상황의 그림 다시 가져왔습니다.
이를 코드로 표현하면 다음과 같습니다.
let hammer = Product(price: 10.0)
let phone = Product(price: 20.0)
let headphones = Product(price: 30.0)
let charger = Product(price: 40.0)
let receipt = Product(price: 40.0)
let smallBox1 = Box()
smallBox1.addItem(i: hammer)
let smallBox2 = Box()
smallBox2.addItem(i: phone)
smallBox2.addItem(i: headphones)
let smallBox3 = Box()
smallBox2.addItem(i: charger)
let middleBox = Box()
middleBox.addItem(i: smallBox2)
middleBox.addItem(i: smallBox3)
let bigBox = Box()
bigBox.addItem(i: smallBox1)
bigBox.addItem(i: middleBox)
bigBox.addItem(i: receipt)
이제 총가격을 구해볼까요?
print(bigBox.getPrice())
// 140.0
한 번에 구해지는군요!
5. 장단점
객체 트리와 기존 코드를 수정하지 않고도 앱에 Item을 준수하는 새로운 유형의 클래스를 추가할 수 있기에
SOLID 원칙 중 OCP(Open/Closed Principle, 개방/폐쇄 원칙)이 준수됩니다.
하지만 클래스간에 기능이 너무 다르다면 공통 인터페이스를 제공하지 어렵습니다.
강제로 공통 인터페이스를 만들어 결합시킨다면 클래스를 과도하게 일반화해야하는 상황이 생길 수 있습니다.
6. 실제 사용 사례
Composite 패턴이 실제로 사용되는 사례는 File System이 있습니다.
예시처럼 폴더에는 파일과 또 다른 폴더가 포함될 수 있습니다.
iOS에서는 Group UI Component를 설계할 때 사용될 수 있습니다.
하나의 View 아래에 여러가지 Input View 혹은 또 다른 View가 존재할 수 있습니다.
조금은 억지스럽지만 다음과 같이 설계됩니다.
protocol FormComponent {
func render()
}
class TextField: FormComponent {
func render() {
print("Rendering a text field")
}
}
class Button: FormComponent {
func render() {
print("Rendering a button")
}
}
class Form: FormComponent {
private var components = [FormComponent]()
func addComponent(_ component: FormComponent) {
components.append(component)
}
func render() {
print("Rendering form:")
components.forEach { $0.render() }
}
}
Conclusion
Composite 패턴을 활용하면 복잡한 계층 구조의 클래스를 단일 객체처럼 사용할 수 있습니다.
장단점에서 언급되었듯 코드의 확장성과 유연성이 증가합니다.
UI 설계에 있어 직접 적용해보면 좋을듯 합니다.
Reference
Composite
/ Design Patterns / Structural Patterns Composite Also known as: Object Tree Intent Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects. Problem
refactoring.guru
'Design Pattern > GoF - Structural Patterns' 카테고리의 다른 글
[GoF Design Patterns] Flyweight (2) | 2024.11.28 |
---|---|
[GoF Design Patterns] Facade (0) | 2024.11.27 |
[GoF Design Patterns] Decorator (3) | 2024.11.13 |
[GoF Design Patterns] Bridge (4) | 2024.10.28 |
[GoF Design Patterns] Adapter (1) | 2024.10.24 |