Skip to content

kingreza/Swift-Visitor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Design Patterns in Swift: Visitor

This repository is part of a series. For the full list check out Design Patterns in Swift

For a cheat-sheet of design patterns implemented in Swift check out Design Patterns implemented in Swift: A cheat-sheet

The problem:

Our customers at YourMechanic can request quotes through our website or through their YourMechanic App. We send out an email and create an internal report when such quotes are requested. After the customer views the quote and decides to book it, that quote becomes an appointment. We send out an email and create an internal report when that happens as well. The content of the email and the report are dynamically generated from the information contained in the quote and the appointment.  We need a system that can provide us with an easy and straightforward way of composing these documents. Ideally we hope to achieve this without adding repeated report/email functionality to our already cluttered Quote and Appointment class.

The solution:

We define our quote and appointment class to accept visitor object. We then define these objects for emails and reports. When these objects are accepted into their respective visiting classes, they can generate the documents needed without having to be part of that class. The Visitor design pattern has always been a little counter intuitive to understand so we are going to approach this step by step, with as much details as possible.

Link to the repo for the completed project: Swift - Visitor

Let's begin:

We are going to start off by constructing our objects. This way we can get the easy stuff out of the way. First off we define our car object.

struct Car {
  let make: String
  let model: String
  let mileage: Int

  init(make: String, model: String, mileage: Int) {
    self.make = make
    self.model = model
    self.mileage = mileage
  }
}

For the sake brevity we are going to have our car have a make, model, and mileage property. We initialize these values through a standard initializer. Next we define our mechanic object.

struct Mechanic {
  let name: String

  init(name: String) {
    self.name = name
  }
}

Since the mechanic object will not be playing a major role in our Visitor example, we'll make it as simple as possible. It will be a simple struct that will have a name property. And finally we'll define a customer object that will have a name, email, zip code and address.

struct Customer {
  let name: String
  let email: String
  let zipcode: String
  let address: String

  init(name: String, email: String, zipcode: String, address: String) {
    self.name = name
    self.email = email
    self.zipcode = zipcode
    self.address = address
  }
}

These values are set through a standard initializer. It's been pretty straightforward so far. So here's where things get a little bit more interesting.

protocol Documentable {
  func accept (documenter: Documenter)
}
protocol Documenter {
  func process(documentable: Quote)
  func process(documentable: Appointment)
}

We define two protocols. In this example I've called them Documentable and Documenter. Another way they could have been named is Visitable and Visitor. The class that will implement the Documentable protocol will accept a Documenter visitor. This means the documentable class can delegate the task of generating the needed documents, be it an email or report to its visitor class the Documentor. The class that implements the Documenter, processes the Documentable objects. In our example we have two Documentable objects Quote and Appointment and two Documenter classes ReportDocumenter and EmailDocumenter.

The way we are planning to generate our emails and reports is to have two classes that implement the Documenter protocol: one for emails and another for reports. These Documenter class will have the functionality needed to generate the email and report without being part of the Quote and Appointment classes. By using visitor we can extend the functionalities of our two visitable classes through external visiting objects.

Before looking at these Documenter classes, let's look at our Quote and Appointment class:

class Quote: Documentable {

  var customer: Customer
  var price: Double
  var car: Car

  init(customer: Customer, price: Double, car: Car) {
    self.customer = customer
    self.price = price
    self.car = car
  }

  func accept (documenter: Documenter) {
    documenter.process(self)
  }
}

The Quote class implements the Documentable protocol, which requires it to have an 'accept' method, taking a Documenter as parameter. This function in turn calls the process method on the Documentor, passing itself. When that is called, any subsequent code that is executed will be part of the visitor class's process function. And since the Documentable class is passing itself to the process method of the Visitor class, the Visitor will have access to it outside the Documentable class code base.

Using this pattern you can now add functionalities and new behaviours without having to add anything to the visitable class. Let's come back to this idea once we have everything in place.

class Appointment: Documentable {

  var customer: Customer
  var mechanic: Mechanic
  var price: Double
  var date: NSDate

  init(customer: Customer, mechanic: Mechanic, price: Double, date: NSDate) {
    self.customer = customer
    self.mechanic = mechanic
    self.price = price
    self.date = date
  }

  func accept (documenter: Documenter) {
    documenter.process(self)
  }
}

We do the same thing with our Appointment class. It implements Documentable and like the Quote class, it calls the process method in its Documenter (visitor) object when a Documenter is accepted by it.

Before building our Documenter classes let's build our Email and Report classes. These are the object that our Documenter (visitor) classes will be constructing.

struct Email {
  let from: String
  let to: String
  let subject: String
  var body: String

  init(from: String = "hi@example.com", to: String, subject: String = "", body: String = "" ) {
    self.from = from
    self.to = to
    self.subject = subject
    self.body = body
  }

  func output() {
    print("From: \(self.from)")
    print("To: \(self.to)")
    print("Subject: \(self.subject)")
    print("Body: \(body)")
  }
}

struct Report {

  let reportType: ReportType
  let title: String
  let content: String

  init(reportType: ReportType, title: String, content: String) {
    self.reportType = reportType
    self.title = title
    self.content = content
  }

  func output() {
    print("Report: \(self.title)")
    print("Content: \(self.content)")
    print("************************\n")
  }
}

enum ReportType: Int {
  case Quote = 0, Appointment
}

There is nothing substantially interesting about these classes. The Email struct holds properties (to, from, subject, etc) that you expect to see in an Email object. The Report struct contains similar objects intended to simulate some arbitrary custom report in our internal system. I have added a simple output function for both of these classes that prints their content to the console.

Now let's see how our Documenters (visitor) classes build these objects.

class EmailDocumenter: Documenter {
  func process(documentable: Quote) {
    var content = "Hello \(documentable.customer.name) \n"
    content += "We have a quote for your \(documentable.car.make) \(documentable.car.model) \n"
    content += "For the services you have requested in \(documentable.customer.address) \n"
    content += "We have generated a quote priced at \(documentable.price) \n"
    let email = Email(to: documentable.customer.email,
                      subject: "Here is a quote for your \(documentable.car.make)",
                      body: content)

    email.output()
  }

  func process(documentable: Appointment) {
    var content = "Hello \(documentable.customer.name) \n"
    content += "We have booked your appointment for \(documentable.date.shortDateAndTime) \n"
    content += "make sure you have not driven your car for an hour before the appointment \n"
    content += "\(documentable.mechanic.name) will be more than happy " +
               "to answer any questions you might have \n"
    content += "You card will be billed for \(documentable.price) " +
                "once the appointment is finished \n"

    let email = Email(to: documentable.customer.email,
                      subject: "Your appointment is set for \(documentable.date.shortDateAndTime)",
                      body: content)

     email.output()

  }
}

Let's start with our EmailDocumenter which is the visitor responsible for building our emails. This class implements the Documenter protocol which forces it to implement two process methods, one accepting a Quote and another accepting an Appointment. In the process method for Quote, we take the Quote and use its content to construct an Email. This email ends up being what we send to the customer to inform them about their quote.

The second process function takes an Appointment as a parameter. And naturally builds an Email related to the appointment booked by the customer. To double check and make sure our emails are being constructed correctly we call the output function after they are generated. This will print out their content to the console.

This class is responsible for building our emails for both Quote and Appointments. As you can see none of this code is in our Quote or Appointment classes. The functionality to generate emails has been separated out of those objects and housed in one central place.

This is ofcourse possible because our Quote and Appointment classes accept Documentors (visitors). Using this we can keep extending new functionalities without mucking around the classes themselves. So let's keep going. We still have a report to build.

class ReportDocumenter: Documenter {
  func process(documentable: Quote) {
    var content = "Quote for \(documentable.car.make) \(documentable.car.model) was generated \n"
    content += "Customer:\t \(documentable.customer.name) \n"
    content += "Address:\t \(documentable.customer.address) \n"
    content += "Quoted Price:\t \(documentable.price)"

    let report = Report(reportType: .Quote,
                        title: "Quote Generation Report for \(documentable.customer.name)",
                        content: content)

    report.output()
  }
  func process(documentable: Appointment) {
    var content = "Appointment for \(documentable.customer.name) was generated\n"
    content += "Customer:\t \(documentable.customer.name)\n"
    content += "Mechanic:\t \(documentable.mechanic.name)\n"
    content += "Time:\t \(documentable.date.shortDateAndTime)"
    content += "Price:\t \(documentable.price)"

    let report = Report(reportType: .Appointment,
                        title: "Appointment Generation Report for \(documentable.customer.name)",
                        content: content)

    report.output()

  }
}

Our ReportDocumenter class (another visitor), much like our EmailDocumenter class implements the Documenter protocol. And like EmailDocumenter, it has two process methods, one taking a Quote and another taking an Appointment. The code within the process methods simply takes the objects passed and uses its properties to build the needed report.

Another benefit of using the Visitor pattern is the fact that you can group similar functionalities together. In our example, the email generation for both Quote and Appointment is in one place and the report generation is in another. Since these two processes are not really related to the Quote or the Appointment or each other, this makes sense.

Now that we have everything in place lets see them in action.

var joe = Mechanic(name: "Joe Stevenson")
var mike = Mechanic (name: "Mike Dundee")

var reza = Customer(name: "Reza Shirazian",
                    email: "reza@example.com",
                    zipcode: "94043",
                    address: "N Rengstorff ave")

var lyanne = Customer(name: "Lyanne Borne",
                      email: "jb_hhm@example.com",
                      zipcode: "37110",
                      address: "E Main St McMinnvile TN")

var sam = Customer(name: "Sam Lee",
                   email: "lee.sam.3oo@example.com",
                   zipcode: "95060",
                   address: "Pacific Ave, Santa Cruz")

var quote1 = Quote(customer: reza,
                   price: 55.00,
                   car: Car(make: "Ford", model: "Mustang", mileage: 9500))

var quote2 = Quote(customer: lyanne,
                   price: 463.25,
                   car: Car(make: "Chevrolet", model: "Silverado",
                   mileage: 15200))

var quote3 = Quote(customer: sam,
                   price: 1155.00,
                   car: Car(make: "Honda", model: "Civic",
                   mileage: 78000))

var appointment1 = Appointment(customer: reza,
                               mechanic: joe,
                               price: 455.88,
                               date: NSDate.generateDateFromArray([2016, 5, 12, 14, 30, 00])!)

var appointment2 = Appointment(customer: sam,
                               mechanic: mike,
                               price: 554.00,
                               date: NSDate.generateDateFromArray([2016, 5, 23, 20, 00, 00])!)

We start be defining our mechanics, customers, quotes and appointments. I used some extensions on NSDate for easy date time creation and formatting. I will include the code for that at end of this article since they're not really related to the visitor design pattern. You can find the repo for the complete solution here: Design Patterns in Swift: Visitor

Once our basic objects are setup, we'll put our quotes and appointments into an array, instantiate our EmailDocumenter and ReportDocumenter and iterate through each quote and appointment, having each accept our Documenter:

var quotes = [quote1, quote2, quote3]
var appointments = [appointment1, appointment2]

var emailDocumenter = EmailDocumenter()

var reportDocumenter = ReportDocumenter()

for quote in quotes {
  quote.accept(emailDocumenter)
  quote.accept(reportDocumenter)
}

for appointment in appointments {
  appointment.accept(emailDocumenter)
  appointment.accept(reportDocumenter)
}

When our Documenter is accepted by a Quote or an Appointment the process method for each Documenter is called, then depending on the type of the object visited, (Quote or Appointment) the correct process method in the Documenter is executed. I suggest putting breakpoints on the accept calls for the Quote or Appointment objects and stepping through the code so you can easily visualise the flow of execution.

With this setup, our main execution results in the following output:

From: hi@yourmechanic.com
To: reza@example.com
Subject: Here is a quote for your Ford
Body: Hello Reza Shirazian 
We have a quote for your Ford Mustang 
For the services you have requested in N Rengstorff ave 
We have generated a quote priced at 55.0 

Report: Quote Generation Report for Reza Shirazian
Content: Quote for Ford Mustang was generated 
Customer:	 Reza Shirazian 
Address:	 N Rengstorff ave 
Quoted Price:	 55.0
************************

From: hi@yourmechanic.com
To: jb_hhm@example.com
Subject: Here is a quote for your Chevrolet
Body: Hello Lyanne Borne 
We have a quote for your Chevrolet Silverado 
For the services you have requested in E Main St McMinnvile TN 
We have generated a quote priced at 463.25 

Report: Quote Generation Report for Lyanne Borne
Content: Quote for Chevrolet Silverado was generated 
Customer:	 Lyanne Borne 
Address:	 E Main St McMinnvile TN 
Quoted Price:	 463.25
************************

From: hi@yourmechanic.com
To: lee.sam.3oo@example.com
Subject: Here is a quote for your Honda
Body: Hello Sam Lee 
We have a quote for your Honda Civic 
For the services you have requested in Pacific Ave, Santa Cruz 
We have generated a quote priced at 1155.0 

Report: Quote Generation Report for Sam Lee
Content: Quote for Honda Civic was generated 
Customer:	 Sam Lee 
Address:	 Pacific Ave, Santa Cruz 
Quoted Price:	 1155.0
************************

From: hi@yourmechanic.com
To: reza@example.com
Subject: Your appointment is set for 12/5/2016, 14:30
Body: Hello Reza Shirazian 
We have booked your appointment for 12/5/2016, 14:30 
make sure you have not driven your car for an hour before the appointment 
Joe Stevenson will be more than happy to answer any questions you might have 
You card will be billed for 455.88 once the appointment is finished 

Report: Appointment Generation Report for Reza Shirazian
Content: Appointment for Reza Shirazian was generated
Customer:	 Reza Shirazian
Mechanic:	 Joe Stevenson
Time:	 12/5/2016, 14:30Price:	 455.88
************************

From: hi@yourmechanic.com
To: lee.sam.3oo@example.com
Subject: Your appointment is set for 23/5/2016, 20:00
Body: Hello Sam Lee 
We have booked your appointment for 23/5/2016, 20:00 
make sure you have not driven your car for an hour before the appointment 
Mike Dundee will be more than happy to answer any questions you might have 
You card will be billed for 554.0 once the appointment is finished 

Report: Appointment Generation Report for Sam Lee
Content: Appointment for Sam Lee was generated
Customer:	 Sam Lee
Mechanic:	 Mike Dundee
Time:	 23/5/2016, 20:00Price:	 554.0
************************

Program ended with exit code: 0

As you can see for each Quote we have generated an Email and a Report, and we have done the same for each Appointment.

Congratulations you have just implemented the Visitor Design Pattern to solve a nontrivial problem.

The repo for the complete project can be found here: Swift - Visitor.

Download a copy of it and play around with it. See if you can find ways to improve its design, Add more complex functionalities. Here are some suggestions on how to expand or improve on the project:

  • Define more documentable objects within the project and extend our documenters to visit and process their data
  • Assume we want to build a receipt for every appointment. How would you extend our current visitor pattern to accommodate this?

About

Design Patterns in Swift: Visitor

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages