[Swift] Multicast Delegate Pattern
Multicast Delegate Pattern
✅ Multicast Delegate Pattern
아래의 문서를 구입하여 영어 문서를 번역하고 이해한 것을 바탕으로 글을 작성하고 있습니다.
multicast delegate 패턴은 delegate 패턴의 변형인 행동 패턴이다. 이를 통해 단순 delegate의 1:1 관계 대신 1:N delegate 관계를 만들 수 있습니다.
1. object needing a delegate(a.k.a delegating object)는 하나 혹은 그 이상의 delegate를 가지는 객체입니다.
2. delegate protocol은 구현하거나 구현해야하는 메소드를 정의합니다. (protocol 정의에서 optional로 만드는 기법이 있습니다.)
3. delegate(s)는 delegate protocol을 구현하는 객체입니다.
4. multicast delegate는 delegate를 보유하는 helper class입니다. delegate-worthy 이벤트가 발생할 때마다 각자에게 통지합니다.
멀티캐스트 딜리게이트 패턴과 딜리게이트 패턴의 주요 차이점은 멀티캐스트 딜리게이트를 위해서 helper class가 있다는 점입니다. Swift는 기본적으로 이 클래스를 제공하지 않으나, 우리가 이번 시간에 만들어 볼 예정입니다.
Note: Swift 5.1에서 Combine 프레임워크 안에서 Multicast 유형을 도입했습니다. Combine 프레임워크 내에 있는 것은 여기서 이야기하는 MulticastDelegate와는 다릅니다.
Combine에 대해서 더 자세히 알고 싶다면 아래 링크를 확인해주세요!
(RxSwift 만큼 Combine이 정리가 잘 되어 있어서, 나중에 사서 공부해야지 !_!)
https://www.raywenderlich.com/books/combine-asynchronous-programming-with-swift
When should you use it?
1:N delegate 관계를 만들 때 사용합니다.
예를 들어서 이 패턴을 사용하여, 다른 객체에 변경 사항이 발생하는 경우 여러 객체에 알릴 수 있습니다. 또한 각 delegate는 스스로의 상태를 업데이트 하거나, 응답으로 관련 작업을 수행할 수도 있습니다.
Playground example
우선 MuticastDelegate helper class를 만들어 봅시다!
// 1
public class MulticastDelegate<ProtocolType> {
// MARK: - DelegateWrapper
// 2
private class DelegateWrapper {
weak var delegate: AnyObject?
init(_ delegate: AnyObject) {
self.delegate = delegate
}
}
}
1. MulticaseDelegate은 모든 ProtocolType을 제네릭 형식으로 허용하는 제네릭 클래스로 정의합니다. Swift는 아직 <ProtocolType>을 프로토콜만으로 제한하는 방법을 제공하지 않습니다. 따라서 ProtocolType에 대한 프로토콜 대신 구체적인 클래스 유형을 전달할 수 있습니다. 하지만 프로토콜을 사용할 가능성이 높으므로 일반 유형의 이름을 Type이 아닌 ProtocolType으로 지정합니다!
2. DelegateWrapper를 내부 클래스로 정의합니다. 이 옵션을 사용하여, delegate 객체를 weak property로 묶을 수 있습니다. 이런 방식으로 multicast delegate는 strong warpper 인스턴스를 직접 delegates하지 않고도 보유할 수 있습니다.
안타깝게도 여기서는 delegate property을 ProtocolType대신 AnyObject로 선언해야 합니다. 왜냐하면 weak 변수는 AnyObject(즉, 클래스)이어야 하기 때문입니다. 우리는 ProtocolType이 AnyObject로써 사용할 수 있는 제네릭 타입이라고 생각할 수 있습니다. 하지만 프르토콜 자체를 객체가 아닌 Type으로 전달해야 하기 때문에 작동하지 않습니다.
// MARK: - Instance Properties
// 1
private var delegateWrappers: [DelegateWrapper]
// 2
public var delegates: [ProtocolType] {
delegateWrappers = delegateWrappers
.filter { $0.delegate != nil }
return delegateWrappers.map
{ $0.delegate! } as! [ProtocolType]
}
// MARK: - Object Lifecycle
// 3
public init(delegates: [ProtocolType] = []) {
delegateWrappers = delegates.map {
DelegateWrapper($0 as AnyObject)
}
}
1. DelegateWrapper가 DelegateWrapper 인스턴스를 보유하도록 선언합니다. DelegateWrapper 인스턴스는 MulticastDelegate에 의해서 전달됩니다.
2. delegate를 위한 연산 프로퍼티를 추가합니다. 이렇게하면 필터링 한 다음에 nil이 아닌 delegate의 배열을 반환해줍니다!
3. 마지막으로 이니셜라이저를 만듭니다!
이미 생선된 멀티캐스트 delegate를 추가 및 삭제하는 수단도 필요합니다.
// MARK: - Delegate Management
// 1
public func addDelegate(_ delegate: ProtocolType) {
let wrapper = DelegateWrapper(delegate as AnyObject)
delegateWrappers.append(wrapper)
}
// 2
public func removeDelegate(_ delegate: ProtocolType) {
guard let index = delegateWrappers.firstIndex(where: {
$0.delegate === (delegate as AnyObject)
}) else {
return
}
delegateWrappers.remove(at: index)
}
1. 이름에서 알 수 있듯이 delegate를 add하는 메소드입니다. delegate를 AnyObject로 래핑합니다.
2. 마찬가지로 remove를 사용하여 제거합니다!
다음으로는 실제로 delegate를 호출하는 수단이 필요합니다.
public func invokeDelegates(_ closure: (ProtocolType) -> ()) {
delegates.forEach { closure($0) }
}
연산 프로퍼티를 delegate를 자동으로 필터링하고, 클로저를 전달된 속성을 호출합니다.
여기까지가 MulticastDelegate helper class가 생겼고, 다음 작업을 보죠!
// MARK: - Delegate Protocol
public protocol EmergencyResponding {
func notifyFire(at location: String)
func notifyCarCrash(at location: String)
}
EmergencyResponding 프로포톨을 생성합니다.
// MARK: - Delegates
public class FireStation: EmergencyResponding {
public func notifyFire(at location: String) {
print("Firefighters were notified about a fire at "
+ location)
}
public func notifyCarCrash(at location: String) {
print("Firefighters were notified about a car crash at "
+ location)
}
}
public class PoliceStation: EmergencyResponding {
public func notifyFire(at location: String) {
print("Police were notified about a fire at "
+ location)
}
public func notifyCarCrash(at location: String) {
print("Police were notified about a car crash at "
+ location)
}
}
두개의 delegate 객체를 정의합니다. 긴급상황이 발생하면 경찰관과 소방관이 모두 대응합니다.
// MARK: - Delegating Object
public class DispatchSystem {
let multicastDelegate =
MulticastDelegate<EmergencyResponding>()
}
MulticastDelegate을 타입으로하는 변수를 선언합니다.
// MARK: - Example
let dispatch = DispatchSystem()
var policeStation: PoliceStation! = PoliceStation()
var fireStation: FireStation! = FireStation()
dispatch.multicastDelegate.addDelegate(policeStation)
dispatch.multicastDelegate.addDelegate(fireStation)
dispatch를 DispatchSystem 인스턴스로 생성합니다. 그런 다음 policeStation 및 fireStation에 대한 위임 인스턴스를 생성하고 dispatch.multicastDelegate.addDelegate(_:)를 호출하여 둘 다 등록합니다.
dispatch.multicastDelegate.invokeDelegates {
$0.notifyFire(at: "Ray’s house!")
}
// console
// Police were notified about a fire at Ray's house!
// Firefighters were notified about a fire at Ray's house!
delegate가 0이 되는 경우, multicast delegate에 대한 어떠한 향후 호출에 대해서도 통지되면 안됩니다.
print("")
fireStation = nil
dispatch.multicastDelegate.invokeDelegates {
$0.notifyCarCrash(at: "Ray's garage!")
}
// 콘솔
// Police were notified about a car crash at Ray's garage!
nil로 처리하여 콘솔에 나타나지 않는 것을 확인할 수 있습니다.
What should you be careful about?
이 패턴은 "information only" delegate 호출에 가장 적합합니다.
delegate가 데이터를 제공해야 하는 경우 이 패턴이 제대로 작동하지 않습니다. 왜냐하면 이 패턴의 경우 여러 delegate들이 데이터를 제공해야 하기 때문에 정보가 중복되거나 처리가 낭비될 수도 있습니다.
이 경우에는 다음 디자인 패턴에서 다루는 chain-of-responsibility pattern 사용하는 것을 고려하세오!
Tutorial project
멀티 캐스트 패턴입니다! 어렵지 않아요!
https://github.com/lgvv/DesignPattern/tree/main/multicast-delegate-pattern
Key points
이번 시간에는 multicast delegate pattern에 대해서 알아보았습니다.
- 멀티캐스트 딜리게이트 패턴을 사용하면 1:N delegate 관계를 만들 수 있습니다. 여기에는 4가지 유형이 있습니다.
(위의 이미지 참고)
- object needing a delegate에는 하나 이상의 delegates가 있습니다. delegate는 구현해야 합니다. delegates는 프로토콜이고, multicast delegate는 delegate를 보유하고 있는 helper class입니다.
- Swift에서는 직접적으로 multicast delegate 객체를 제공하지 않습니다. 그러나 이 패턴을 위해 직접 구현하는 것은 쉽습니다.