Reputation: 808
There are scenarios when two interacting sets of generic classes is a good design pattern. A simple example is the Observable-Observer pattern. The observable posts events to the observer but the pattern is the same no matter what type of event that is being observed.
My first thought was that the preferred way would be to define two generic protocols. This should offer the smallest coupling, which tends to be good as the code base grows.
protocol ProtocolObserver {
typealias EventType
func update<O:ProtocolObservable where O.EventType == EventType>(observable:O, event:EventType) -> Void
}
protocol ProtocolObservable {
typealias EventType
func registerObserver<O:ProtocolObserver where O.EventType == EventType>(observer:O) -> Bool
func unregisterObserver<O:ProtocolObserver where O.EventType == EventType>(observer:O) -> Void
}
Trying to define classes that implement the above protocols turned out to be a world of hurt. I failed to find any way to do it.
Implementing a generic baseclass would, however, be an acceptable solution.
protocol GenericObserver {
func update<EventType>(observable:GenericObservable<EventType>, event:EventType);
}
class GenericObservable<EventType> {
private var observers:[GenericObserver] = []
func registerObserver(observer:GenericObserver) -> Bool {
// Code to avoid registering the same observer twice
observers.append(observer)
return true
}
func unregisterObserver(observer:GenericObserver) -> Void {
// Code to remove the observer if present in observers
}
func notifyObservers(event:EventType) -> Void {
for observer in observers {
observer.update(self, event: event)
}
}
}
No problem defining some classes that implement the protocol this time. Adding them to an instance of the generic observer did not show the behaviour I expected.
let numberObservable = GenericObservable<NSNumber>()
class NumberObserver : GenericObserver {
func update<NSNumber>(observable:GenericObservable<NSNumber>, event:NSNumber) {
print("Number Event \(event)")
}
}
let numberObserver = NumberObserver()
numberObservable.registerObserver(numberObserver)
class DataObserver : GenericObserver {
func update<NSData>(observable:GenericObservable<NSData>, event:NSData) {
print("Data Event \(event)")
}
}
let dataObserver = DataObserver()
numberObservable.registerObserver(dataObserver)
numberObservable.notifyObservers(NSNumber(int: 42))
I expected numberObservable.registerObserver(dataObserver)
to cause a compilation error. Instead it happily printed the output
Number Event 42
Data Event 42
This all leaves me with two questions:
What have I misunderstood when I expect the compiler to not accept numberObservable.registerObserver(dataObserver)
?
Is there a way to implement a pair of classes that conform to ProtocolObserver
and ProtocolObservable
respectively?
Upvotes: 2
Views: 1588
Reputation: 40965
Your questions, 1 and 2, are in fact strongly related.
Before getting started though, I should point out that when you have first-class functions, the observable/observer pattern is almost entirely redundant. Instead of mandating interfaces for callbacks, you can just supply a closure. I’ll show this in the answer to question 2.
First, 1. The problem you are experiencing is type erasure. Your base class is the only place in which you have defined registerObserver
and it looks like this:
class GenericObservable<EventType> {
private var observers:[GenericObserver] = []
func registerObserver(observer:GenericObserver) -> Bool {
// Code to avoid registering the same observer twice
observers.append(observer)
return true
}
//...
}
That is, it will take and store a protocol reference to any type. There is no constraint on what that type is, it can be anything. For example, you could notify an Int
:
extension Int: GenericObserver {
func update<EventType>(observable:GenericObservable<EventType>, event:EventType) {
print("Integer \(self)")
}
}
numberObservable.registerObserver(2)
The problem will come when the callees try to use EventType
. EventType
could be anything. It is similar to this function:
func f<T>(t: T) { }
T
can be any type you like – a String
, an Int
, a Foo
. But you will not be able to do anything with it, because it provides zero guarantees. To make a generic type useful, you have to either constrain it (i.e. guarantee it has certain features, like that it’s an IntegerType
that can be added/subtracted for example), or pass it on to another generic function that similarly unconstrained (such as put it in a generic collection, or call print
or unsafeBitCast
which will operate on any type).
Basically, your observers have all declared “I have a family of methods, update
, which you can call with any type you like”. This is not very useful, unless you’re writing something like map
or a generic collection like an array, in which case you don’t care what T
is.
This might help clear up some confusion – this does not do what you think it does:
class DataObserver : GenericObserver {
func update<NSData>(observable:GenericObservable<NSData>, event:NSData) {
print("Data Event \(event)")
}
}
Here you have not declared that DataObserver
specifically takes an NSData
class. You’ve just named the generic placeholder NSData
. Similar to naming a variable NSData
– it doesn’t mean that is what the variable is, just that’s what you’ve called it. You could have written this:
class DataObserver : GenericObserver {
func update<Bork>(observable:GenericObservable<Bork>, event: Bork) {
print("Data Event \(event)")
}
}
Ok so how to implement an observable protocol with an associated type (i.e. a typealias in the protocol). Here’s an example of one. But note, there is no Observer
protocol. Instead, Observable
will take any function that receives the appropriate event type.
protocol Observable {
typealias EventType
func register(f: EventType->())
}
// No need for an "Observer" protocol
Now, let’s implement this, fixing EventType
to be an Int
:
struct FiresIntEvents {
var observers: [Int->()] = []
// note, this sets the EventType typealias
// implicitly via the types of the argument
mutating func register(f: Int->()) {
observers.append(f)
}
func notifyObservers(i: Int) {
for f in observers {
f(i)
}
}
}
var observable = FiresIntEvents()
Now, if we want to observe via a class, we can:
class IntReceiverClass {
func receiveInt(i: Int) {
print("Class received \(i)")
}
}
let intReceiver = IntReceiverClass()
// hook up the observing class to observe
observable.register(intReceiver.receiveInt)
But we can also register arbitrary functions:
observable.register { print("Unowned closure received \($0)") }
Or register two different functions on the same receiver:
extension IntReceiverClass {
func recieveIntAgain(i: Int) {
print("Class recevied \(i) slightly differently")
}
}
observable.register(intReceiver.recieveIntAgain)
Now, when you fire the events:
observable.notifyObservers(42)
you get the following output:
Class received 42
Unowned closure received 42
Class recevied 42 slightly differently
But with this technique, if you try to register a function of the wrong event type, you get a compilation error:
observable.register(IntReceiverClass.receiveString)
// error: cannot invoke 'register' with an argument list of type '(IntReceiverClass -> (String) -> ())
Upvotes: 4