Introduction
The Abstract Factory Pattern is a way to group the creation of related objects, like products of a certain brand or make by building a common factory interface. If this all sounds abstract, a picture can help:
A short breakdown:
- All the factories have a common interface called AbstractFactory
- The concrete factories, in our diagram there are two, implement this interface
- These concrete factories produce objects with a common interface so those are interchangeable. In our example those are called AbstractProductOne and AbstractProductTwo
This is a somewhat more complicated design pattern, but when used well can lead to interchangeable concrete implementations. However, considering the relative complexity of this pattern, it will require extra initial work. Also, due to the high abstraction levels, it can lead in some cases to code that is more difficult to test, debug and maintain.
So, even though this pattern is very flexible and powerful, use it wisely and sparingly.
Implementation in Go
After you create the project, we will start the preliminaries:
import "fmt"
In our example we will define a Vehiclefactory interface, to encapsulate two factories, one who produces Volkswagen vehicles, and one who does the same for Renault. The vehicles can be either cars or bikes.
The cars have an AbstractCar interface, and likewise, the bikes have an AbstractBike interface.
We will start by defining the VehicleFactory interface itself:
type VehicleFactory interface {
createCar(color string) AbstractCar
createBike(numberOfWheels int16) AbstractBike
}
In it we see two different methods used to make the two different products i.e. cars and bikes
Next we define the interfaces for both the car and the bike. Both interfaces have a GetDescription() method to render a string representation of themselves:
type AbstractCar interface {
GetDescription() string
}
type AbstractBike interface {
GetDescription() string
}
Next we come to vehicles themselves, first the Volkswagen vehicle struct:
type VolkswagenCar struct {
make string
color string
}
This struct has two fields, both private. In Go this is done by starting the fieldname with a lower-case letter.
The GetDescription() is then quite straightforward:
func (c VolkswagenCar) GetDescription() string {
return fmt.Sprintf("Make: %s, color: %s", c.make, c.color)
}
This method just returns a string representation of the object.
The VolkswagenBike struct is fashioned in a similar manner:
type VolkswagenBike struct {
make string
numberOfWheels int16
}
func (b VolkswagenBike) GetDescription() string {
return fmt.Sprintf("Make: %s, numberOfWheels: %d", b.make, b.numberOfWheels)
}
Again, some private fields for the make and the number of wheels a bike has.
The fields in both structs are private because in our setup, once the structs are constructed, there is no need to change the inner state of the struct. Mind you, that is for this particular setup.
Not surprisingly, the RenaultCar and the RenaultBike have similar code:
type RenaultCar struct {
make string
color string
}
func (r RenaultCar) GetDescription() string {
return fmt.Sprintf("Make: %s, color: %s", r.make, r.color)
}
type RenaultBike struct {
make string
numberOfWheels int16
}
func (b RenaultBike) GetDescription() string {
return fmt.Sprintf("Make: %s, numberOfWheels: %d", b.make, b.numberOfWheels)
}
The concrete factories
Now we come to the interesting bit, the first concrete factory:
type VolkswagenFactory struct{}
func (f VolkswagenFactory) createCar(color string) AbstractCar {
return VolkswagenCar{color: color, make: "Volkswagen"}
}
func (f VolkswagenFactory) createBike(numberOfWheels int16) AbstractBike {
return VolkswagenBike{numberOfWheels: numberOfWheels, make: "Volkswagen"}
}
As you can see we have two factory methods, both returning an interface type, so for the client, the underlying object and the underlying implementation are completely transparent. In both methods, for our particular case, we simply initialize the struct and return it.
The RenaultFactory is very similar:
type RenaultFactory struct{}
func (r RenaultFactory) createCar(color string) AbstractCar {
return RenaultCar{color: color, make: "Renault"}
}
func (r RenaultFactory) createBike(numberOfWheels int16) AbstractBike {
return RenaultBike{numberOfWheels: numberOfWheels, make: "Renault"}
}
Testing time
Now it is time to test our ideas:
func main() {
var factory VehicleFactory
factory = RenaultFactory{}
car := factory.createCar("red")
bike := factory.createBike(3)
fmt.Println(car.GetDescription())
fmt.Println(bike.GetDescription())
}
Line by line:
- We declare a factory variable of type Vehiclefactory. Remember that VehicleFactory is an interface class
- Next we instantiate a RenaultFactory and assign that to our factory variable. Since RenaultFactory implements the VehicleFactory interface, we can do that.
- Next we create a car and a bike, and print out their respective descriptions. You will see that both them of have the mark ‘Renault’
- Now change RenaultFactory to VolkswagenFactory and you will get different results, without changing the rest of the code.
Conclusion
As you can see, setting up the factory is quite some work even in a simple case like this one. However you gain a lot of flexibility and ease of use.
Mind you, as I said in the introduction, because of the higher level of abstraction, using this pattern can lead to extra initial work, and in some case, code that is harder to debug and maintain.
When done well, this pattern offers flexibility, even at runtime, and maintainability.