3
\$\begingroup\$

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
\$\endgroup\$
2
\$\begingroup\$
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.

\$\endgroup\$
  • \$\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 Apr 13 '16 at 22:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.