책 오브젝트 14장를 기반으로 하는 코드스피츠 강의 오브젝트2 - 4회차 를 정리한 내용입니다.
이 포스트는 Java 예제를 Swift 예제로 바꿔서 다루고 있습니다.
Library와 Framework 차이점?
Framework 라고 말할 수 있다면 제어 역전이 되어 있는지 아닌지? 를 보면 알 수 있다.
제공한 메서드가 어느 시점에 호출될지를 우리가 결정하지 못한다.
이 제어를 Framework 에서 담당하게 된다.
Library 는 우리가 제어를 할 수 있다.
즉, Library 는 값의 획득만 위임할 수 있다.
제어를 처리하는 Class 가 안정화되면 다양한 객체를 공급하도록 해야 프로그램이 안정적으로 동작하게 된다.
Composite Pattern
컴포지트 패턴(Composite pattern)이란 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다.
대표적으로 파일 시스템이 있다.
디자인 패턴의 일반적인 커리큘럼
1. 좋은 상속 예로 Templated method pattern
2. 상속은 조합 폭발이 일어나기 때문에 합성으로 바꾸면서 Strategy pattern
3. Strategy 객체를 많이 만들다 보면 포괄적인 객체와 단일 객체를 합성할 수 있는 Composite pattern
4. Strategy pattern이 Collector <>- Element 이고 Element => Element 간 연결하면서 Decorator pattern
5. Decorator pattern 는 중간에 멈출 수 없기 때문에 중간에 멈출수 있는 Chain of responsibility 를 배운다.
6. Composite pattern 스스로 Loop 를 외부에 위임하는 Visitor pattern 을 배운다.
7. 마지막으로 Command pattern 을 배운다.
8. Strategy pattern <-> Strategy pattern 연결하기 위해서 Adapter pattern을 배운다.
Command pattern 는 지연 실행 기능이 있어서 함수형 프로그래밍과 연관이 있다.
코딩!
앞으로 만들어볼 예제를 UML 로 그려보았다.
전체소스는 여기에서 확인할 수 있습니다.
TaskReport, CompositeTask 는 Composite Pattern 이다.
WIKI 에 있는 Composite 와는 흡사 모양이 다르다.
현업에서는 하나의 통합된 실행기와 처리기가 통합되는 경우가 흔히 발생하는 경우이다.
우리는 하나로 통합된 디자인 패턴 객체와 여러 Class 로 분리된 객체 두 가지 방법을 모두 다룰 수 있어야만 한다.
CompositeTask
/*
Composite pattern 의 그룹 객체를 가진다.
*/
class CompositeTask {
private let _title: String
private let _date: Date
private var isComplete: Bool
// private var _taskReport: TaskReport = TaskReport()
private var _list = [CompositeTask]()
public init(title: String, date: Date, isComplete: Bool = false) {
self._title = title
self._date = date
self.isComplete = isComplete
}
public func toggle() {
isComplete = !isComplete
}
// func addTask(task: Task) {
func addTask(title: String, date: Date) {
_list.append(CompositeTask(title: title, date: date))
}
func removeTask(task: CompositeTask) {
_list.removeAll {
$0 === task
}
}
func getReport(sortType: CompositeSortType) -> TaskReport {
let report = TaskReport(task: self)
for t in _list {
report.add(report: t.getReport(sortType: sortType))
}
return report
}
}
extension CompositeTask {
var title: String {
return _title
}
var date: Date {
return _date
}
var isCompleted: Bool {
return isComplete
}
}
func addTask(task: CompositeTask) 를 하지 않고 구성 속성을 받고 있는 이유는?
func addTask(title: String, date: Date)
CompositeTask 를 외부에서 생성할 수 있도록 하는 순간 CompositeTask 생성자 수정의 여파를 막도록 하기 위함이다.
그러므로 오직 addTask() 안에서만 CompositeTask 를 생성하도록 한다.
CompositeTask 수정의 여파가 CompositeTask 까지만 미치도록 한다.
CompositeSortType
typealias CompositeCamparable = (CompositeTask, CompositeTask) -> Void
enum CompositeSortType {
case title_asc
case title_desc
case date_asc
case date_desc
/*
Java 는 enum 의 안정성을 위해서 코드중복을 방치한다.
Class 객체 생성으로 생성시점을 미룰 순 있지만 동시성 문제를 안고가야한다.
일반적으로 엔터프라이즈에서는 동시성 문제가 훨씬 무섭기 때문에 코드 중복을 감수하더라도 enum 을 사용한다.
*/
func comparable(a: CompositeTask, b: CompositeTask) -> Bool {
switch self {
case .title_asc:
return a.title < b.title
case .title_desc:
return a.title > b.title
case .date_asc:
return a.date < b.date
case .date_desc:
return a.date > b.date
}
}
}
동시성 문제는 코드의 중복을 방치하는 것보다 더 큰 위협이 된다.
Java 에서 enum 은 static 초기화 이전에 OS 에 의해서 생성된다.
Thread 가 시작되기 전에 만들어지기 때문에 Thread safe 하다.
func comparable(a: CompositeTask, b: CompositeTask) -> Bool
비교하는 책임을 전략 객체로 바꿀순 있지만 그대신 동시성 대가를 치뤄야 한다.
일반적으로 엔터프라이즈에서는 동시성 문제가 훨씬 무섭기 때문에 코드 중복을 감수하더라도 enum 을 사용한다.
Renderer
typealias SupplierVisitorFactory = () -> Visitor
class Renderer {
// private let visitor: Visitor
// public init(visitor: Visitor) {
// self.visitor = visitor
// }
/*
Render 를 할때마다 새로운 visitor 가 생성되기 때문에 동시성 문제를 해결할 수 있다.
*/
private let factory: SupplierVisitorFactory
public init(factory: @escaping SupplierVisitorFactory) {
self.factory = factory
}
/*
순회하면서 그리는 함수를 정의
depth 는 사용자가 이 함수를 최초 실행될 때 depth 를 0으로 준다는 보장이 없기 때문에 private 이다.
*/
private func render(visitor: Visitor, report: TaskReport, depth: Int) {
// 제어를 실행하는 객체와 제어를 통제하는 객체를 분리하여야 한다.
// 여기서는 제어를 실행하고 있다. for loop)
visitor.drawTask(task: report.task, depth: depth)
for report in report._list {
render(visitor: visitor, report: report, depth: depth + 1)
}
visitor.end(depth: depth)
}
/*
사용자가 호출할 수 있도록 public 으로 지정한다.
*/
public func render(report: TaskReport) {
let visitor = self.factory()
render(visitor: visitor, report: report, depth: 0)
}
}
생성자에서 Visitor 의 구체 객체를 받는것이 아니라 Factory(생성기) 를 전달받는다.
이는 render 함수를 호출할 때 새로운 Visitor 가 생성되기 때문에 동시성 문제를 해결할 수 있다.
Renderer 는 2가지 render() 함수를 가지고 있다.
차이점은 접근지시자이이다.
외부 함수와 내부 함수로 구별되었을까?
사용자가 depth 의 인자를 0으로 줄 것이라고 예상할 수 없기 때문에 외부 함수에서 0을 지정하여 넘겨주기 위해서이다.
private render(visitor: Visitor, report: TaskReport, depth: Int)
public render(report: TaskReport)
Visitor
WIKI 에 있는 Visitor pattern의 UML 이다.
이렇게 분리를 하면 구조를 수정하지 않고도 실질적으로 새로운 동작을 기존의 객체 구조에 추가할 수 있게 된다.
protocol Visitor {
func drawTask(task: CompositeTask, depth: Int)
func end(depth: Int)
}
class ConsoleVisitor: Visitor {
func drawTask(task: CompositeTask, depth: Int) {
var padding = ""
(0..<depth).forEach { _ in
padding += "-"
}
let isCompletedOut = task.isCompleted ? "[v]" : "[ ]"
print("\(padding) \(isCompletedOut) \(task.title) (\(task.date))")
}
func end(depth: Int) {
}
}
class JsonVisitor: Visitor {
func getPadding(depth: Int) -> String {
var padding = ""
(0..<depth).forEach { _ in
padding += " "
}
return padding
}
func drawTask(task: CompositeTask, depth: Int) {
let padding = getPadding(depth: depth)
print("\(padding) {")
print("\(padding) title: \"\(task.title)\",")
print("\(padding) date: \"\(task.date)\",")
print("\(padding) isComplete: \"\(task.isCompleted)\",")
print("\(padding) sub: [ ")
}
func end(depth: Int) {
let padding = getPadding(depth: depth)
print("\(padding) ]")
print("\(padding) },")
}
}
Visitor 에 2가지 Lifecycle 을 얻을 수 있다.
[drawTask, end]
객체지향 설계에서 큰 흐름은 Template method pattern 으로 강제를 시키고 내부는 유연하게 Visitor pattern을 사용한다.
예를 들면
Android 에서 Acitivity Lifecycle [onCreate, onResume, onPause] 등은 Lifecycle 타는 거대한 Visitor 다.
Renderer 는 Strategy 객체를 공급한 결과가 된다.
그렇다면 큰 범주에서 보면 Visitor pattern 도 Strategy pattern 일까?
Visitor VS Strategy
제어 능력을 뺏고 행위만 남겨두는 것을 Visitor 라고 부른다.
제어 역전이란 무엇일까?
제어 역전은 참조를 거꿀로 하는 것이 아니다.
일반적으로 알고 있는 의존성 역전과 제어역전의 의미는 다르다.
의존성 역전은 구상 객체를 아는 것이 아니라 구상 객체의 추상 객체를 아는 것이다.
제어 역전(IoC)은 제어 구문이 Lifecycle 을 가지고 있는 객체를 소비하는 것이다.
Main
func testConsoleVisitor() {
let root = CompositeTask(title: "Root", date: Date())
root.addTask(title: "sub1", date: Date())
root.addTask(title: "sub2", date: Date())
let report: TaskReport = root.getReport(sortType: CompositeSortType.title_asc)
let sub1 = report.tasks[0].task
let sub2 = report.tasks[1].task
sub1.addTask(title: "sub1_1", date: Date())
sub1.addTask(title: "sub1_2", date: Date())
sub2.addTask(title: "sub2_1", date: Date())
sub2.addTask(title: "sub2_2", date: Date())
let renderer1 = Renderer(factory: { ConsoleVisitor() })
renderer1.render(report: root.getReport(sortType: CompositeSortType.title_asc))
}
실행결과:
[ ] Root (2019-10-22 06:01:44 +0000)
- [ ] sub1 (2019-10-22 06:01:44 +0000)
-- [ ] sub1_1 (2019-10-22 06:01:44 +0000)
-- [ ] sub1_2 (2019-10-22 06:01:44 +0000)
- [ ] sub2 (2019-10-22 06:01:44 +0000)
-- [ ] sub2_1 (2019-10-22 06:01:44 +0000)
-- [ ] sub2_2 (2019-10-22 06:01:44 +0000)
func testJsonVisitor() {
let root = CompositeTask(title: "Root", date: Date())
root.addTask(title: "sub1", date: Date())
root.addTask(title: "sub2", date: Date())
let report: TaskReport = root.getReport(sortType: CompositeSortType.title_asc)
let sub1 = report.tasks[0].task
let sub2 = report.tasks[1].task
sub1.addTask(title: "sub1_1", date: Date())
sub1.addTask(title: "sub1_2", date: Date())
sub2.addTask(title: "sub2_1", date: Date())
sub2.addTask(title: "sub2_2", date: Date())
let renderer1 = Renderer(factory: { JsonVisitor() })
renderer1.render(report: root.getReport(sortType: CompositeSortType.title_asc))
}
실행결과
{
title: "Root",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
{
title: "sub1",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
{
title: "sub1_1",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
]
},
{
title: "sub1_2",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
]
},
]
},
{
title: "sub2",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
{
title: "sub2_1",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
]
},
{
title: "sub2_2",
date: "2019-10-22 06:17:05 +0000",
isComplete: "false",
sub: [
]
},
]
},
]
},
마무리
Composite Pattern 은 내부 속성의 캡슐화를 유지하면서 개별, 그룹의 책임을 수행하는 패턴,
Visitor Pattern 은 캡슐화를 외부에 노출하고 책임을 외부에 노출하는 패턴으로 알고 있었다.
Visitor pattern UML
ElementB, ElementA 의 책임은 중복된다.
그 중복을 없애기 위해 하나의 Visitor pattern 을 적용하는 것으로 이해하고 있다.
제어의 관점에서는 아직 뚜렷하게 구체적으로 설명하기 어려운 상태이다.
조금 더 실력이 쌓이면 이에 대해 포스팅을 하겠다.
전체소스는 여기에서 확인할 수 있습니다.
'객체지향설계' 카테고리의 다른 글
코드스피츠 2강 오브젝트 - 5회차 (0) | 2019.10.31 |
---|---|
코드스피츠 2강 오브젝트 - 3회차 (0) | 2019.10.30 |
코드스피츠 2강 오브젝트 - 2회차(2) (0) | 2019.10.29 |
코드스피츠 2강 오브젝트 - 1회차 (0) | 2019.10.28 |
코드스피츠 2강 오브젝트 - 2회차(1) (0) | 2019.10.28 |