The problem:
We have a set of mobile mechanics who are assigned specific zip codes. Each zip code has its own hourly rate. We want to increase and decrease these rates when the number of idle mechanics within a zip code falls or goes above specific thresholds. This way we can proactively set the going rate for each mechanics when demand is high and bring it down when demand is low within a specific zip code.
The solution:
I implemented a solution using the Observer Design Pattern, however I want to make sure I got the terminology right, I think what I call subscriber in my solution is what is called an observer and what I call an observer is actually a handler that propagates the notifications.
More importantly I also want to make sure nothing I've done violates the pattern's definition.
Either way it's been awhile since I dealt with design patterns so any input will be greatly appreciated. Here is the code: Reading through a complicated implementation of a design pattern could seem a bit daunting here so if you're interested the repo as I mentioned earlier can be found here
import Foundation
class Mechanic{
var observer: Observer?
let name: String
var zipcode: Zipcode
var status: Status = .Idle{
didSet{
observer?.propertyChanged("Status", oldValue: oldValue.rawValue, newValue: status.rawValue, options: ["Zipcode": zipcode.value])
}
}
init(name: String, location: Zipcode){
self.name = name
self.zipcode = location
}
}
enum Status: Int{
case Idle = 1, OnTheWay, Busy
}
protocol Observer: class{
var subscribers: [Subscriber] {get set}
func propertyChanged(propertyName: String, oldValue: Int, newValue: Int, options: [String:String]?)
func subscribe(subscriber: Subscriber)
func unsubscribe(subscriber: Subscriber)
}
class MechanicObserver: Observer{
var subscribers: [Subscriber] = []
func propertyChanged(propertyName: String, oldValue: Int, newValue: Int, options:[String:String]?){
print("Change in property detected, notifying subscribers")
let matchingSubscribers = subscribers.filter({0ドル.properties.contains(propertyName)})
matchingSubscribers.forEach({0ドル.notify(propertyName, oldValue: oldValue, newValue: newValue, options: options)})
}
func subscribe(subscriber: Subscriber){
subscribers.append(subscriber)
}
func unsubscribe(subscriber: Subscriber) {
subscribers = subscribers.filter({0ドル !== subscriber})
}
}
protocol Subscriber: class{
var properties : [String] {get set}
func notify(propertyName: String,oldValue: Int, newValue: Int, options: [String:String]?)
}
class ZipcodePriceManager: Subscriber{
var properties : [String] = ["Status"]
var zipcodes: Set<Zipcode>
var supply: [Zipcode: Int] = [:]
init(zipcodes: Set<Zipcode>, supply: [Zipcode: Int]){
self.zipcodes = zipcodes
self.supply = supply
}
func notify(propertyName: String, oldValue: Int, newValue: Int, options: [String:String]?){
if properties.contains(propertyName){
print("\(propertyName) is changed from \(Status(rawValue: oldValue)!) to \(Status(rawValue: newValue)!)")
if propertyName == "Status"{
if let options = options{
let zipcode = zipcodes.filter({0ドル.value == options["Zipcode"]}).first
if let zipcode = zipcode{
if (Status(rawValue: newValue) == Status.Idle && Status(rawValue: oldValue) != Status.Idle){
supply[zipcode]! += 1
}else if (Status(rawValue: newValue) != Status.Idle && Status(rawValue: oldValue) == Status.Idle){
supply[zipcode]! -= 1
}
updateRates()
print("**********************")
}
}
}
}
}
func updateRates(){
supply.forEach({(zipcode: Zipcode, supply: Int) in
if (supply <= 1){
zipcode.adjustment = 0.50
print("Very High Demand! Adjusting price for \(zipcode.value): rate is now \(zipcode.rate) because supply is \(supply)")
}else if (supply <= 3){
zipcode.adjustment = 0.25
print("High Demand! Adjusting price for \(zipcode.value): rate is now \(zipcode.rate) because supply is \(supply)")
}else{
zipcode.adjustment = 0.0
print("Normal Demand. Adjusting price for \(zipcode.value): rate is now \(zipcode.rate) because supply is \(supply)")
}
})
}
}
class Zipcode: Hashable, Equatable{
let value: String
var baseRate: Double
var adjustment: Double
var rate: Double{
return baseRate + (baseRate * adjustment)
}
init (value: String, baseRate: Double){
self.value = value
self.baseRate = baseRate
self.adjustment = 0.0
}
var hashValue: Int{
return value.hashValue
}
}
func == (lhs: Zipcode, rhs: Zipcode) -> Bool {
return lhs.value == rhs.value
}
Main defintion with test cases:
var mountainView = Zipcode(value: "94043", baseRate: 40.00)
var redwoodCity = Zipcode(value: "94063", baseRate: 30.00)
var paloAlto = Zipcode(value: "94301", baseRate: 50.00)
var sunnyvale = Zipcode(value: "94086", baseRate: 35.00)
var zipcodes : Set<Zipcode> = [mountainView, redwoodCity, paloAlto, sunnyvale]
var steve = Mechanic(name: "Steve Akio", location: mountainView)
var joe = Mechanic(name: "Joe Jackson", location: redwoodCity)
var jack = Mechanic(name: "Jack Joesph", location: redwoodCity)
var john = Mechanic(name: "John Foo", location: paloAlto)
var trevor = Mechanic(name: "Trevor Simpson", location: sunnyvale)
var brian = Mechanic(name: "Brian Michaels", location: sunnyvale)
var tom = Mechanic(name: "Tom Lee", location: sunnyvale)
var mike = Mechanic(name: "Mike Cambell", location: mountainView)
var jane = Mechanic(name: "Jane Sander", location: mountainView)
var ali = Mechanic(name: "Ali Ham", location: paloAlto)
var sam = Mechanic(name: "Sam Fox", location: mountainView)
var reza = Mechanic(name: "Reza Shirazian", location: mountainView)
var max = Mechanic(name: "Max Watson", location: sunnyvale)
var raj = Mechanic(name: "Raj Sundeep", location: sunnyvale)
var bob = Mechanic(name: "Bob Anderson", location: mountainView)
var mechanics = [steve, joe, jack, john, trevor, brian, tom, mike, jane, ali, sam, reza, max, raj, bob]
var supply: [Zipcode: Int] = [:]
zipcodes.forEach({(zipcode: Zipcode) in supply[zipcode] = mechanics.filter({(mechanic:Mechanic) in mechanic.status == Status.Idle && mechanic.zipcode === zipcode}).count})
var priceManager = ZipcodePriceManager(zipcodes: zipcodes, supply: supply)
let observer = MechanicObserver()
observer.subscribe(priceManager)
mechanics.forEach({0ドル.observer = observer})
john.status = .OnTheWay
steve.status = .OnTheWay
steve.status = .Busy
steve.status = .Idle
trevor.status = .OnTheWay
brian.status = .OnTheWay
tom.status = .OnTheWay
reza.status = .OnTheWay
tom.status = .Busy
raj.status = .OnTheWay
observer.unsubscribe(priceManager)
print("unsubscribed")
raj.status = .Idle
Current output:
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 6
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
Normal Demand. Adjusting price for 94086: rate is now 35.0 because supply is 5
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 5
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
Normal Demand. Adjusting price for 94086: rate is now 35.0 because supply is 5
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from OnTheWay to Busy
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 5
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
Normal Demand. Adjusting price for 94086: rate is now 35.0 because supply is 5
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Busy to Idle
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 6
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
Normal Demand. Adjusting price for 94086: rate is now 35.0 because supply is 5
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 6
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
Normal Demand. Adjusting price for 94086: rate is now 35.0 because supply is 4
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 6
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
High Demand! Adjusting price for 94086: rate is now 43.75 because supply is 3
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 6
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
High Demand! Adjusting price for 94086: rate is now 43.75 because supply is 2
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 5
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
High Demand! Adjusting price for 94086: rate is now 43.75 because supply is 2
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from OnTheWay to Busy
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 5
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
High Demand! Adjusting price for 94086: rate is now 43.75 because supply is 2
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
Change in property detected, notifying subscribers
Status is changed from Idle to OnTheWay
Normal Demand. Adjusting price for 94043: rate is now 40.0 because supply is 5
High Demand! Adjusting price for 94063: rate is now 37.5 because supply is 2
Very High Demand! Adjusting price for 94086: rate is now 52.5 because supply is 1
Very High Demand! Adjusting price for 94301: rate is now 75.0 because supply is 1
**********************
unsubscribed
Change in property detected, notifying subscribers
Program ended with exit code: 0
1 Answer 1
protocol Observer: class{ var subscribers: [Subscriber] {get set} func propertyChanged(propertyName: String, oldValue: Int, newValue: Int, options: [String:String]?) func subscribe(subscriber: Subscriber) func unsubscribe(subscriber: Subscriber) }
Nevermind your terminology, there's a major problem here. Who needs to know all those things? The Mechanic
class which is notifying the observer only needs to know that the propertyChanged
method exists. And the rest of what the observer does with that property changed information is up to the observer.
Maybe it has subscribers that can subscribe and unsubscribe via these methods. Maybe it has a delegate. Maybe it runs some closures. Maybe it just runs its own code. Maybe it posts an NSNotification
. It really doesn't matter. So with that in mind, the subscribers
array and the subscribe
and unsubscribe
methods should be removed from the protocol.
Now, with that in mind, I think that MechanicObserver
class is a really unnecessary man in the middle here. This seems over complicated. The Mechanic tells the MechanicObserver who tells the MechanicObserver subscribers. And problematically, whether or not those subscribers get notified is based on whether or not they have a particular string in a special array property they are forced to have? There are several issues here.
First of all, again, I don't see the point in having the MechanicObserver rather than just having subscribers subscribe directly to the Mechanic.
Second, you only allow subscribing to changes in Int
properties, and only allow string data in the options. You've really boxed yourself in very tight with the protocol, just one reason this is going to have some scalability issues.
Third, this looping string comparison can quickly become very expensive if we have several things subscribed to this change and they each have several values in their subscribers
array.
Finally, this quickly becomes very problematic when ZipCodePriceManager
wants to subscribe to the status of mechanics... and then you apply the same pattern to your TowTruck
model class and your Garage
model class, and they also have a status
property, and probably a zipCode
too... and who knows what else. There is no way to indicate what model type or even what instance of that model actually had the changing property.
-
\$\begingroup\$ I agree that the subscribe/unsubscribe should be taken out of the observer protocol, there is no need to enforce a specific way for the observer to deal with the property change. I also understand your point that having a MechanicObserver is overkill and somewhat FizzBuzzEnterprise-like but this is part of a series of article I'm writing on design patterns and I made up the problem just so I can solve it using the observer pattern. Also thank you for taking the time and reviewing my code, I'm going to heed your advice and rethink some of my approaches. \$\endgroup\$Reza Shirazian– Reza Shirazian2016年04月13日 22:13:41 +00:00Commented Apr 13, 2016 at 22:13
Explore related questions
See similar questions with these tags.