In my first internship, one of the tasks I had to do was learned about Go as well as revised on design patterns. So I mixed them together by trying to come up with my own examples for the patterns and implement them in Go.
I picked out those that I thought were being used more often instead of doing it for every single patterns mentioned in the famous book about design patterns by the Gang of Four(GoF).
Table of Contents
Creational Patterns
Builder
Definition
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations. - GoF
In layman’s terms, when working with complex objects in OOP, we try to delegate the creation of objects to a different object called a Builder.
The Builder object is an independent object that create the final product we want through steps we designed it to.
Another way to understand is by imagining it as an assembly line where we provide a detail model of what we want and the machine will build each part and assemble them together. All of which is done behind the scenes and we only care about the model design we input and the final output product.
Implementation
The Builder Pattern comprises of 4 main components:
-
Product: This is essentially the data model of the final object the we want. It usually contains different “parts” and properties meaning it can be created from smaller objects. For example a computer is built from objects such as screen, keyboard, cpu, gpu. Even simpler, a “profile” is built from names, age, home address, phone number, which are basically strings and numbers.
-
Builder or Abstract Builder: This will be the interface for our builders
-
Concrete Builder: The actual implementation for how the parts are built is in the concrete builder. Different types of product will have a different builder. It is an object that can construct other objects. For example if the concrete builder can specify if a car object runs on gas or electricity, or which type of wheels, and assembles them together.
-
Director: This will provide an interface to call the builder. We will specify a builder for it, and it will tell the builder to do things accordingly.
Those are the theories however, in real applications that I saw, mostly there’s only a Product and a Builder class.
Example code written in Go: Full sourcecode
Here we will try to implement Builder Pattern to create “product” which are computer objects with different models(Mac, Windows) with data: “name”, “price”, and “description”.
type Product struct {
name Name
price Price
description Description
}
Name and Description are of type string, and price is an integer. Then we define an abstract builder.
type AbstractBuilder interface {
Build() Product
}
Next up are Concrete Builders that implements the AbstractBuilder interface.
type WindowsBuilder struct{}
func (compBuilder WindowsBuilder) Build() Product {
computer := Product{
name: "Windows",
price: 100,
description: "This model was built to run on Windows"}
return computer
}
type MacBuilder struct{}
func (macBuilder MacBuilder) Build() Product {
computer := Product{
name: "OS X",
price: 200,
description: "This model was built to run on OS X"}
return computer
}
And finally the Director that make use of builders.
type Director struct {
builder AbstractBuilder
}
//newBuilder : users provide the Director which builder they want to use
func (director *Director) newBuilder(concreteBuilder AbstractBuilder) {
director.builder = concreteBuilder
}
//makeComputer : an interface for users to build product they want
//without needing to care about how it is built
func (director *Director) makeComputer() Product {
return director.builder.Build()
}
Conclusion
The builder pattern is better used when the representations of different complex objects do not share a common interface. It provides a few notable advantages over other methods of creation:
- You have good control over the process of creation, specifying each step and receive the product only when it’s finished so something goes wrong along the way you can handle them.
- Encapsulation of construction: each ConcreteBuilder contains all the code to create and assemble a particular kind of product. Then different Directors can reuse it to build Product variants from the same set of parts.
- You can change the product final presentations by creating a new builder.
However there are a few disadvantages as well:
- You have to create many different ConcreteBuilder for various types of products. Access to particular parts of the product so it requires some modifications.
Factory Method
Definition
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. - GoF
In layman terms, we want to have our program create objects without knowing which type of object until runtime.
Let’s say we have a text editor with the function OpenDocument. And in our code we have some subclasses that implement ways to represent different types of documents. However, which type of document we need to use is not known until the user has chosen a file. This is a case where the factory method can be extremely helpful because you can avoid numerous duplications of IF-ElSE and SWITCH-CASE statements.
Implementation
Factory Method consists of 4 participants:
- Product: defines the interface of objects the factory method creates.
- ConcreteProduct: implements the Product interface.
- Creator: declares the factory method, which returns an object of type Product. Creator may also define a default implementation of the factory method that returns a default ConcreteProduct object.
- ConcreteCreator: overrides the factory method to return an instance of a ConcreteProduct.
First of are the Product interface and implementations of ConcreteProduct
type Document interface {
Show()
New() Document
}
type XMLDocument struct {
xmlData string
}
func (xmlDoc XMLDocument) Show() {
fmt.Println(xmlDoc.xmlData)
}
func (xmlDoc XMLDocument) New() Document {
newDoc := XMLDocument{xmlData: "New XML doc"}
return newDoc
}
type JSONDocument struct {
jsonData string
}
func (jsonDoc JSONDocument) Show() {
fmt.Println(jsonDoc.jsonData)
}
func (jsonDoc JSONDocument) New() Document {
newDoc := JSONDocument{jsonData: "New JSON doc"}
return newDoc
}
Then we create a Creator interface (IApplication) that declares the factory method and the ConcreteCreator(CoreApplication) that implements the factory method. Also, we must provide the factory with the models for creation.
type IApllication interface {
OpenFile(fileType string) Document
}
type CoreApplication struct {
DocFactory map[string]Document
}
func (coreApp CoreApplication) OpenFile(fileType string) Document {
return coreApp.DocFactory[fileType].New()
}
func (coreApp *CoreApplication) AddDocType(typeName string, doc Document) {
if coreApp.DocFactory != nil {
coreApp.DocFactory[typeName] = doc
} else {
coreApp.DocFactory = make(map[string]Document)
coreApp.DocFactory[typeName] = doc
}
}
And in main
func main() {
fmt.Println("Factory Method Demo")
var coreApp CoreApplication
coreApp.AddDocType("XML", XMLDocument{})
coreApp.AddDocType("JSON", JSONDocument{})
newDoc := coreApp.OpenFile("XML")
newDoc.Show()
newDoc = coreApp.OpenFile("JSON")
newDoc.Show()
}
Conclusion
This pattern is generally useful when objects don’t care about how it’s created or we don’t want to repeatedly hardcode object creation branches. Some biggest disadvantages of the pattern involve tight coupling between different layers of abstraction and overall longer code, more classes.
Structural Patterns
Adapter
Definition
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces. - GoF
When programming, incompatibility issues are prone to occur. There will be times when we need to reuse code of different interfaces. To do that we create an adapter subclass that provides us with the interface we need, but also able to process and reuse code from a different interface.
Implementation
Main components of Adapter Pattern:
- Target is the domain-specific interface that Client wants to use.
- Adapter adapts the interface Adaptee to the Target interface. It implements the Target interface in terms of the Adaptee.
- Adaptee defines an existing interface that needs adapting.
- Client collaborates with objects conforming to the Target interface.
Let say we have a Car interface that allows us to call Run(). We have a NormalCar that can run and a RaceCar that can run. However there exists a different kind of automobile we call FlyingCar that can drive on the ground or fly in the sky. Now we will try to create an adapter called AdvancedCar to make use of FlyingCar while maintaining the same interface as Car.
First are some definitions of Car, NormalCar and RaceCar:
type Car interface {
Run()
}
type NormalCar struct {
Name string
}
func (car NormalCar) Run() {
fmt.Println("Normal car is running")
}
type RaceCar struct {
Name string
}
func (car RaceCar) Run() {
fmt.Println("Race car is speeding")
}
Then a new type of automobile is introduced called FlyingCar:
type FlyingCar struct {
Name string
}
func (car FlyingCar) Drive() {
fmt.Println("Flying car is driving")
}
func (car FlyingCar) Fly() {
fmt.Println("Flying car is flying")
}
But since our client is using the interface for Car we need to adapt this FlyingCar through an adapter class called AdvancedCar:
type AdvancedCar struct {
flyCar FlyingCar
Name string
}
func (advCar AdvancedCar) Run() {
advCar.flyCar.Drive()
}
type Client struct {
car Car
}
We have done all the work behind the scenes, our client only has to call Run() on the car he chooses.
type Client struct {
car Car
}
func (client Client) makeCarRun() {
client.car.Run()
}
In main program:
func main() {
fmt.Println("Adapter Pattern Demo")
client := Client{car: NormalCar{Name: "Normal Car"}}
client.makeCarRun()
client.car = RaceCar{Name: "Race Car"}
client.makeCarRun()
client.car = AdvancedCar{Name: "Advanced Car"}
client.makeCarRun()
}
Conclusion
Adapter Pattern is used a lot in Android development for handling lists, list views and can be applied the same way to other platform development. It is useful when we need an existing unrelated class to help us with a task but want the interface to match our specific context. However, we must be careful when determining how much “adapting” it does or should it be a two-way adapter.
Decorator
Definition
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. - GoF
So basically we use this pattern when we want to add additional behaviors or states to objects without applying inheritance because it causes too much dependency and unnecessary coupling.
To achieve that we will create a Decorator, which forwards requests to its Component object. It may optionally perform additional operations before and after forwarding the request.
Implementation
Key parts of Decorator Pattern include:
- Component: defines the interface for objects that can have responsibilities added to them dynamically.
- ConcreteComponent: defines an object to which additional responsibilities can be attached.
- Decorator: maintains a reference to a Component object and defines an interface that conforms to Component’s interface.
- ConcreteDecorator: adds responsibilities to the component.
Here is an example of a Pizza class and 2 Decorator class that add behaviors or state to the pizza without needing inheritance.
We have an interface for Pizza type with Describe function that will print a string and default getter and setter.
type Pizza interface {
Describe(Pz Pizza)
SetName(name string)
GetName() string
}
Then two types of pizza that implements the Pizza interface: The original Italian pizza
type ItalianPizza struct {
Name string
}
func (pz *ItalianPizza) Describe(Pz Pizza) {
fmt.Printf("My pizza type: %s\n", pz.Name)
}
And a different kind of pizza only available in Vietnam:
type VietnamesePizza struct {
Name string
EggType string
}
func (pz *VietnamesePizza) Describe(Pz Pizza) {
fmt.Printf("Different pizza type: %s with a different member: %s\n",
pz.Name,
pz.EggType)
}
Then we create an interface Decorator:
type Decorator interface {
Decorate(func(pz Pizza)) func(pz Pizza)
}
In this example we will have 2 types of concrete decorator implements Decorator, one to extend a behavior:
type Decorator interface {
Decorate(func(pz Pizza)) func(pz Pizza)
}
type BehaviorDecorator struct{}
func (d BehaviorDecorator) Decorate(f func(pz Pizza)) func(pz Pizza) {
return func(pz Pizza) {
f(pz)
pz.SetName("Behavior Name")
fmt.Printf("I changed pizza's name to: %s\n", pz.GetName())
}
}
And another for an extend in member:
type StateDecorator struct {
hot string
}
func (stateDecorator StateDecorator) Decorate(f func(pz Pizza)) func(pz Pizza) {
return func(pz Pizza) {
f(pz)
fmt.Printf("Added hot string: %s to %s\n", stateDecorator.hot, pz.GetName())
}
}
By applying closure functions in Go, we can use these decorators as follows:
func main() {
fmt.Println("Decorator Pattern Demo")
pizza := ItalianPizza{Name: "Original Itatalian Name"}
newBehavior := BehaviorDecorator{}
newState := StateDecorator{hot: "so hot"}
decorator := newBehavior.Decorate(newState.Decorate(pizza.Describe))
decorator(&pizza)
vietPizza := VietnamesePizza{Name: "Banh trang nuong", EggType: "Trung cut"}
decorator = newState.Decorate(vietPizza.Describe)
decorator(&vietPizza)
}
Conclusion
Decorator Pattern provides more flexibility than static inheritance. In addition it helps avoid creating a high-level class that adds responsibilities. Decorators are easily created, customized and add on as needed. However, it also means we need to create many small similar decorator classes, which can be hard for someone else to understand from the start.
Behavioral Patterns
Observer
Definition
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. - GoF
In other words, Observer Pattern helps us handle events by having the abstract observer to notify dependending subjects of such events so those subjects could make changes accordingly. Most importantly this is achieved without having the subjects tightly coupled.
Implementation
Main structure of Observer Pattern:
- Subject: knows its observers. Any number of Observer objects may observe a subject. Also provides an interface for attaching and detaching Observer objects.
- Observer: defines an updating interface for objects that should be notified of changes in a subject.
- ConcreteSubject: stores state of interest to ConcreteObserver objects and sends a notification to its observers when its state changes.
- ConcreteObserver: maintains a reference to a ConcreteSubject object.Also stores state that should stay consistent with the subject’s and implements the Observer updating interface to keep its state consistent with the subject’s.
Below we will try to implement 2 observers, one will observe the string Name and the other observe the integer Price. Full sourcecode
First of is the Observer interface and our ConcreteObservers:
type Observer interface {
Update(*ConcreteSubject)
}
type NameObserver struct {
Name string
}
func (nameObs *NameObserver) Update(subject *ConcreteSubject) {
fmt.Printf("Updated Name from: %s to %s\n", nameObs.Name, subject.Name)
nameObs.Name = subject.Name
}
type PriceObserver struct {
Price int
}
func (priceObs *PriceObserver) Update(subject *ConcreteSubject) {
fmt.Printf("Updated Price from: %d to %d\n", priceObs.Price, subject.Price)
priceObs.Price = subject.Price
}
Then we define and implement the Subject and ConcreteSubject:
type Subject interface {
Notify()
Attach(Observer)
Detach(Observer)
}
// An event
type ConcreteSubject struct {
Name string
Price int
ObserversMap map[Observer]struct{}
}
func (subject *ConcreteSubject) makeChanges(name string, price int) {
subject.Name = name
subject.Price = price
}
func (subject *ConcreteSubject) Attach(obs Observer) {
subject.ObserversMap[obs] = struct{}{}
}
func (subject *ConcreteSubject) Detach(obs Observer) {
delete(subject.ObserversMap, obs)
}
func (subject ConcreteSubject) Notify() {
for obs := range subject.ObserversMap {
obs.Update(&subject)
}
}
As you can see the ConcreteSubject maintains a list of Observers and will notify all of them when needed. In our main program, these types can be used like so:
func main() {
fmt.Println("Observer Pattern Demo")
newEvent := ConcreteSubject{Name: "Orgin",
Price: 1,
ObserversMap: map[Observer]struct{}{}}
newNameObserver := NameObserver{Name: "Blank"}
newPriceObserver := PriceObserver{Price: 0}
newEvent.Attach(&newNameObserver)
newEvent.Attach(&newPriceObserver)
newEvent.Notify()
newEvent.makeChanges("new name", 100)
newEvent.Notify()
newEvent.makeChanges("another name", -1)
newEvent.Notify()
}
//OUTPUT:
//Observer Pattern Demo
//Updated Name from: Blank to Orgin
//Updated Price from: 0 to 1
//Updated Name from: Orgin to new name
//Updated Price from: 1 to 100
//Updated Name from: new name to another
//Updated Price from: 100 to -1
Conclusion
Despite the advantage of making code more reusable, Observer Pattern has a few problems such as: client might forget to call Notify, an observer might want to listen to many subjects at once or unproper deletion of subjects leaves nil references.
Strategy
Definition
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. - GoF
One thing can usually be done multiple ways, however having separate classes for each algorithm makes it harder to maintain. So we can have one object and use Strategy Pattern to specify the encapsulated algorithms at runtime depending on the context.So when we need to change the algorithms of a certain object at runtime based on some specific context, we can consider Strategy Pattern as a solution.
Implementation
These are the main components of Strategy Pattern:
- Strategy: declares an interface common to all supported algorithms. Context uses this interface to call the algorithm defined by a ConcreteStrategy.
- ConcreteStrategy: implements the algorithm using the Strategy interface.
- Context: is configured with a ConcreteStrategy object. Also maintains a reference to a Strategy object and may define an interface that lets Strategy access its data.
Let’s say we want an object to have a sort function. However we have different sorting algorithms based on some contexts. Full sourcecode
Assume we will have context with formatted array structure provided at run time and depending that we will execute the sort algorithm that can provide the fastest run time for that particular array
type Context struct {
strategy Strategy
}
func (context Context) Sort() {
context.strategy.DoSort()
}
For the strategy of each sort algorithm:
type Strategy interface {
DoSort()
}
type MergeSort struct{}
func (msort MergeSort) DoSort() {
fmt.Println("Merge sort algo is being used")
}
type BubbleSort struct{}
func (bsort BubbleSort) DoSort() {
fmt.Println("Bubble sort is being used")
}
type QuickSort struct{}
func (qsort QuickSort) DoSort() {
fmt.Println("Quick sort is being used")
}
Then we can run them like this:
func main() {
fmt.Println("Strategy Pattern Demo")
context := Context{strategy: QuickSort{}}
context.Sort()
context.strategy = MergeSort{}
context.Sort()
context.strategy = BubbleSort{}
context.Sort()
}
Conclusion
Strategy Pattern helps avoid conditional statements by delegating the task to the strategy. However, clients must be aware of these strategies to use them appropriately and it increases the number of objects which can be hard to maintain if not implemented properly. Strategy Pattern lets you change the guts of an object while Decorator pattern lets you change the skin.
Architectural Patterns
MVC
Definition
Model View Controller (MVC) is a software architecture pattern, commonly used to implement user interfaces: it is therefore a popular choice for architecting web apps. However it can also be applied for the whole software as well. MVC tries to separate the application into 3 components that are loosely coupled so it can be extended easily.
- Model: The model defines what data the app should contain. If the state of this data changes, then the model will usually notify the view (so the display can change as needed) and sometimes the controller (if different logic is needed to control the updated view).
- View: The view defines how the app’s data should be displayed. A view can simply be a text box where text can be displayed.
- Controller: The controller contains logic that updates the model and/or view in response to input from the users of the app. Implementation
Let’s create a simplified, minimalistic version of an MVC software. Sourcecode
First is the view:
type View interface {
Show()
SetContent(string)
}
type TextBox struct {
content string
}
func (txtBox TextBox) Show() {
fmt.Println("Text Box content: ", txtBox.content)
}
func (txtBox *TextBox) SetContent(content string) {
txtBox.content = content
}
Then a model to store and operate on data:
type Model interface {
Set(int)
Get() int
}
type Database struct {
data int
}
func (db *Database) Set(value int) {
db.data = value
}
func (db Database) Get() int {
return db.data
}
And last but not least the controller, usually in real web apps, the controller and the view will interact via some sort of event handling delegate.
type Controller interface {
Clicked(View, Model)
}
type TextController struct{}
func (txtController TextController) Clicked(view View, model Model) {
fmt.Println("A button was clicked")
model.Set(30)
value := model.Get()
content := strconv.Itoa(value)
view.SetContent(content)
view.Show()
}
In our main(), they will interact with each other like this:
webView := TextBox{}
dataModel := Database{}
defaultController := TextController{}
defaultController.Clicked(&webView, &dataModel)
Conclusion
The MVC pattern can be implemented differently depending on usage but at the core, it serves to speed up the development process as well as maintainability and scalability. Because the 3 components are separated and loosely coupled they can be developed simultaneously. For the same reason, a Model can be used by different sets of controllers. Controllers can control different views with the same interface making the code extremely flexible.
References
- http://tmrts.com/go-patterns/creational/builder.html
- Design Patterns Elements of Reusable Object-Oriented Software - GoF
- https://stackoverflow.com/questions/2832017/what-is-the-difference-between-loose-coupling-and-tight-coupling-in-the-object-o
- https://sourcemaking.com/design_patterns
- https://www.tutorialspoint.com/design_pattern/design_pattern_overview.htm