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.
- For more on the Visitor Pattern
- For more on the Facade Pattern
- For more on the Observer Pattern
- For more on the Strategy Pattern
- For examples (with code) of more design patterns in Go, Golang By Example is a great site
- Packt Publishing has a nice little repository that shows various design patterns in Go
- For information on the SOLID principles that provide much of the underlying philosophy behind many design patterns
- The excellent standard library is documented here
- For more inspiration, see my own article on The Flyweight Pattern in Go