본문 바로가기

객체지향설계

코드스피츠 2강 오브젝트 - 5회차

책 오브젝트를 기반으로 하는 코드스피츠 강의 오브젝트2 - 5회차 를 정리한 내용입니다.

 

1. 코드스피츠 2강 오브젝트 - 1회차

2. 코드스피츠 2강 오브젝트 - 2회차(1)

3.코드스피츠 2강 오브젝트 - 2회차(2)

4.코드스피츠 2강 오브젝트 - 3회차

5.코드스피츠 2강 오브젝트 - 4회차

6.코드스피츠 2강 오브젝트 - 5회차

7.코드스피츠 2강 오브젝트 - 6회차 (Final)

 

이전 강의 요약

Visitor 는 제어를 뺏고 행위만 남겨두는 것

Renderer 가 모든 제어를 가져가고 Visitor 는 행위만 공급하는 것

 

Framework 의 첫번째 단계인 제어를 역전 시키는 DI 하는데 성공했다.

 

Visitor, Composite 를 사용하면 제어 역전을 통해서 제어 구문을 분리해낼 수 있다.

하지만 제어를 분리해내는 것만으로는 Framework는 작동하지 않는다. 

제어 역전은 성공했지만 행위를 역전되지 않기 때문에 각각 객체가 행위를 소유하고 있다. 

행위를 가지고 있는 객체들에게 많은 자유권이 부여되기 때문에 여전히 행위를 캡슐화하는 방법이 필요하다.

 

디자인패턴에서는 행위를 캡슐화할 수 있는 패턴으로 Command 패턴을 제공한다.

 

제어 분리 패턴 행위 분리 패턴
Composite Command
Visitor  

 

코딩!

전체코드는 여기에서 확인할 수 있습니다.

 

Wiki Command Pattern

커맨드 패턴(Command pattern)이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.

 

 

 

 

우리가 앞으로 구현할 Command Pattern 예제를 살펴보자!

Composite, Visitor, Command 가 복합적으로 사용되고 있다는 것을 알 수 있다. 

 

Command 패턴의 역할 별로 해당되는 객체를 분류했다. 

 

명령(command): AddCommand, TitleCommand

수신자(receiver): CommandTask

발동자(invoker): CommandTask

클라이언트(client): Client 

 

 

Command 

 일반적으로 Command 패턴의 대표하는 메소드는 execute() 가 있다.

 Client 가 직접 조작하는 CompositeTaskCommand 패턴에 행위를 위임한다.

 행위만 위임하는 것이 아니라 그 행위를 기억할 수 있다 (undo, redo)

protocol Command {
    func execute(task: CompositeTask)
    func undo(task: CompositeTask)
}

 

Concreate Command

 코드스피츠 1강에서 켄트벡 아저씨가 말했던 대칭성이 있어야 한다. 를 말했던 적이 있다.

 (get, set 과 같이 대칭성이 있어야 한다.)

 

 그러므로 Add 가 있으면 Remove 도 존재해야 한다.

 

여러 Command 객체를 만들어서 코드의 양이 증가한 것 같지만

합쳐보면 코드의 양이 훨씬 작다.

     

결과적으로 디자인패턴이 의미하는 건 무엇일까?

각각의 해법에 대해서 가장 짧은 코드로 짜는 것이다.

Command 패턴 코드의 양이 짧은 이유는 중복 코드를 Command 객체에서 처리하기 때문이다.

 

class AddCommand: Command {
    private let title: String
    private let date: Date
    private var oldTask: CompositeTask?
    
    public init(title: String, date: Date) {
        self.title = title
        self.date = date
    }

    func execute(task: CompositeTask) {
        oldTask = task.addTask(title: title, date: date)
    }

    func undo(task: CompositeTask) {
        if let oldTask = oldTask {
            task.removeTask(task: oldTask)
        }
    }
}


class TitleCommand: Command {
    private final let title: String
    private var oldTitle: String = ""

    public init(title: String) {
        self.title = title
    }

    func execute(task: CompositeTask) {
        oldTitle = task.title
        task.setTitle(title: title)
    }

    func undo(task: CompositeTask) {
        task.setTitle(title: oldTitle)
    }
}

 

CompositeTask

undo, redo를 구현하기 위해서 cursor 라는 개념을 구현했다. 

 

class CommandTask {
    private let task: CompositeTask
    private var _commands = [Command]()

    private var isComplete: Bool = false
    private var saved = [String: String]()
    // redo를 구현하기 위해 cursor 를 선언
    private var cursor = 0
    
    public init(title: String, date: Date) {
        self.task = CompositeTask(title: title, date: date)
    }

    public func addTask(title: String, date: Date) {
        addCommand(command: AddCommand(title: title, date: date))
    }
    
    public func removeTask(task: CompositeTask) {
        addCommand(command: RemoveCommand(task: task))
    }
    /*
     Memonto pattern 을 구현할 때 2가지 경우가 있음.
     1. Seriablization 을 외부에 출력하는 경우
     2. 내부에 기억하는 경우 (일반적인 경우)
      - Key 가 필요
     
     */
    public func save(key: String) {
        let visitor = JsonVisitor()
        let renderer1 = Renderer(factory: { visitor })
        renderer1.render(report: task.getReport(sortType: CompositeSortType.title_asc))
        saved.updateValue(visitor.getJson(), forKey: key)
    }

    public func load(key: String) {
        let json = saved[key]
        // json 순회하면서 복원
        // 과
    }
    func addCommand(command: Command) {
        // 새로운 Command 가 추가되면 현재상태의 cursor 를 기준으로 cursor 보다 큰 Command 를 삭제한다.
        (0..<cursor).forEach {
            _commands.remove(at: $0)
        }

        _commands.append(command)
        // cursor += 1 은 버그를 유발할 수 있다.
        // 그러므로 불변식 (invariant) 로 커서를 증가시켜준다.
        cursor = _commands.count - 1
        command.execute(task: task)
    }
    /*
     여러 Command 객체를 만들어서 코드의 양이 증가한 것 같지만
     합쳐보면 코드의 양이 훨씬 작다.
     
     디자인패턴은?
     각각의 해법에 대해서 가장 짧은 코드로 짜는 것이다.
     
     Command 패턴 코드의 양이 짧은 이유는 중복 코드를 Command 객체에서 처리하기 때문이다.
     */
    public func undo() {
        if cursor < 0 { return }
        _commands[cursor].undo(task: task)
        cursor -= 1
    }

    public func redo() {
        if cursor > _commands.count - 1 { return }
        cursor += 1
        _commands[cursor].execute(task: task)
    }

    public func toggle() {
        // Toggle 이라는 명령이라는 코드가 Type 형태로 변경되었다.
        addCommand(command: Toggle())
    }

    public func setTitle(title: String) {
        // title 이란 상태는 Command 객체가 생성되면 title 에 대한 상태를 기억해야 하는 책임이 있다.
        // 함수의 클로저 변수와 같은 역할을 한다.
        addCommand(command: TitleCommand(title: title))
    }

    public func setDate(date: Date) {
        addCommand(command: DateCommand(date: date))
    }

    func getReport(sortType: CompositeSortType) -> TaskReport {
        task.getReport(sortType: sortType)
    }
}

 

Visitor

코드스피츠 2강 오브젝트 - 4회차에서 Vistor 구현한 예제와 동일하다.

 

실행결과

let root = CommandTask(title: "Root", date: Date())
root.addTask(title: "sub1", date: Date())

let renderer1 = Renderer(factory: { ConsoleVisitor() })
renderer1.render(report: root.getReport(sortType: CompositeSortType.title_asc))
root.undo()
renderer1.render(report: root.getReport(sortType: CompositeSortType.title_asc))

 

1. "Root" 를 만들고 "sub1" 을 Task 로 추가하였다.

2. 현재 Task 를 출력한다. 

3. undo 를 실행한다.

4. 현재 Task 를 출력한다. 

 

 

 [ ] Root (2019-10-24 14:19:17 +0000)
- [ ] sub1 (2019-10-24 14:19:17 +0000)

========= after undo ==========
 [ ] Root (2019-10-24 14:19:17 +0000)

 

sub1 을 추가하였고 undo를 수행한 결과 sub1 이 삭제된 것을 확인할 수 있다. 😀

 

Framework로 가는 마지막은?

Save / Load 할 수 있어야 한다.

 

Memento pattern

객체를 이전 상태로 되돌릴 수 있는 기능을 제공

 

 

마무리

제어와 행위를 분리시키는 패턴이라는 개념을 처음 알게 되었다.

나는 중복 코드를 좀 더 논리적으로 제거할 수 있는 방법을 알게 된 것 같아서 기쁘다. 

 

전체코드는 여기에서 확인할 수 있습니다.