Learn Go by Building a Bus Service


Go is a pragmatic language. It’s about getting stuff done. This allows for a more direct coding style that I personally find to be a relief from the “classic” object-oriented coding styles with their proliferation of class hierarchies. With Go, we can still take advantage of many relevant design patterns without getting distracted by having to write getters, setters, factory classes and complex class hierarchies.

Design patterns are fundamentally just recipes - or best practices - for how to approach certain classes of problems. But when applied in Go, these patterns become simple, or almost trivial, to implement. Perhaps in part due to the keyword minimalism (public static void main, anyone?), in part due to the simplicity of structs compared to classes with their constructors and destructors, or because of the lack of inheritance to foul up attempts at decoupling the code. But most importantly because Go has excellent support for functional programming. These qualities combine to make design patterns simple and succinct in Go. So with that in mind, let’s demonstrate by going on a bus ride! Along the way, we’ll deliver passengers to their destinations while also applying some design patterns. There will be plenty of Go code along the way, so at least basic familiarity with the language is expected for following along.

A Bus Service

Let’s first define a bus with the purpose of transporting passengers. For the sake of ticket handling, we’ll need to ID each passenger, and we’ll use their Social Security Number (SSN) for that. These will all be defined in a busservice package.

Note: Thanks to response from Reddit for pointing out that in a real-world application, we should never use the SSN as reference in our data structures as the SSN is a sensitive and private piece of information in many countries. For the purposes of demonstration, however, we make an exception.

package busservice

// Passenger represents a bus passenger, uniquely identified by their SSN.
type Passenger struct {
	SSN string
	SeatNumber uint8
}

// Bus carries Passengers from A to B if they have a valid bus ticket.
type Bus struct {
	name string
	Passengers []Passenger
}

Code snippet 01

This is great! We have a bus with some passengers. Observe how we might get a manifest (passenger list), after first adding a few Passengers.

func main() {
	expressLine := busservice.Bus{"Express Line"}
	expressLine.Passengers := make([]busservice.Passenger, 0)
	expressLine.Passengers = append(expressLine.Passengers, busservice.Passenger{SSN: "001"})
	expressLine.Passengers = append(expressLine.Passengers, busservice.Passenger{SSN: "002"})

	// Get a manifest!
	ssns := make([]string, 0)
	for _, p := range expressLine.Passengers {
		ssns = append(ssns, p.SSN)
	}
	fmt.Printf("This bus carries %d passengers, here are their SSN's: %v\n", len(ssns), ssns)
}

Code snippet 02

Lovely. But there’s a problem! We have exposed the Passengers data structure to the outside world. We are free to use any data structure we want inside our busservice package, but when it concerns the package’s outward interface, we have to take steps to hide the implementation details. Otherwise, we lose control over our own package, and it will be impossible to change the Passengers slice later without affecting the rest of the application. But why would we change it in the first place?

Let’s say that our Bus grows (functionality-wise) to a point where we find ourselves often needing to find a specific Passenger by SSN. Having to iterate the Passengers slice every single time is sub-optimal, and we want to replace it with a map as shown below.

// Bus carries Passengers from A to B if they have a valid bus ticket.
type Bus struct {
	name string
	Passengers map[string]Passenger // Map of SSN to Passengers
}

Code snippet 03

But this affects everything, including how we add Passengers to the Bus. We already know the solution: make the Passengers data structure unexported (private) and export some convenience methods for working with it. But how do you iterate over an arbitrary data structure as we’ll need to do in order to generate a manifest?

The Visitor Pattern

This is where the Visitor Pattern comes in. We’ll define a method VisitPassengers, which takes a function that is called for every passenger. For good measure, let’s also internalize the functionality for adding passengers. Observe the implementation below.

// Bus carries Passengers from A to B if they have a valid bus ticket.
type Bus struct {
	name string
	passengers map[string]Passenger
}

// Add adds a single Passenger to the Bus. For brevity, we don't care too much about
// accidentally adding the same Passenger more than once.
func (b *Bus) Add(p Passenger) {
	if b.passengers == nil {
		b.passengers = make(map[string]Passenger)
	}
	b.passengers[p.SSN] = p
}

// VisitPassengers calls function visitor for each Passenger on the Bus.
func (b *Bus) VisitPassengers(visitor func(Passenger)) {
	for _, p := range b.passengers {
		visitor(p)
	}
}

Code snippet 04

Now let’s take another look at our main function.

func main() {
	expressLine := busservice.Bus{"Express Line"}
	expressLine.Add(busservice.Passenger{"001"})
	expressLine.Add(busservice.Passenger{"002"})
	
	// Get a manifest!
	ssns := make([]string, 0)
	expressLine.VisitPassengers(func(p busservice.Passenger) { ssns = append(ssns, p.SSN) })
	fmt.Printf("This bus carries %d passengers, here are their SSN's: %v\n", len(ssns), ssns)
}

Code snippet 05

That’s a lot better! We’ve successfully hidden the data structure from the outside world, which means we have the freedom to change it later, should we so desire. We can even replace it with a binary search tree if we wanted to, without affecting the rest of the application. To achieve that, we would have to change the logic of the VisitPassengers method, but it would still get called exactly the same way. Also, now we can quickly and efficiently find a specific Passenger using constant-time lookup O(1) rather than looping over a slice, which is done in linear time, O(n).

// FindPassenger returns the Passenger that matches the given SSN, if found.
// Otherwise, an empty Passenger is returned.
func (b *Bus) FindPassenger(ssn string) Passenger {
	if p, ok := b.passengers[ssn]; ok {
		return p
	}
	return Passenger{} // A nobody.
}

Code snippet 06

Note that VisitPassengers calls visitor with a value of type Passenger, which means that the visitor is receiving a copy of the Passenger, and therefore cannot change anything on the Passenger - at least not as far as Bus is concerned. For more information on this topic, Yury Pitsishin has a good explanation. Passing a copy of Passenger is a good protection to have since a regular visitor should probably not have the option of modifying it, but we could easily add support for updating passengers if we wished to. See below.

// UpdatePassengers calls function visitor for each Passenger on the bus.
// Passengers are passed by reference and may be modified.
func (b *Bus) UpdatePassengers(visitor func(*Passenger)) {
	ps := make(map[string]Passenger, len(b.passengers))
	for ssn, p := range b.passengers {
		visitor(&p)
		ps[ssn] = p
	}
	b.passengers = ps
}

Code snippet 07

Since we’re not using pointers to Passenger in b.passengers, we’ll have to create a new map with the updated Passengers and assign it back to Bus, as we can’t take the address of map entries. This is just a nuance of the current implementation as you could also have used a type with pointers.

Why can’t we take the address of a map member? For more information on why not, see this post.

With this implementation, we can start assigning seat numbers if we want to.

func main() {
	// Previous statements...

	// Assign seat numbers to passengers.
	var seatNumber uint8
	expressLine.UpdatePassengers(func(p *busservice.Passenger) {
		seatNumber++
		p.SeatNumber = seatNumber
	})

	expressLine.VisitPassengers(func(p busservice.Passenger) {
		fmt.Printf("Passenger with SSN %q has seat %d\n", p.SSN, p.SeatNumber)
	})
}

Code snippet 08

The Facade Pattern

As our application grows and we want to interact more with sets of Passengers (such as when getting the manifest), we can take it further and convert a map of Passengers into its own datatype, with its own dedicated set of methods that work on the entire set. This is perfectly doable while respecting the privacy of the data structure itself. Observe.

// Passengers represents a set of Passengers, using their SSN as key.
type Passengers map[string]Passenger

// NewPassengerSet returns an empty set of Passengers, ready to use.
func NewPassengerSet() Passengers {
	return make(map[string]Passenger)
}

// Visit calls visitor once for every Passenger in the set.
func (p Passengers) Visit(visitor func(Passenger)) {
	for _, one := range p {
		visitor(one)
	}
}

// Find returns the Passenger with the given SSN. If none was found,
// an empty Passenger is returned.
func (p Passengers) Find(ssn string) Passenger {
	if one, ok := p[ssn]; ok {
		return one
	}
	return Passenger{}
}

// VisitUpdate calls visitor for each Passenger in the set.
// Updating their SSN's is not recommended.
func (p Passengers) VisitUpdate(visitor func(p *Passenger)) {
	for ssn, pp := range *p {
		visitor(&pp)
		(*p)[ssn] = pp
	}
}

// Manifest returns the SSN's of all Passengers in the set.
func (p Passengers) Manifest() []string {
	ssns:=make([]string, 0, len(p))
	p.Visit(func(p Passenger) { ssns = append(ssns, p.SSN) })
	return ssns
}

Code snippet 09

Notice how we make use of our own Visit method in the implementation. At this point, we’ve also introduced a constructor function (NewPassengerSet) to make sure that Bus doesn’t have to know which data structure we’ve used on our implementation. This is Bus now.

// Bus carries Passengers from A to B if they have a valid bus ticket.
type Bus struct {
	name string
	passengers Passengers
}

// NewBus returns a new Bus with an empty passenger set.
func NewBus(name string) Bus {
	b := Bus{}
	b.name = name
	b.passengers = NewPassengerSet()
	return b
}

// Add adds a single passenger to the Bus. For brevity, we don't care too much
// about accidentally adding the same Passenger more than once.
func (b *Bus) Add(p Passenger) {
	if b.passengers == nil {
		b.passengers = make(map[string]Passenger)
	}
	b.passengers[p.SSN] = p
	fmt.Printf("%s: boarded passenger with SSN %q\n", b.name, p.SSN)
}

// Remove removes a single Passenger from the Bus.
func (b *Bus) Remove(p Passenger) {
	delete(b.passengers, p.SSN)
	fmt.Printf("%s: unboarded passenger with SSN %q\n", b.name, p.SSN)
}

// Manifest asks Passengers for a SSN manifest and returns it.
func (b Bus) Manifest() []string {
	return b.passengers.Manifest()
}

// VisitPassengers calls function visitor for each Passenger on the bus.
func (b *Bus) VisitPassengers(visitor func(Passenger)) {
	b.passengers.Visit(visitor)
}

// FindPassenger and UpdatePassengers look the same.

Code snippet 10

Due to the constructor function, func main has changed a bit.

func main() {
	expressLine := busservice.NewBus()
	expressLine.Add(busservice.Passenger{SSN: "001"})
	expressLine.Add(busservice.Passenger{SSN: "002"})
	
	// Get a manifest!
	ssns := expressLine.Manifest()
	fmt.Printf("This bus carries %d passengers, here are their SSN's: %v\n", len(ssns), ssns)
}

Code snippet 11

Notice how the responsibility of handling passengers has shifted from Bus to Passengers. This is good, since Bus should really be more concerned with the responsibility of getting its payload from A to B. Also, all the logic concerning passengers has simplified from the perspective of Bus, and Bus can even choose not to expose the functionality for updating its Passengers. By turning Passengers into its own type, which is then embedded in an unexported field on Bus, we’ve also demonstrated the Facade Pattern where Bus provides the interface to its passengers and has complete control over which methods to expose, and which not to.

The Observer Pattern

Let’s take a look at the bus stop. There are people there waiting to board the Bus, but they are not Passengers yet, not until they actually board the Bus. Until that happens, we’ll call them Prospects.

The bus stop itself could simply be a name (of the stop), a list of prospects and a list of known Busses that has that stop on their route. Let’s define them. For the Prospect, we’ll probably need to know where they are going as well, so let’s add a Destination and some boilerplate code for BusStop.

// Prospect is a potential Passenger. Prospects wait at BusStops to board Buses.
type Prospect struct {
	SSN string
	Destination *BusStop
}

// BusStop represents a place where a Bus can stop and signal to
// prospects (future passengers) that they may board.
type BusStop struct {
	Name string
	prospects []Prospect
	busses []Bus
}

// AddStop adds the given BusStop to the list of stops that the
// Bus will stop at. Each stop is visited in order.
func (b *Bus) AddStop(busStop *BusStop) {
	b.stops = append(b.stops, busStop)
}

Code snippet 12

How will the Prospects know when a Bus arrives at a BusStop? There needs to be some kind of event to signal the Bus arrival and set in motion the boarding algorithm. Although Bus could simply search for the appropriate BusStop and call a NotifyArrival method on it, this will not fly. There could be hundreds of stops, many of them empty, and maintaining such a list for each Bus is not tenable. Instead, when a Prospect arrives at a BusStop, the BusStop will register itself with the Busses on its route. This is more efficient as the Bus won’t need to maintain a list of every Prospect that may board it, nor will it need to receive an update when more Prospects arrive as the Bus is en-route. On top of that, it’s an advantage for the Bus to know in advance which BusStops it can skip.

The arrival of a Prospect is an event in itself, and it requires Prospects to know the BusStops, but let’s focus on the dynamics between BusStop and Bus. When a Prospect arrives, the method NotifyProspectArrival is called. It may look like this.

// NotifyProspectArrival is called whenever a prospect arrives at Busstop.
func (b *BusStop) NotifyProspectArrival(p Prospect) {
	b.prospects = append(b.prospects, p)
	// Notify all Busses on this route.
	for _, bus := range b.busses {
		if bus.StopsAt(p.Destination) {
			bus.NotifyBoardingIntent(b)
		}
	}
}

Code snippet 13

And we’ll need to implement NotifyBoardingIntent as well.

// NotifyBoardingIntent is called by BusStop every time a Prospect arrives
// and instructs the Bus to signal its arrival at that BusStop. 
func (b *Bus) NotifyBoardingIntent(busStop *BusStop) {
	if b.StopsAt(busStop) {
		return // We already intend to stop here.
	}
	b.addBusStop(busStop)
}

Code snippet 14

And the related notifier.

// NotifyArrival notifies the current BusStop that the Bus has arrived.
func (b *Bus) NotifyArrival() {
	curr := b.stops[b.currentStop]
	curr.NotifyBusArrival(b)
}

Code snippet 15

This example is a bit atypical compared to the typical examples of Observer patterns, but the principle is the same: The core of the pattern is NotifyBoardingIntent that allows BusStop to register itself as an Observer with the Bus, and NotifyArrival where the Subject (Bus) notifies the observer of its arrival. Since Bus only arrives at exactly one BusStop, we find that exact BusStop by calling b.currentStop and then notify it of our arrival by calling curr.NotifyBusArrival. More information about the role of observers and subjects can be found here.

There’s a few methods here that we didn’t implement yet, but they are trivial. We’ll keep a slice of BusStops internally and an index into that slice that tells us which one is the current BusStop.

// Bus carries Passengers from A to B if they have a valid bus ticket.
type Bus struct {
	name string
	passengers Passengers
	stops []*BusStop
	currentStop int16
}

Code snippet 16

The type for stops are pointers so we make sure we’re notifying the actual BusStops and not copies of them. Since stops is an internal data structure, we don’t need any fancy Visitor Patterns here; we have no intention of exposing this list, at least not for the purposes of this article.

// StopsAt checks if Bus stops at the given BusStop, and returns true
// if it does, and false otherwise.
func (b Bus) StopsAt(busStop *BusStop) bool {
	for _, stop := range b.stops {
		if stop.Equals(busStop) { // Some kind of equality check, anyway.
			return true
		}
	}
	return false
}

// CurrentStop returns the BusStop that the Bus is currently stopped at.
func (b Bus) CurrentStop() *BusStop {
	return b.stops[b.currentStop]
}

// AddStop adds the given BusStop to the list of stops that the Bus will stop at.
// Each stop is visited in order.
func (b *Bus) AddStop(busStop *BusStop) {
	b.stops = append(b.stops, busStop)
}

// Go takes the Bus to the next BusStop. Go returns true if there are still more
// stops to visit.
func (b *Bus) Go() bool {
	b.currentStop++
	lastIndex := int16(len(b.stops) - 1)
	if b.currentStop == lastIndex {
		fmt.Printf("%s: reached the end of the line, everybody out\n", b.name)
		b.VisitPassengers(func(p Passenger) {
			b.Remove(p)
		})
		return false
	}
	if b.currentStop == 0 {
	        fmt.Printf("%s: starting\n", b.name)
	} else {
		fmt.Printf(
			"%s: carrying %d passengers: heading for next stop\n",
			b.name,
			len(b.passengers),
		)
	}
	curr := b.stops[b.currentStop]
	fmt.Printf("%s: arriving at %q\n", b.name, curr.Name)
	curr.NotifyBusArrival(b)
	return b.currentStop < lastIndex
}

Code snippet 17

The above works a bit better if we initialize the Bus with a currentStop of -1, so let’s change the constructor.

// NewBus returns a new Bus with an empty passenger set.
func NewBus(name string) Bus {
	b := Bus{}
        b.name = name
	b.currentStop = -1
	b.passengers = NewPassengerSet()
	return b
}

Code snippet 18

And the notifier and equality check on BusStop is implemented below, with the former having the purpose of boarding its prospects.

// NotifyBusArrival is called by Bus upon arrival.
func (b *BusStop) NotifyBusArrival(bus *Bus) {
	for _, p := range b.prospects {
		if bus.StopsAt(p.Destination) {
			bus.Add(p.ToPassenger())
		}
	}
}

// Equals returns true if the given BusStop is the same as the receiver.
func (b *BusStop) Equals(busStop *BusStop) bool {
	return b.Name == busStop.Name
}

Code snippet 19

You can have Prospect convert himself into a Passenger or let Bus have that responsibility. The advantage of the first approach is that Bus never have to know what a Prospect is, so I personally consider that a better implementation. Converting a Prospect into a Passenger is trivial because they are structurally similar, so we just need to transfer the SSN.

// ToPassenger returns a Passenger with the same SSN as his or her Prospect.
func (p Prospect) ToPassenger() Passenger {
	return Passenger{SSN: p.SSN, , Destination: p.Destination}
}

Code snippet 20

And that’s pretty much all there is to the Observer Pattern.

At this point, we can actually implement the algorithm that makes the Bus drive around and pick up Passengers. We’ll do this in main with a few BusStops and Passengers to make it interesting.

func main() {
	fmt.Println("Starting simulation")
	expressLine := busservice.NewBus("Express Line")

	s1 := busservice.BusStop{Name: "Downtown"}
	s2 := busservice.BusStop{Name: "The University"}
	s3 := busservice.BusStop{Name: "The Village"}

	expressLine.AddStop(&s1)
	expressLine.AddStop(&s2)
	expressLine.AddStop(&s3)

	john := busservice.Prospect{
		SSN:         "12345612-22",
		Destination: &s2,
	}
	betty := busservice.Prospect{
		SSN:         "11223322-67",
		Destination: &s3,
	}
	s1.NotifyProspectArrival(john)
	s1.NotifyProspectArrival(betty)

	for expressLine.Go() {
		expressLine.VisitPassengers(func(p busservice.Passenger) {
			fmt.Printf(
				" Passenger with SSN %q is heading to %q\n",
				p.SSN,
				p.Destination.Name,
			)
		})
	}
	fmt.Println("Simulation done")
}

Code snippet 21

Running the simulation should produce the following output.

> go run main.go

Starting simulation
Express Line: starting
Express Line: arriving at "Downtown"
Express Line: boarded passenger with SSN "123456-1222"
Express Line: boarded passenger with SSN "112233-2223"
    Passenger with SSN "123456-1222" is heading to "The University"
    Passenger with SSN "112233-2223" is heading to "The Village"
Express Line: carrying 2 passengers: heading for next stop
Express Line: arriving at "The University"
Express Line: unboarded passenger with SSN "123456-1222"
    Passenger with SSN "112233-2223" is heading to "The Village"
Express Line: reached the end of the line, everybody out
Express Line: unboarded passenger with SSN "112233-2223"
Simulation done

Code snippet 22

The Strategy Pattern

It’s time to charge Passengers for riding our Busses!

Charging Passengers is non-optional - there are no free bus rides! Functionality for charging Passengers is therefore part of the boarding algorithm, but the decision of which amount of money to charge each Passenger is best left to the BusCompany to decide.

To avoid burdening each Bus with the decisions surrounding ticket prices, we’ll use the Strategy Pattern. Let’s take a look at the boarding algorithm, which is currently very simple.

bus.Add(p.ToPassenger())

Code snippet 23

Since we’re no longer just adding a Passenger to a Bus, let’s change the name of bus.Add to Bus.Board and implement it together with a method for charging Passengers ticket prices. Observe.

// Board adds the given Passenger to the Bus and charges them a ticket price
// calculated by chargeFn if they don't already have a paid ticket.
// Board returns false if the Passenger was not allowed to board the Bus.
func (b *Bus) Board(p *Passenger, chargeFn PriceCalculator) bool {
	var allowed bool // Default value is false
	if p.HasValidTicket {
		allowed = true
	} else {
		amount := chargeFn(*p)
		allowed = p.Charge(amount)
	}
	if allowed {
		b.add(p)
	}
	return allowed
}

// Charge prints a message that the Passenger has been charged "amount" money,
// and returns a copy with validTicket = true.
func (p Passenger) Charge(amount float64) {
	if p.HasValidTicket{
		return p // We already charged this Passenger.
	}
	fmt.Printf("Passenger with SSN %s: charged %.2f of arbitrary money\n",p.SSN,amount)
	p.HasValidTicket=true
	return p}

// PriceCalculator is used by BusCompany to determine the ticket price for a Passenger.
// PriceCalculator returns the ticket price in the local currency.
type PriceCalculator func(p Passenger) float64

Code snippet 24

Note how we don’t pass chargeFn directly to Passenger: it’s not Passenger’s responsibility to know any details about how a BusCompany handles ticket pricing, it’s sufficient that they know how to be charged a certain amount of money. But at this point, another question presents itself: who is responsible for passing chargeFn to Board?

BusCompany might offer these two policies.

// WorkdayPricing charges EUR 6 for regular Passengers
// and EUR 4.5 for seniors during workdays.
func WorkdayPricing(p Passenger) float64 {
	if p.IsSenior() {
		return 4.5
	}
	return 6.0
}

// WeekendPricing charges EUR 5 for regular Passengers
// and EUR 3.5 for seniors during weekends.
func WeekendPricing(p Passenger) float64 {
	if p.IsSenior() {
		return 3.5
	}
	return 5.0
}

Code snippet 25

It’s then up to the BusCompany to supply each Bus with the correct policy for the day.

// BusCompany represents the bus company responsible for the Bus service.
// BusCompany determines price policies.
type BusCompany string

// GetPricing returns a price calculator based on the pricing policy of the day.
func (b BusCompany) GetPricing() PriceCalculator {
	wd := time.Now().Weekday()
	if wd == time.Saturday || wd == time.Sunday {
		return WeekendPricing
	}
	return WorkdayPricing
}

Code snippet 26

Now that we have a BusCompany that can supply prices - and two pricing strategies - we can refactor Bus.NotifyBusArrival to use the new Board method.

// NotifyBusArrival is called by Bus upon arrival.
func (b *BusStop) NotifyBusArrival(bus *Bus) {
	for _, p := range b.prospects {
		if bus.StopsAt(p.Destination) {
			pas := p.ToPassenger()
			bus.Board(pas, bus.Company.GetPricing()(pas))
		}
	}
}

Code snippet 27

To make this work, we just need to know if the Passenger is senior or not.

// SeniorAge is the minimum age from which a Passenger is considered
// a senior to the BusCompany.
const SeniorAge = 65

// IsSenior returns true if the Passenger is a senior, and false otherwise.
// IsSenior detects age by extracting the last two digits from the SSN and
// treating them like an age.
func (p Passenger) IsSenior() bool {
	age,err:=strconv.ParseInt(p.SSN[len(p.SSN)-2:],10,8)
	if err != nil {
		panic("invalid SSN: " + p.SSN)
	}
	return age >= SeniorAge
}

Code snippet 28

We might have tied SeniorAge to BusCompany so it can vary with each company’s policy, but let’s just say that it’s a national law and therefore equal for all. In that case, using a const is fine. Also, just to keep it simple, we’ll use the last two digits of the SSN as the age and just panic if the age extraction fails. In a real program, we would propagate the error or check for valid SSN’s at the package boundaries so we don’t have to check it each and every time.

Going back to the main function for a second, we’ll immediately notice that these changes have made Betty a senior.

john := busservice.Prospect{
	SSN:         "12345612-22",
	Destination: &s2,
}
betty := busservice.Prospect{
	SSN:         "11223322-67",
	Destination: &s3,
}

Code snippet 29

We can now run the program and verify that Betty is indeed charged a lower ticket price for seniors (in this case the weekend price). Output below.

Starting simulation
Express Line: starting
Express Line: arriving at "Downtown"
Passenger with SSN 12345612-22: charged 5.00 of arbitrary money
Express Line: boarded passenger with SSN "12345612-22"
Passenger with SSN 11223322-67: charged 3.50 of arbitrary money
Express Line: boarded passenger with SSN "11223322-67"
    Passenger with SSN "12345612-22" is heading to "The University"
    Passenger with SSN "11223322-67" is heading to "The Village"
Express Line: carrying 2 passengers: heading for next stop
Express Line: arriving at "The University"
Express Line: unboarded passenger with SSN "12345612-22"
    Passenger with SSN "11223322-67" is heading to "The Village"
Express Line: reached the end of the line, everybody out
Express Line: unboarded passenger with SSN "11223322-67"
Simulation done

Code snippet 30

The Strategy Pattern allowed us to keep Bus focused on charging the Passenger for the ride, rather than the technicalities of the implementation: whether or not it’s a weekday and to what degree BusCompany decides to let the Passenger’s characteristics affect the pricing. The method Bus.Board receives a simple PriceCalculator function and leaves it up to the BusCompany to analyze the situation and provide the correct pricing policy.

Conclusion

Over the course of this bus tour, we’ve applied the Visitor Pattern to avoid exposing an internal data structure (Bus.Passengers) to other packages. We then went on to abstract the same data structure using the Facade Pattern, which allows us to write convenience methods that operate easily on a collection of passengers, which we demonstrated by implementing a solution for getting a passenger manifest.

Next, we started visiting different bus stops while boarding and unboarding passengers, which we achieved by implementing an event-driven notification system using the Observer Pattern. This allows us to keep responsibilities where they belong as opposed to having a know-it-all algorithm that ties into everything else.

After having verified that our algorithm works, we then applied the Strategy Pattern in order to charge passengers different ticket prices without creating a dependency on a specific implementation, and while keeping the responsibilities of the calculation details out of the details of how a bus operates.

Finally, we ran the finished simulation and took a couple of passengers around the city.

I hope you enjoyed reading, and maybe even learned something. The entire tour code is available in my GitHub repository.

References

For more information about each topic, search! But here’s some links to get you started.