Customer와 Store 두 가지 객체가 있다고 상상해보자. Customer는 곧 출시를 앞둔 특정 상품에 매우 관심이 있다(예를 들자면 iPhone13..ㅎㅎ). 고객은 매일 스토어에 방문할 수 있다. 하지만 여전히 상품으로 들어와있지 않다면 고객의 많은 시간을 낭비하게 될 것이다.
만약, 스토어에서 새로운 상품이 출시 되어 구매가 가능해 질 때 모든 고객들에게 이메일을 보내주면 어떨까? 매일 왔다갔다 하지 않아도 되므로, 고객의 시간을 절약해 줄 수 있다. (동시에 새로운 상품에 관심이 없는 사람들에게는 귀찮은 이메일이 될 수 도 있지만)
스토어에서 메일을 보내주지 않을 경우, 고객의 시간이 낭비 될수 있다는 단점이 존재하고,
스토어에서 메일을 보내 줄 경우, 관심이 없는 고객에게도 메일을 보낼 수 있다는 단점이 존재한다.
이렇게 두 가지 상충하는 문제가 발생한다.
두 종류의 객체가 있다고 해보자.
Observer pattern은 구독 매커니즘을 사용한다.
1. Publisher는 구독자 정보를 가지고 있다.
private lazy var observers = [Observer]()
2. 구독자로 등록을 한다.
내가 만약 Publisher에(ex. 재품의 출시) 상태를 추적하고 싶은 경우 Subscriber로 등록 한다. 실제 앱의 경우 아마 수십개의 다른 subscriber class가 존재할 것이다.
func attach(_ observer: Observer) {
print("Subject: Attached an observer.\n")
observers.append(observer)
}
3. 구독자들에게 변화를 알린다.
Publisher의 상태의 변화가 있는 경우 등록된 Subscribers에게 변화를 알려주게 된다. Publiser는 반드시 알림(notification) method를 선언해야 한다. 알림 메소드는 일부 데이터를 파라미터로 전달할 수 있어야 한다.
func notify() {
print("Subject: Notifying observers...\n")
observers.forEach({ $0.update(subject: self)})
}
4. 언제든지 구독을 취소 할 수 있다.
func detach(_ observer: Observer) {
if let idx = observers.firstIndex(where: { $0 === observer }) {
observers.remove(at: idx)
print("Subject: Detached an observer.\n")
}
}
1. 모든 subscriber들은 동일한 인터페이스를 구현하고 있어야 한다.
이 인터페이스를 통해서만 다른 Publisher와 통신하게 된다.
예를 들어 모든 subscriber들이 동일한 update메소드를 가지고 있어서, publishers는 모든 구독자의 update메소드를 호출하기만 하면되는 것이다.(Publisher에 3번 참고)
protocol Observer: class {
func update(subject: Subject)
}
여러 종류의 publisher 타입을 가지고 있고, 여러 subscriber들과 함께 사용 되도록 하기 위해서 같은 인터페이스를 만들어야 하는 것이다.
Publisher와 Subscriber를 각각 살펴 보았다면 이제 전체 과정을 정리해보자.
publisher는 subsciptions 데이터를를 가지고 있어서 구독자를 리스트에 추가하거나 제거할 수 있다.
Publisher의 상태가 변하거나 행동(behavior)을 할 때, 이벤트가 발생한다. 새로운 이벤트가 발생 했을 때, publisher는 각각의 구독자 객체에 인터페이스에 선언된 notification method를 호출한다.
Subscriber 인터페이스는 notification 인터페이스를 선언한다. 대부분의 경우에 하나의 update 메소드로 구성되어있다. 이 메소드는 여러개의 파라미터(publisher가 보낸 event와 관련된 것들)를 가질 수 있다.
구체적인 subscriber는 publisher에 의한 notification에 대한 응답으로 몇가지 action을 수행한다. 모든 클래스는 반드시 같은 인터페이스를 구현하고 있어서 publisher가 구체적인 클래스를 복제 하지 않는다.
보통, subscriber는 update를 올바르게 처리하기 위한 정보들이 필요하다. 이러한 이유로, Publisher는 관련 데이터를 notification method의 인자로 보낸다.
client는 pulisher과 subscriber객체를 생성하고, 등록한다.
한 객체의 상태가 다른 객체의 변화를 요구하는 경우에 사용한다. 또한, 일련의 객체의 변화를 미리 감지 할 수 없을 때 사용한다.
예를 들자면, 사용자 정의 버튼 클래스를 생성하고, 사용자가 버튼을 누를 때 마다 실행되도록
클라이언트가 일부 사용자 정의 코드를 버튼에 연결하려고 할때 사용된다.
➕ Open/Close 원칙
➕ 런타임 시간에 객체 관의 관계를 설립할 수 있다.
➖ 구독자들은 랜덤한 순서로 notify를 받게 된다.
import XCTest
/// The Subject owns some important state and notifies observers when the state
/// changes.
class Subject {
/// For the sake of simplicity, the Subject's state, essential to all
/// subscribers, is stored in this variable.
var state: Int = { return Int(arc4random_uniform(10)) }()
/// @var array List of subscribers. In real life, the list of subscribers
/// can be stored more comprehensively (categorized by event type, etc.).
private lazy var observers = [Observer]()
/// The subscription management methods.
func attach(_ observer: Observer) {
print("Subject: Attached an observer.\n")
observers.append(observer)
}
func detach(_ observer: Observer) {
if let idx = observers.firstIndex(where: { $0 === observer }) {
observers.remove(at: idx)
print("Subject: Detached an observer.\n")
}
}
/// Trigger an update in each subscriber.
func notify() {
print("Subject: Notifying observers...\n")
observers.forEach({ $0.update(subject: self)})
}
/// Usually, the subscription logic is only a fraction of what a Subject can
/// really do. Subjects commonly hold some important business logic, that
/// triggers a notification method whenever something important is about to
/// happen (or after it).
func someBusinessLogic() {
print("\nSubject: I'm doing something important.\n")
state = Int(arc4random_uniform(10))
print("Subject: My state has just changed to: \(state)\n")
notify()
}
}
/// The Observer protocol declares the update method, used by subjects.
protocol Observer: class {
func update(subject: Subject)
}
/// Concrete Observers react to the updates issued by the Subject they had been
/// attached to.
class ConcreteObserverA: Observer {
func update(subject: Subject) {
if subject.state < 3 {
print("ConcreteObserverA: Reacted to the event.\n")
}
}
}
class ConcreteObserverB: Observer {
func update(subject: Subject) {
if subject.state >= 3 {
print("ConcreteObserverB: Reacted to the event.\n")
}
}
}
/// Let's see how it all works together.
class ObserverConceptual: XCTestCase {
func testObserverConceptual() {
let subject = Subject()
let observer1 = ConcreteObserverA()
let observer2 = ConcreteObserverB()
subject.attach(observer1)
subject.attach(observer2)
subject.someBusinessLogic()
subject.someBusinessLogic()
subject.detach(observer2)
subject.someBusinessLogic()
}
}