3
\$\begingroup\$

I'm solving the following problem using the Mediator design pattern in Swift:

Assume we track the location of all our mobile mechanics. We have noticed that there are times when a mobile mechanic might need assistance from another mechanic or a last minute need for a part that someone else might carry. We want to build a system where mechanics can send requests with the option of defining specific parts needed to all mechanics that are close to their location

I would love some feedback on how I can improve this, more importantly if it remains true to the definition of the Mediator pattern.

Here is the code, the full repo can be found here: Design Patterns in Swift: Mediator

import Foundation
class Mechanic{
 let mediator: Mediator
 var location: (Int, Int)
 var name: String
 init (mediator: Mediator, name: String, location: (Int, Int)){
 self.mediator = mediator
 self.name = name
 self.location = location
 }
 func send(request: Request){
 mediator.send(request)
 }
 func receive(request: Request){
 print("\(name) received request from \(request.mechanic.name): \(request.message)")
 if let parts = request.parts{
 print("request is for parts:")
 for part in parts{
 print(part.name)
 }
 }
 print("******************")
 }
 func isCloseTo(mechanic: Mechanic, within distance: Float) -> Bool
 {
 return hypotf(Float(mechanic.location.0 - location.0), Float(mechanic.location.1 - location.1)) <= distance
 }
}
class Part{
 var name: String
 var price: Double
 init (name: String, price: Double){
 self.name = name
 self.price = price
 }
}
class Request {
 var message: String
 var parts: [Part]?
 var mechanic: Mechanic
 init(message: String, mechanic: Mechanic, parts: [Part]?)
 {
 self.message = message
 self.parts = parts
 self.mechanic = mechanic
 }
 convenience init(message: String, mechanic: Mechanic){
 self.init(message: message, mechanic: mechanic, parts: nil)
 }
}
protocol Mediator{
 func send(request: Request)
}
class RequestMediator: Mediator{
 private let closeDistance: Float = 50.0
 private var mechanics: [Mechanic] = []
 func addMechanic(mechanic: Mechanic){
 mechanics.append(mechanic)
 }
 func send(request: Request) {
 for oneOfTheMechanics in mechanics{
 if oneOfTheMechanics !== request.mechanic && request.mechanic.isCloseTo(oneOfTheMechanics, within: closeDistance){
 oneOfTheMechanics.receive(request)
 }
 }
 }
}

Here is the main setup and some test cases

import Foundation
var requestManager = RequestMediator()
var steve = Mechanic(mediator: requestManager, name: "Steve Akio", location: (23,12))
var joe = Mechanic(mediator: requestManager, name: "Joe Bob", location: (13,12))
var dave = Mechanic(mediator: requestManager, name: "Dave Far", location: (823,632))
var mike = Mechanic(mediator: requestManager, name: "Mike Nearfar", location: (800,604))
requestManager.addMechanic(steve)
requestManager.addMechanic(joe)
requestManager.addMechanic(dave)
requestManager.addMechanic(mike)
steve.send(Request(message: "I can't find this address anyone close by knows where Rengstorff Ave is?", mechanic: steve))
joe.send(Request(message: "I need some brake pads anyone close by has some?", mechanic: joe, parts: [Part(name: "StopIt Brake Pads", price: 35.25)]))
dave.send(Request(message: "Dang it I spilled all my oil, anyone around here got a spare 5 Quart Jug.. and some filters too", mechanic: dave, parts:[Part(name: "Engine Oil SuperPlus", price: 23.33), Part(name: "Filters", price: 4.99)]))

Here is the output you get with this setup:

Joe Bob received request from Steve Akio: I can't find this address anyone close by knows where Rengstorff Ave is?
******************
Steve Akio received request from Joe Bob: I need some brake pads anyone close by has some?
request is for parts:
StopIt Brake Pads
******************
Mike Nearfar received request from Dave Far: Dang it I spilled all my oil, anyone around here got a spare 5 Quart Jug.. and some filters too
request is for parts:
Engine Oil SuperPlus
Filters
******************
Program ended with exit code: 0
asked Apr 15, 2016 at 0:41
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

It's not an optimal example of the Mediator pattern. The important part of the pattern is that the colleagues don't have to know anything about each other, yet you are passing colleagues to each other through the request object.

I think a better example would be to have RouteManagers generating requests, which then inform the mediator that they have a request available. The mediator then gets the request from the manager and forwards it to the available Mechanics. A mechanic would inform the mediator when it's availability status changes... Something like that at least.

The key idea behind the mediator is that it observes state changes from its colleagues and modifies other colleagues in response. A fantastic example of a mediator is the UIViewController in iOS. Models tell the viewController when their state changes, then the viewController examines the new state and uses the information to update the appearance of views.

As for your code. About the only substantive changes I would make are:

  • Turn the Part class into a struct. Check out "Prefer structs over classes"
  • Make Request.parts non-optional (an empty array can mean no parts.)
  • If you are targeting iOS/OS X, then use CLLocationCoordinate2D instead of the (Int, Int) tuple. Even if you aren't targeting one of these platforms, you should put more semantic meaning on the tuple, at least call them latitude and longitude or something.
answered Apr 15, 2016 at 23:52
\$\endgroup\$
7
  • \$\begingroup\$ Why would you turn the Part class into a struct? I'm not agreeing or disagreeing, but I think this should be explained further. \$\endgroup\$ Commented Apr 16, 2016 at 1:54
  • \$\begingroup\$ As for CLLocationCoordinate2D, that means we have to import CoreLocation into a chunk of code which probably doesn't even require import Foundation. \$\endgroup\$ Commented Apr 16, 2016 at 1:55
  • \$\begingroup\$ From github's Swift style-guide: "Prefer structs over classes: Value types [struct] are simpler, easier to reason about, and behave as expected with the let keyword." A wrench is a wrench, they don't have identity. One 3ドル can of oil is the same as any other 3ドル can of oil. You have no need for a class in this case, so don't use one. \$\endgroup\$ Commented Apr 16, 2016 at 2:06
  • \$\begingroup\$ I wasn't looking for a conversation. I was recommending an edit before I upvote. \$\endgroup\$ Commented Apr 16, 2016 at 2:08
  • 1
    \$\begingroup\$ Depends. If you're targeting OSX/iOS only, it's probably fine. But we can't make that assumption about Swift any more. \$\endgroup\$ Commented Apr 16, 2016 at 2:24
2
\$\begingroup\$

How are you handling location here?

What does location: (Int, Int) represent?

The fact that I have to ask these question is already a big enough problem.

So at a minimun, we deserve a type just for location. This could be perfectly as simple as:

typealias CoordinateDegree = Double
typealias CoordinateLocation = (latitude: CoordinateDegree, longitude: CoordinateDegree)

This type would be perfectly fine if this were just a data point we weren't doing anything with. But it's not. It's more than that.

So we probably want a struct.

typealias CoordinateDegree = Double
struct CoordinateLocation {
 let latitude: CoordinateDegree
 let longitude: CoordinateDegree
 init(latitude: CoordinateDegree, longitude: CoordinateDegree) {
 self.latitude = latitude
 self.longitude = longitude
 }
}

And why do we need more than a simple typealiased tuple? Because we're performing some logic over these values. Namely, we're calculating distance. So why not make a method for doing that?

// It should be documented whether this represents miles, kilometers, or something else
typealias CoordinateDistance = Double
extension CoordinateLocation {
 func distanceFrom(location: CoordinateLocation) -> CoordinateDistance {
 // Calculate and return the distance
 }
}

Now... I haven't simply copy & pasted your distance calculation into here, because if your location actually represents latitude & longitude, your calculation is wrong. You're just finding the straight line distance between two points on a flat plane. The problem is, the earth isn't flat, despite what these sorts of people might say.
So we need a smarter calculation.

Let's start with some functions for converting between radians & degrees.

typealias CoordinateRadian = Double
extension CoordinateDegree {
 var radianValue: CoordinateRadian {
 return self * M_PI / 180.0
 }
}
extension CoordinateRadian {
 var degreeValue: CoordinateDegree {
 return self * 180.0 / M_PI
 }
}

We don't really need the degreeValue for our calculations, but I included it for completeness sake.

We need radians in order to use the Havershine function, which looks something like this in Swift:

import Darwin
typealias CoordinateDistance = Double
extension CoordinateLocation {
 private static let radiusEarth: CoordinateDistance = 3961.0 // Miles
 func distanceFrom(location: CoordinateLocation) -> CoordinateDistance {
 // Calculate and return the distance
 let deltaLongitude = location.longitude - self.longitude
 let deltaLatitude = location.latitude - self.latitude
 let a = pow((sin(deltaLatitude.radianValue/2)), 2) + cos(self.latitude.radianValue) * cos(location.latitude.radianValue) * pow((sin(deltaLongitude.radianValue/2)), 2)
 let c = 2 * atan2(sqrt(a), sqrt(1-a))
 return c * CoordinateLocation.radiusEarth
 }
}

Of course, you notice here that I defined the Earth's radius as 3,961 miles. It's a CoordinateDistance type, so depending on whether you want to document that type as miles or kilometers will effect what you define this value to be.

It's also worth noting that the Earth isn't perfectly smooth. It's bumpy. The radius varies by about 20 miles from the smallest to largest values. Also, there are more accurate formula out there.

And perhaps, best of all, there are good frameworks out there like CoreLocation, which will take care of all of the types and logic I showed you in this answer.

answered Apr 16, 2016 at 20:34
\$\endgroup\$
1
  • \$\begingroup\$ The idea is to represent a solution that shows the Mediator design pattern in a somewhat easy to understand problem. While I agree with all you have said regarding calculating distances between two position on earth (in fact I'm probably going to use what you've provided in a separate work related problem, (if I don't use CoreLocation)), I think including these calculation would divert focus from the pattern. \$\endgroup\$ Commented Apr 16, 2016 at 22:18

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.