I'm solving the following problem using the Visitor design pattern in Swift:
When a quote is requested or an appointment is booked we send out an email to the customer, and build an internal report about the the quote and the appointment. The content of the email and report are dependant on the customer's address, their car make model, price and the mechanic selected for the service. We need a system that can provide us with an easy and straightforward way of composing these documents. Ideally without having to add the email/report functionality into our already cluttered quote and appointment classes.
I would appreciate feedback on how this can be improved, if it remains true to the visitor design pattern and any input on design decisions I've made for solving this problem.
Here is the full code, the full repo can be found here: Design Patterns in Swift: Visitor
protocol Documentable {
func accept (documenter: Documenter)
}
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)
}
}
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)
}
}
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
}
}
struct Mechanic {
let name: String
init(name: String) {
self.name = name
}
}
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
}
}
protocol Documenter {
func process(documentable: Quote)
func process(documentable: Appointment)
}
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.process()
}
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.process()
}
}
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.process()
}
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.process()
}
}
enum ReportType: Int {
case Quote = 0, Appointment
}
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 process() {
print("Report: \(self.title)")
print("Content: \(self.content)")
print("************************\n")
}
}
struct Email {
let from: String
let to: String
let subject: String
var body: String
init(from: String = "[email protected]", to: String, subject: String = "", body: String = "" ) {
self.from = from
self.to = to
self.subject = subject
self.body = body
}
func process() {
print("From: \(self.from)")
print("To: \(self.to)")
print("Subject: \(self.subject)")
print("Body: \(body)")
}
}
extension NSDateFormatter {
convenience init(dateFormat: String) {
self.init()
self.dateFormat = dateFormat
}
}
extension NSDate {
struct Date {
static let formatterShortDateAndTime = NSDateFormatter(dateFormat: "dd/M/yyyy, H:mm")
}
var shortDateAndTime: String {
return Date.formatterShortDateAndTime.stringFromDate(self)
}
static func generateDateFromArray(input: [Int]) -> NSDate? {
guard input.count == 6 else {
return nil
}
let calendar = NSCalendar(identifier: NSCalendarIdentifierGregorian)
let components = NSDateComponents()
components.year = input[0]
components.month = input[1]
components.day = input[2]
components.hour = input[3]
components.minute = input[4]
components.second = input[5]
return calendar?.dateFromComponents(components)
}
}
Here is the main setup and some test cases:
var joe = Mechanic(name: "Joe Stevenson")
var mike = Mechanic (name: "Mike Dundee")
var reza = Customer(name: "Reza Shirazian",
email: "[email protected]",
zipcode: "94043",
address: "N Rengstorff ave")
var lyanne = Customer(name: "Lyanne Borne",
email: "[email protected]",
zipcode: "37110",
address: "E Main St McMinnvile TN")
var sam = Customer(name: "Sam Lee",
email: "[email protected]",
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])!)
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)
}
Here is the output we get with the current setup
From: [email protected]
To: [email protected]
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: [email protected]
To: [email protected]
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: [email protected]
To: [email protected]
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: [email protected]
To: [email protected]
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: [email protected]
To: [email protected]
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
************************