Design Patterns in Swift: Chain of Responsibility

Posted on

Problem

I’m solving the following problem using the Chain Of Responsibility design pattern in Swift:

Not all mechanics are created equally. Some mechanics are more
experienced and can do more than others. We need a system where every
job is propagated from the least experienced mechanic to the most.
This way experienced mechanics that can perform more jobs are not
busy with jobs that more junior mechanics can take care of.

I would love some feedback on how I can improve this. If it remains true to the definition of Chain Of Responsibility or if there are any swift standard coding convention that I should be following.

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

import Foundation

enum Skill: Int{
  case OilChangeOnly = 1, Junior, Apprentice, MasterMechanic
}

class Job{
  let minimumSkillSet: Skill
  let name: String
  var completed: Bool = false

  init(minimumSkillSet: Skill, name: String){
    self.minimumSkillSet = minimumSkillSet
    self.name = name
  }
}


class Mechanic{
  let skill: Skill
  var name: String
  var isBusy: Bool = false

  init(skill: Skill, name: String){
    self.skill = skill
    self.name = name
  }

  func performJob(job: Job) -> Bool{
    if job.minimumSkillSet > self.skill || isBusy{
      assert(false, "This mechanic is either busy or insufficiently skilled for this job, he should have never been asked to perform it, there is something wrong in the chain of responsibility");
    }else
    {
      isBusy = true
      print("(name) with skill set (skill) has started to do (job.name)")
      job.completed = true
      return true
    }
  }
}
class MechanicSkillGroup{

  var mechanics: [Mechanic]
  var nextLevel: MechanicSkillGroup?
  var skill: Skill

  init(skill: Skill, mechanics: [Mechanic], nextLevel: MechanicSkillGroup?){
    self.mechanics = mechanics
    self.skill = skill
    self.nextLevel = nextLevel
  }

  func performJobOrPassItUp(job: Job) -> Bool{
    if (job.minimumSkillSet > skill || mechanics.filter({$0.isBusy == false}).count == 0){
      if let nextLevel = nextLevel{
        return nextLevel.performJobOrPassItUp(job)
      }else{
        print("No one is available to do this job")
        return false
      }
    }else{
      if let firstAvailableMechanic = mechanics.filter({$0.isBusy == false}).first{
        return firstAvailableMechanic.performJob(job)
      }
      assert(false, "This should never be reached since our if-else statement is fully exhaustive. You cannot have both all mechanics busy and an available mechanic within one skill group");
    }
  }
}

class Shop{

  private var firstMechanics: MechanicSkillGroup

  init(firstMechanics: MechanicSkillGroup){
      self.firstMechanics = firstMechanics
  }

  func performJob(job: Job) -> Bool{
    return firstMechanics.performJobOrPassItUp(job)
  }
}

Here is main with setup and some test cases

import Foundation

var steve = Mechanic(skill: .MasterMechanic, name: "Steve Frank")
var joe = Mechanic(skill: .MasterMechanic, name: "Joe Alison")
var jack = Mechanic(skill: .MasterMechanic, name: "Jack Ryan")
var brian = Mechanic(skill: .MasterMechanic, name: "Drake Jin")

var masterMechanics = MechanicSkillGroup(skill: .MasterMechanic, mechanics: [steve, joe, jack, brian], nextLevel: nil)

var tyson = Mechanic(skill: .Apprentice, name: "Tyson Trump")
var tina = Mechanic(skill: .Apprentice, name: "Tina Bernard")
var bryan = Mechanic(skill: .Apprentice, name: "Bryan Tram")
var lin = Mechanic(skill: .Apprentice, name: "Lin Young")

var apprenticeMechanics = MechanicSkillGroup(skill: .Apprentice, mechanics: [tyson, tina, bryan, lin], nextLevel: masterMechanics)

var ken = Mechanic(skill: .Junior, name: "Ken Hudson")
var matt = Mechanic(skill: .Junior, name: "Matt Lowes")
var sandeep = Mechanic(skill: .Junior, name: "Sandeep Shenoy")
var tom = Mechanic(skill: .Junior, name: "Tom Berry")

var juniorMechanics = MechanicSkillGroup(skill: .Junior, mechanics: [ken, matt, sandeep, tom], nextLevel: apprenticeMechanics)

var grant = Mechanic(skill: .OilChangeOnly, name: "Grant Hughes")
var larry = Mechanic(skill: .OilChangeOnly, name: "Larry White")
var bryant = Mechanic(skill: .OilChangeOnly, name: "Bryant Newman")
var reza = Mechanic(skill: .OilChangeOnly, name: "Reza Shirazian")
var laura = Mechanic(skill: .OilChangeOnly, name: "Laura Lee")
var arnold = Mechanic(skill: .OilChangeOnly, name: "Arnold Shummer")

var oilChangeOnlyes = MechanicSkillGroup(skill: .OilChangeOnly, mechanics: [grant], nextLevel: juniorMechanics)

var shop = Shop(firstMechanics: oilChangeOnlyes)

var jobs = [Job(minimumSkillSet: .Junior, name: "Windshield Viper"),
            Job(minimumSkillSet: .Apprentice, name: "Light Bulb Change"),
            Job(minimumSkillSet: .Apprentice, name: "Battery Replacement"),
            Job(minimumSkillSet: .OilChangeOnly, name: "General Oil Change"),
            Job(minimumSkillSet: .OilChangeOnly, name: "General Oil Change"),
            Job(minimumSkillSet: .OilChangeOnly, name: "General Oil Change"),
            Job(minimumSkillSet: .OilChangeOnly, name: "General Oil Change"),
            Job(minimumSkillSet: .MasterMechanic, name: "Timing Belt Replacement"),
            Job(minimumSkillSet: .Junior, name: "Brake Pads Replacement")
]

for job in jobs{
  shop.performJob(job)
}

Here is output you’d get with this setup:

Ken Hudson with skill set Junior has started to do Windshield Wiper
Tyson Trump with skill set Apprentice has started to do Light Bulb Change
Tina Bernard with skill set Apprentice has started to do Battery Replacement
Grant Hughes with skill set OilChangeOnly has started to do General Oil Change
Matt Lowes with skill set Junior has started to do General Oil Change
Sandeep Shenoy with skill set Junior has started to do General Oil Change
Tom Berry with skill set Junior has started to do General Oil Change
Steve Frank with skill set MasterMechanic has started to do Timing Belt Replacement
Bryan Tram with skill set Apprentice has started to do Brake Pads Replacement
Program ended with exit code: 9   

Solution

Formatting

There are several formatting issues with your code.

The standard is 4 spaces per indentation level. It looks like you’ve opted for 2, but it appears you’ve at least applied this consistently, although I really heavily prefer 4.

Your opening braces frequently appear squished right up against the preceding word. There should be a space.

In at least one place, you’ve left this:

}else
{

Which not only looks bad, but is inconsistent with other parts of your code.

Personally, I prefer not to “cuddle” my else (and catch) keywords, and as such, I write them like this:

}
else {

The primary inspiration behind this brace style has a lot to do with how Xcode handles collapsing code.

enter image description here

You also have a wild semicolon ; hanging out at the end of one of your assert statements.

Most of these things can be addressed automatically with SwiftLint.


assert & print

Neither of these are really particularly helpful.

First of all, if we’re going to log, we should use a real logging library. That’s a really minor issue here. The real problem here is that we’re printing to communicate things that should probably be communicate in the return values (or already are).

As for the asserts, again, I think we’re using these to communicate information that should probably be handled with return values. Or in some cases, we can probably reorganize our logic to simply get rid of them altogether.


Method Naming

What your methods do and how they’re named aren’t exactly in sync.

A method called performJob probably shouldn’t return a Bool indicating whether or not the job could be performed. And methods should generally never have Or or And as part of their name (there are exceptions, but they are rare).

The performJobOrPassItUp method probably makes a lot more sense framed in this way:

func firstAvailableMechanicForJobWithSkillLevel(skillLevel: Skill) -> Mechanic?

That is to say… the method either returns a mechanic or returns nil. Returning nil indicates there are no available mechanics and whatever is looking to get a job of that skill level accomplished must look elsewhere or try again later. Otherwise, it passes back out a mechanic which can do the job.


Boolean literals and checking if arrays are empty

mechanics.filter({$0.isBusy == false}).count == 0

First of all, we should almost never be comparing against boolean literals. We can simply write !$0.isBusy.

Second of all, if we want to know whether an array is empty, we should check isEmpty:

mechanics.filter({!$0.isBusy}).isEmpty

Initializers

In both your Mechanic and Job classes, you don’t have a way to initialize all of the variables. In both cases, you default initialize a Bool value. Why not just make that part of the initializers, but give it a default value.

class Job{
    let minimumSkillSet: Skill
    let name: String
    var completed: Bool

    init(minimumSkillSet: Skill, name: String, completed: Bool = false) {
        self.minimumSkillSet = minimumSkillSet
        self.name = name 
        self.completed = false
    }
}

String-Typing

The Mechanic class has a name property which is a string. And this is fine. The name here is nothing more than an identifier which we might want to use to display to the user or something.

But the Job class’s name property is doing something completely different. We may still want a pretty string to display to the user, but this property is doing more than that. I think we need a full fledged type here. Why, for example, would a "General Oil Change" ever require anything other than the OilChangeOnly skill? So the list of jobs is surely countable, and the actual skill level for any particular job is surely constant.

Leave a Reply

Your email address will not be published. Required fields are marked *