3. 디자인 패턴 - 행동 패턴
행동 패턴
종류: Pattern
1. Interpreter
번역기처럼 입력값을 받아서 약속된 값을 돌려주는 것
사용되는 경우
- 간단한 문법 해석에 적합합니다.
- SQL과 같은 계층적 언어를 해석하는데 사용됩니다.
- 통신 Protocol을 설명하기 위해 사용되는 언어들의 해석에도 사용됩니다.
구조
- context: 해석이 필요한 문장
- abstractExpression: protocol
- terminalExpression: 해석하는 독립적인 코드
- nonTerminalExpression: 하나 이상의 terminalExpresion을 참조하여 해석하는 코드
protocol AbstractExpression {
func interpret(_ context: Context)
}
final class Context {
enum Gender {
case male
case female
}
private var persons: [String: Gender] = [:]
func getPersons() -> [String: Gender] {
self.persons
}
func lookup(name: String) -> Gender {
return self.persons[name]!
}
func assign(expression: TerminalExpression, gender: Gender) {
self.persons[expression.name] = gender
}
}
final class TerminalExpression: AbstractExpression {
let name: String
init(name: String) {
self.name = name
}
func interpret(_ context: Context) {
print(context.lookup(name: self.name))
}
}
final class NonterminalExpression: AbstractExpression {
private var maleCount: Int = 0
private var femaleCount: Int = 0
func interpret(_ context: Context) {
for person in context.getPersons() {
if person.value == .male {
maleCount += 1
} else if person.value == .female {
femaleCount += 1
}
}
print("남자는 \(maleCount)명, 여자는 \(femaleCount)명입니다")
}
}
let context = Context()
let a = TerminalExpression(name: "김연경")
let b = TerminalExpression(name: "손흥민")
let c = TerminalExpression(name: "류현진")
context.assign(expression: a, gender: .female)
context.assign(expression: b, gender: .male)
context.assign(expression: c, gender: .male)
let expression = NonterminalExpression()
expression.interpret(context)
a.interpret(context) // female
2. Template
동작 상의 알고리즘의 프로그램 뼈대를 정의한다. 따라서 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해줌
구조
class SteakMaker {
final func makeSteak() {
grill()
pourSauce()
plating()
print("----------")
}
func grill() {
print("고기를 굽습니다.")
}
func pourSauce() {
print("소스를 붓습니다.")
}
func plating() {
print("플레이팅을 합니다.")
}
}
class ChefYagom: SteakMaker {
override func grill() {
print("고기를 겉에만 아주 살짝 익힙니다.")
}
override func plating() {
print("플레이팅 후 파슬리 가루를 마지막으로 뿌려줍니다.")
}
}
let steakMaker = SteakMaker()
let odong = ChefOdong()
let yagom = ChefYagom()
steakMaker.makeSteak()
odong.makeSteak()
yagom.makeSteak()
/*
고기를 굽습니다.
소스를 붓습니다.
플레이팅을 합니다.
----------
고기를 굽습니다.
소스를 접시 한 켠에 붓습니다.
플레이팅을 합니다.
----------
고기를 겉에만 아주 살짝 익힙니다.
소스를 붓습니다.
플레이팅 후 파슬리 가루를 마지막으로 뿌려줍니다.
----------
*/
3. Chain of Responsibility
request를 처리하는 객체들이 연결되어있고 자기가 담당하는 거면 처리하고 아니면 넘겨준다
구조
- handler
- 인터페이스로 요청을 처리하는 단일 메서드 포함
- baseHandler
- 모든 핸들러가 공통적으로 가져야하는 보일러 플레이트 코드를 구현하는 코드
- protocol extension으로 대체 가능!
- concreteHandler
- 실제로 처리하는 핸들러
- client
- 실제 핸들러들을 가지고 순서를 연결
// Handler
protocol Handler: AnyObject {
var nextHandler: Handler? { get set }
func setNext(handler: Handler)
func handle(request: String) -> String?
}
// base handler
extension Handler {
func setNext(handler: Handler) {
if self.nextHandler == nil {
self.nextHandler = handler
} else {
self.nextHandler?.setNext(handler: handler)
}
}
func handle(request: String) -> String? {
return nextHandler?.handle(request: request)
}
}
// Concrete Handler
class TomatoHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
print("토마토 기계 전달 완료")
if request == "토마토" {
return "토마토 슬라이스 완성!"
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "요청에 실패했습니다."
}
}
}
}
class OnionHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
print("양파 기계 전달 완료")
if request == "양파" {
return "양파 슬라이스 완성!"
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "요청에 실패했습니다."
}
}
}
}
class LettuceHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
print("양상추 기계 전달 완료")
if request == "양상추" {
return "양상추 손질 완성!"
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "요청에 실패했습니다."
}
}
}
}
class PickleHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
print("피클 기계 전달 완료")
if request == "피클" {
return "피클 슬라이스 완성!"
} else {
if let response = nextHandler?.handle(request: request) {
return response
} else {
return "요청에 실패했습니다."
}
}
}
}
// Client
class Client {
private var firstHandler: Handler?
func request(request: String) -> String {
return self.firstHandler?.handle(request: request) ?? "firstHandler를 설정해주세요"
}
func addHandler(handler: Handler) {
if let firstHandler = firstHandler {
firstHandler.setNext(handler: handler)
} else {
self.firstHandler = handler
}
}
}
// 사용해보기
let client = Client()
let tomatoMachine = TomatoHandler()
let onionMachine = OnionHandler()
let lettuceMachine = LettuceHandler()
let pickleMachine = PickleHandler()
client.addHandler(handler: tomatoMachine)
client.addHandler(handler: onionMachine)
client.addHandler(handler: lettuceMachine)
client.addHandler(handler: pickleMachine)
print(client.request(request: "양상추"))
/*
토마토 기계 전달 완료
양파 기계 전달 완료
양상추 기계 전달 완료
양상추 손질 완성!
*/
4. Command
요청을 객체의 형태로 캡슐화해서 사용자가 보낸 요청을 나중에 이용할 수 있도록 저장/로깅/취소하는 패턴
ex) 카페의 주문서가 바로 command, 주문을 받는 사람은 커피 만드는 과정은 알 필요 없이 전달만 해주면 됨!
구조
- Invoker: 명령을 받아서 요청을 시작하는 객체
- Receiver: 수신자, 비즈니스 로직을 가지고 있고 실제로 수행하는 객체
- command: 명령을 실행하기 위한 인터페이스
- concrete command: 인터페이스를 채택해서 실제로 실행되는 개체, receiver를 가지고 있고 실제로 실행되는 행위를 위임
문제점
// receiver
struct TV {
func turnOn() {
print("TV가 켜졌습니다")
}
}
// invoker
struct HomeApp {
private let tv: TV // invoker가 receiver를 가지고 있어 서로 의존하고 있다!!!
init(tv: TV) {
self.tv = tv
}
func pressButton() {
tv.turnOn()
}
}
func turnOnTV() {
let tv = TV()
let homeApp = HomeApp(tv: tv)
homeApp.touch()
}
turnOnTV() // TV가 켜졌습니다
// Command
protocol Command {
func execute()
}
// Concrete Command
struct turnOnTVCommand: Command {
private var tv: TV // Receiver
init(tv: TV) {
self.tv = tv
}
func execute() {
tv.turnOn() // Receiver의 메서드 호출
}
}
struct changeChannelCommand: Command {
private var tv: TV // Receiver
private var channel: String
init(tv: TV, channel: String) {
self.tv = tv
self.channel = channel
}
func execute() {
tv.change(channel) // Receiver의 메서드 호출
}
}
// Receiver
struct TV {
func turnOn() {
print("TV가 켜졌습니다.")
}
func change(_ channel: String) {
print("TV가 \\(channel)번 채널을 틀었습니다.")
}
}
// Invoker
final class HomeApp {
private var redButton: Command? // Protocol인 Command를 가짐
private var numberButton: Command?
func setCommand(redButton: Command, numberButton: Command) {
self.redButton = redButton
self.numberButton = numberButton
}
func pressRedButton() {
redButton?.execute() // Command의 excute()를 호출
}
func pressNumberButton() {
numberButton?.execute()
}
}
let homeApp = HomeApp()
let newTV = TV()
let turnOnTV = turnOnTVCommand(tv: newTV)
let changeChannelOfTV = changeChannelCommand(tv: newTV, channel: "11")
homeApp.setCommand(redButton: turnOnTV, numberButton: changeChannelOfTV)
homeApp.pressRedButton() // TV가 켜졌습니다.
homeApp.pressNumberButton() // TV가 11번 채널을 틀었습니다.
- invoker와 receiver와의 의존성이 없어서 분리 가능
5. Iterator
컬렉션의 요소들의 기본표현을 노출하지 않고 순회할 수 있는 패턴
구조
- Container – Iterator를 만드는 역할
makeIterator()
원하는 객체의 Iterator를 리턴
- Iterator – 객체의 요소에 접근하는 방법을 제공
next()
객체에서 다음 요소를 리턴prev()
객체에서 이전 요소를 리턴getCurrent()
현재 위치의 요소를 리턴isLast()
객체의 마지막 요소인지 Bool값을 리턴- (
prev()
,getCurrent()
,isLast()
는 필요에 따라 선택적으로 구현하면 됩니다.)
// Iterator 패턴 구현
struct Food {
let name: String
}
struct Foods {
let foods: [Food]
}
struct FoodsIterator: IteratorProtocol {
private var current = 0
private let foods: [Food]
init(foods: [Food] {
self.foods = foods
}
mutating func next() -> Food? {
defer { current += 1 }
return foods.count > current ? foods[food] : nil
}
}
extension Foods: Sequence {
func makeIterator() -> FoodsIterator {
return FoodsIterator(foods: foods)
}
}
// Iterator 사용 - Client
let favoriteFoods = Foods(foods: [Food(name: "마라탕")])
for food in favoriteFoods {
print("나는 \(food)을/를 좋아해!")
}
6. Mediator
다른 객체들과 직접 통신하지 않으면서 중재자를 통해 통신하는 패턴
- 바로 통신하는 것보다는 사이에 중재자를 두는 게 훨씬 결합도를 낮추는 좋은 방법!
구조
- component
- 비즈니스 로직을 포함하는 클래스
- mediator에 대한 참조를 가지고 있음
- mediator
- 컴포넌트간의 통신을 위한 인터페이스
- concreteMediator
- component에 대한 참조를 가지고 component간의 통신을 매개
// Mediator
protocol Mediator {
func notify(sender: Component, price: Int)
}
// Component
class Component {
var mediator: Mediator?
}
// concrete component
class Seller: Component {
var isSold: Bool = false
func sell(price: Int) {
print("판매자: 그 가격에 거래할게요.")
mediator?.notify(sender: self, price: price)
}
}
class Buyer: Component {
var money: Int = 50000
func buy() {
print("구매자: \(money)원에 살게요.")
self.mediator?.notify(sender: self, price: money)
}
func deposit(price: Int) {
print("구매자: \(price)원 입금했습니다.")
money -= price
}
}
// ConcreteMediator
class Broker: Mediator {
private let seller: Seller
private let buyer: Buyer
func notify(sender: Component, price: Int) {
if sender is Buyer {
seller.sell(price: price)
}
if sender is Seller {
buyer.deposit(price: price)
print("중매자: 거래가 성사되었습니다.")
}
}
init(seller: Seller, buyer: Buyer) {
self.seller = seller
self.buyer = buyer
}
}
let seller = Seller()
let buyer = Buyer()
let broker = Broker(seller: seller, buyer: buyer)
seller.mediator = broker
buyer.mediator = broker
buyer.buy()
/*
구매자: 50000원에 살게요.
판매자: 그 가격에 거래할게요.
구매자: 50000원 입금했습니다.
중매자: 거래가 성사되었습니다.
*/
7. Memento
객체를 이전 상태로 되돌릴 수 있는 기능을 제공하는 소프트웨어 디자인 패턴
구조
- originator
- 스냅샷을 생성할 수 있고 복원도 가능
- memento
- 스냅샷역할을 하는 객체
- caretaker
- 언제 캡처하고 언제 복원되어야 하는지 아는 댇체
- 과거로 가야할 때 메멘토를 스택에서 가져와서 오리지네이터의 복원 메소드에 전달
//'나이'라는 상태를 지니고 있는 오리지네이터
class Originator {
private var age: Int
init(age: Int) {
self.age = age
}
func setAge(to number: Int) {
age = number
}
func getAge() -> Int {
return age
}
//오리지네이터는 스냅샷을 위한 메멘토 객체를 만들 수 있음(오리지네이터가 복원할 수도 메멘토가 복원할 수도 있음 이건 캡슐화 정도의 차이!)
func createMemento() -> Memento {
return Memento(originator: self)
}
}
//메멘토 - 나이라는 상태를 사진처럼 찍어(스냅샷) 가지고 있을 객체
class Memento {
private let originator: Originator // originator를 가지고 있어서 여기서 복원 메소드 호출
private let age: Int
init(originator: Originator) {
self.originator = originator
age = originator.getAge()
}
func restore() {
originator.setAge(to: age)
}
}
//케어테이커 - 오리지네이터로부터 메멘토를 요청하고(내부 상태 저장용)
//오리지네이터에게 메멘토를 다시 돌려줄 수 있음(상태 복구용)
class Caretaker {
private var savedStates = [Memento]()
func saveState(using memento: Memento) {
savedStates.append(memento)
}
func restoreState() {
if let lastState = savedStates.popLast() {
lastState.restore()
} else {
print("복구 실패!")
}
}
}
let originator = Originator(age: 21)
let caretaker = Caretaker()
//지금 나이를 저장해둬야지~
caretaker.saveState(using: originator.createMemento())
originator.setAge(to: 22)
originator.setAge(to: 23)
originator.setAge(to: 24)
//나이 먹기 싫다!! 타임머신!!
caretaker.restoreState()
8. Observer
관찰 중인 객체에서 발생하는 이벤트를 여러 다른 객체에 알리는 메커니즘을 정의할 수 있는 디자인 패턴
구조
Publisher
- 여러 개의 Observer를 가질 수 있습니다.
- Observer에게 상태의 변경을 알려주고 Observer를 추가하거나 제거하는 프로토콜을 제공합니다.
Concrete Publisher
- 구체 타입의 Observer 들을 속성으로 가지고 있습니다.
- Publisher의 상태를 저장하고 있습니다. 그래야 상태가 변경 될 때마다 Observer에게 알릴 수 있으니까요!
Observer(Subscriber)
- Publisher의 변경 사항을 update 받는 프로토콜을 제공합니다.
Concrete Observer
- Concrete Publisher의 상태가 변할 때 마다 이들에게 알려줍니다.
- Publisher의 속성이 어떻게 바뀌었는지 업데이트하는 메서드를 구현합니다.
protocol Observer: AnyObject {
var name: ObserverName { get }
func didChange(name: String, from oldValue: String, to newValue: String)
}
protocol Publisher: AnyObject {
var observers: [Observer] { get }
func addObserver(name: ObserverName)
func removeObserver(name: ObserverName)
}
enum ObserverName: CaseIterable {
case jay
case kitty
case james
case steven
case ellen
case soll
case sooback
}
final class MyObserver: Observer {
var name: ObserverName = .ellen
func didChange(name: String, from oldValue: String, to newValue: String) {
print("나는 \(self.name) 관찰자에요! \\(name)의 테스트 숫자가 \(oldValue ?? "")에서 \(newValue)(으)로 변경되었어요")
}
}
final class MyPublisher: Publisher {
var observers: [Observer] = []
private let publisherName = "Publisher"
var testNumber: Int = 0 {
didSet {
observers.compactMap({ observer in
observer.didChange(name: self.publisherName, from: "\\(oldValue)", to: "\\(self.testNumber)")
})
}
}
func addObserver(name: ObserverName) {
let observer = MyObserver()
observer.name = name
self.observers.append(observer)
}
func removeObserver(name: ObserverName) {
observers = self.observers.filter({ observer in
observer.name != name
})
}
}
let publisher = MyPublisher()
publisher.addObserver(name: .ellen)
publisher.testNumber = 10
publisher.testNumber = 20
/*
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 0에서 10(으)로 변경되었어요
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 10에서 20(으)로 변경되었어요
*/
publisher.addObserver(name: .james)
publisher.testNumber = 30
/*
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 0에서 10(으)로 변경되었어요
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 10에서 20(으)로 변경되었어요
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 20에서 30(으)로 변경되었어요
나는 james 관찰자에요! Publisher의 테스트 숫자가 20에서 30(으)로 변경되었어요
*/
publisher.removeObserver(name: .james)
publisher.testNumber = 20
/*
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 0에서 10(으)로 변경되었어요
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 10에서 20(으)로 변경되었어요
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 20에서 30(으)로 변경되었어요
나는 james 관찰자에요! Publisher의 테스트 숫자가 20에서 30(으)로 변경되었어요
나는 ellen 관찰자에요! Publisher의 테스트 숫자가 30에서 20(으)로 변경되었어요
*/
9. State
객체 내부의 상태에 따라서 객체가 다른 행동을 하도록 해주는 패턴
- strategy 패턴과 매우 흡사
구조
- context: state를 가지고 있고 상태별 작업을 state에게 우임
- state: 상태에 따른 기능들에 대한 프로토콜
- concrete state: state 프로토콜을 채택하여 실제 메소드를 구현
//Context
class Light {
var state: State // state를 가지고 관리
init(lightState: State) {
self.state = lightState
}
func changeStateToOn() {
print("불 켜는 버튼 누름")
state.onButtonPushed() // state에게 행동을 위임
state = TurnOnState()
}
func changeStateToOff() {
print("불 끄는 버튼 누름")
state.offButtonPushed()
state = TurnOffState()
}
}
//State
protocol State {
func onButtonPushed()
func offButtonPushed()
}
//Concrete State
class TurnOnState: State {
func onButtonPushed() {
print("이미 불 켜짐. 반응 없음")
}
func offButtonPushed() {
print("불을 끈다")
}
}
class TurnOffState: State {
func onButtonPushed() {
print("불을 켠다")
}
func offButtonPushed() {
print("이미 꺼져있음. 반응 없음")
}
}
//실행 결과
light.changeStateToOff()
light.changeStateToOn()
light.changeStateToOn()
light.changeStateToOff()
light.changeStateToOff()
/*
불 끄는 버튼 누름
이미 꺼져있음. 반응 없음
불 켜는 버튼 누름
불을 켠다
불 켜는 버튼 누름
이미 불 켜짐. 반응 없음
불 끄는 버튼 누름
불을 끈다
불 끄는 버튼 누름
이미 꺼져있음. 반응 없음
*/
10. Strategy
실행 중에 알고리즘을 선택하게 해서 여러 전략들을 선택해서 쓸 수 있는 패턴
- 런타임에 원하는 알고리즘을 선택!
ex) 지도 앱 길찾기
학교 → 집까지의 행위는 동일하지만 걸어서, 차타고, 지하철 등등의 다양한 알고리즘이 존재하고 우리는 실시간으로 알고리즘을 선택
구조
// strategy
protocol Strategy {
func 길찾기()
}
// concreteStrategy
class Car: Strategy {
func 길찾기() {
print("자동차 경로를 탐색합니다.")
}
}
class PublicTransport: Strategy {
func 길찾기() {
print("대중교통 경로를 탐색합니다.")
}
}
class Walk: Strategy {
func 길찾기() {
print("도보 경로를 탐색합니다.")
}
}
class Bicycle: Strategy {
func 길찾기() {
print("자전거 경로를 탐색합니다.")
}
}
class SharedScooter: Strategy {
func 길찾기() {
print("공유킥보드 경로를 탐색합니다.")
}
}
// context
class 지도앱 {
private var strategy: Strategy = PublicTransport()
init() {
print("지도앱 서비스를 시작합니다.\\n")
}
func 전략설정(to strategy: Strategy) {
self.strategy = strategy
}
func 길찾기() {
strategy.길찾기()
}
}
let map = 지도앱()
// 초기 전략: 대중교통
map.길찾기()
// 대중교통에서 자전거로 전략 변경
map.전략설정(to: Bicycle())
map.길찾기()
// 자전거에서 공유킥보드로 전략 변경
map.전략설정(to: SharedScooter())
map.길찾기()
/*
지도앱 서비스를 시작합니다.
대중교통 경로를 탐색합니다.
자전거 경로를 탐색합니다.
공유킥보드 경로를 탐색합니다.
*/
11. Visitor
알고리즘을 객체 구조에서 분리시키는 디자인 패턴!
- 실제 로직을 갖고 있는 visitor가 element에 접근하면서 element에 해당하는 로직을 수행하는 패턴
보험을 파는 로직을 가진 보험설계사가 회사, 집 등등을 돌아다니면서 판매
구조
- visitor: element에 방문했을 때 수행할 동작을 정의한 프로토콜
- element: visitor가 방문하면서 수행해야하는 대상
- visitor를 실행할 수 있는 accept 메소드를 가지고 있음
// visitor
protocol Tutor {
func teach(student: YagomSchoolStudent)
func teach(student: JostSchoolStudent)
}
extension Tutor {
func teach(student: YagomSchoolStudent) {
print("\(student.name), 공부하세요.")
}
func teach(student: JostSchoolStudent) {
print("\(student.name), 공부하세요")
}
}
class MathTutor: Tutor { }
class EnglishTutor: Tutor { }
// element
protocol Student {
// visitor를 매개변수로 받아서 visitor의 로직을 실행할 수 있는 함수
func receivePrivateLesson(tutor: Tutor)
}
class YagomSchoolStudent: Student {
let name: String
init(name: String) {
self.name = name
}
func receivePrivateLesson(tutor: Tutor) {
tutor.teach(student: self)
}
}
class JostSchoolStudent: Student {
let name: String
init(name: String) {
self.name = name
}
func receivePrivateLesson(tutor: Tutor) {
tutor.teach(student: self)
}
}
func run() {
let yagom = YagomSchoolStudent(name: "yagom")
let jost = JostSchoolStudent(name: "jost")
let mathTutor = MathTutor()
yagom.receivePrivateLesson(tutor: mathTutor)
jost.receivePrivateLesson(tutor: mathTutor)
let englishTutor = EnglishTutor()
yagom.receivePrivateLesson(tutor: englishTutor)
jost.receivePrivateLesson(tutor: englishTutor)
}