After learning about the factory design pattern, where we grouped a family of related objects in our case payment methods, one can be quick to think--what if I group families of objects in a more structured hierarchy of families?
The Abstract Factory design pattern is a new layer of grouping to achieve a bigger (and more complex) composite object, which is used through its interfaces. The idea behind grouping objects in families and grouping families is to have big factories that can be interchangeable and can grow more easily. In the early stages of development, it is also easier to work with factories and abstract factories than to wait until all concrete implementations are done to start your code. Also, you won't write an Abstract Factory from the beginning unless you know that your object's inventory for a particular field is going to be very large and it could be easily grouped into families.
Grouping related families of objects is very convenient when your object number is growing so much that creating a unique point to get them all seems the only way to gain the flexibility of the runtime object creation. The following objectives of the Abstract Factory method must be clear to you:
For our example, we are going to reuse the Factory we created in the Builder design pattern. We want to show the similarities to solve the same problem using a different approach so that you can see the strengths and weaknesses of each approach. This is going to show you the power of implicit interfaces in Go, as we won't have to touch almost anything. Finally, we are going to create a new Factory to create shipment orders.
The following are the acceptance criteria for using the Vehicle
object's Factory method:
Vehicle
object using a factory returned by the abstract factory.Motorbike
or a Car
that implements both interfaces (Vehicle
and Car
or Vehicle
and Motorbike
).This is going to be a long example, so pay attention, please. We will have the following entities:
VehicleFactory
method:VehicleFactory
interface to return vehicle that implements the Vehicle
and Motorbike
interfaces.VehicleFactory
interface to return vehicles that implement the Vehicle
and Car
interfaces.
For clarity, we are going to separate each entity into a different file. We will start with the Vehicle
interface, which will be in the vehicle.go
file:
package abstract_factory type Vehicle interface { NumWheels() int NumSeats() int }
The Car
and Motorbike
interfaces will be in the car.go
and motorbike.go
files, respectively:
// Package abstract_factory file: car.go package abstract_factory type Car interface { NumDoors() int } // Package abstract_factory file: motorbike.go package abstract_factory type Motorbike interface { GetMotorbikeType() int }
We have one last interface, the one that each factory must implement. This will be in the vehicle_factory.go
file:
package abstract_factory type VehicleFactory interface { NewVehicle(v int) (Vehicle, error) }
So, now we are going to declare the car factory. It must implement the VehicleFactory
interface defined previously to return Vehicles
instances:
const ( LuxuryCarType = 1 FamilyCarType = 2 ) type CarFactory struct{} func (c *CarFactory) NewVehicle(v int) (Vehicle, error) { switch v { case LuxuryCarType: return new(LuxuryCar), nil case FamilyCarType: return new(FamilyCar), nil default: return nil, errors.New(fmt.Sprintf("Vehicle of type %d not recognized ", v)) } }
We have defined two types of cars--luxury and family. The car
Factory will have to return cars that implement the Car
and the Vehicle
interfaces, so we need two concrete implementations:
//luxury_car.go package abstract_factory type LuxuryCar struct{} func (*LuxuryCar) NumDoors() int { return 4 } func (*LuxuryCar) NumWheels() int { return 4 } func (*LuxuryCar) NumSeats() int { return 5 } package abstract_factory type FamilyCar struct{} func (*FamilyCar) NumDoors() int { return 5 } func (*FamilyCar) NumWheels() int { return 4 } func (*FamilyCar) NumSeats() int { return 5 }
That's all for cars. Now we need the motorbike factory, which, like the car factory, must implement the VehicleFactory
interface:
const ( SportMotorbikeType = 1 CruiseMotorbikeType = 2 ) type MotorbikeFactory struct{} func (m *MotorbikeFactory) Build(v int) (Vehicle, error) { switch v { case SportMotorbikeType: return new(SportMotorbike), nil case CruiseMotorbikeType: return new(CruiseMotorbike), nil default: return nil, errors.New(fmt.Sprintf("Vehicle of type %d not recognized ", v)) } }
For the motorbike Factory, we have also defined two types of motorbikes using the const
keywords: SportMotorbikeType
and CruiseMotorbikeType
. We will switch over the v
argument in the Build
method to know which type shall be returned. Let's write the two concrete motorbikes:
//sport_motorbike.go package abstract_factory type SportMotorbike struct{} func (s *SportMotorbike) NumWheels() int { return 2 } func (s *SportMotorbike) NumSeats() int { return 1 } func (s *SportMotorbike) GetMotorbikeType() int { return SportMotorbikeType } //cruise_motorbike.go package abstract_factory type CruiseMotorbike struct{} func (c *CruiseMotorbike) NumWheels() int { return 2 } func (c *CruiseMotorbike) NumSeats() int { return 2 } func (c *CruiseMotorbike) GetMotorbikeType() int { return CruiseMotorbikeType }
To finish, we need the abstract factory itself, which we will put in the previously created vehicle_factory.go
file:
package abstract_factory import ( "fmt" "errors" ) type VehicleFactory interface { Build(v int) (Vehicle, error) } const ( CarFactoryType = 1 MotorbikeFactoryType = 2 ) func BuildFactory(f int) (VehicleFactory, error) { switch f { default: return nil, errors.New(fmt.Sprintf("Factory with id %d not recognized ", f)) } }
We are going to write enough tests to make a reliable check as the scope of the book doesn't cover 100% of the statements. It will be a good exercise for the reader to finish these tests. First, a motorbike
Factory test:
package abstract_factory import "testing" func TestMotorbikeFactory(t *testing.T) { motorbikeF, err := BuildFactory(MotorbikeFactoryType) if err != nil { t.Fatal(err) } motorbikeVehicle, err := motorbikeF.Build(SportMotorbikeType) if err != nil { t.Fatal(err) } t.Logf("Motorbike vehicle has %d wheels ", motorbikeVehicle.NumWheels()) sportBike, ok := motorbikeVehicle.(Motorbike) if !ok { t.Fatal("Struct assertion has failed") } t.Logf("Sport motorbike has type %d ", sportBike.GetMotorbikeType()) }
We use the package method, BuildFactory
, to retrieve a motorbike Factory (passing the MotorbikeFactory
ID in the parameters), and check if we get any error. Then, already with the motorbike factory, we ask for a vehicle of the type SportMotorbikeType
and check for errors again. With the returned vehicle, we can ask for methods of the vehicle interface (NumWheels
and NumSeats
). We know that it is a motorbike, but we cannot ask for the type of motorbike without using the type assertion. We use the type assertion on the vehicle to retrieve the motorbike that the motorbikeVehicle
represents in the code line sportBike, found := motorbikeVehicle.(Motorbike)
, and we must check that the type we have received is correct.
Finally, now we have a motorbike instance, we can ask for the bike type by using the GetMotorbikeType
method. Now we are going to write a test that checks the car factory in the same manner:
func TestCarFactory(t *testing.T) { carF, err := BuildFactory(CarFactoryType) if err != nil { t.Fatal(err) } carVehicle, err := carF.Build(LuxuryCarType) if err != nil { t.Fatal(err) } t.Logf("Car vehicle has %d seats ", carVehicle.NumWheels()) luxuryCar, ok := carVehicle.(Car) if !ok { t.Fatal("Struct assertion has failed") } t.Logf("Luxury car has %d doors. ", luxuryCar.NumDoors()) }
Again, we use the BuildFactory
method to retrieve a Car
Factory by using the CarFactoryType
in the parameters. With this factory, we want a car of the Luxury
type so that it returns a vehicle
instance. We again do the type assertion to point to a car instance so that we can ask for the number of doors using the NumDoors
method.
Let's run the unit tests:
go test -v -run=Factory . === RUN TestMotorbikeFactory --- FAIL: TestMotorbikeFactory (0.00s) vehicle_factory_test.go:8: Factory with id 2 not recognized === RUN TestCarFactory --- FAIL: TestCarFactory (0.00s) vehicle_factory_test.go:28: Factory with id 1 not recognized FAIL exit status 1 FAIL
Done. It can't recognize any factory as their implementation is still not done.
The implementation of every factory is already done for the sake of brevity. They are very similar to the Factory method with the only difference being that in the Factory method, we don't use an instance of the Factory method because we use the package functions directly. The implementation of the vehicle
Factory is as follows:
func BuildFactory(f int) (VehicleFactory, error) { switch f { case CarFactoryType: return new(CarFactory), nil case MotorbikeFactoryType: return new(MotorbikeFactory), nil default: return nil, errors.New(fmt.Sprintf("Factory with id %d not recognized ", f)) } }
Like in any factory, we switched between the factory possibilities to return the one that was demanded. As we have already implemented all concrete vehicles, the tests must run too:
go test -v -run=Factory -cover . === RUN TestMotorbikeFactory --- PASS: TestMotorbikeFactory (0.00s) vehicle_factory_test.go:16: Motorbike vehicle has 2 wheels vehicle_factory_test.go:22: Sport motorbike has type 1 === RUN TestCarFactory --- PASS: TestCarFactory (0.00s) vehicle_factory_test.go:36: Car vehicle has 4 seats vehicle_factory_test.go:42: Luxury car has 4 doors. PASS coverage: 45.8% of statements ok
All of them passed. Take a close look and note that we have used the -cover
flag when running the tests to return a coverage percentage of the package: 45.8%. What this tells us is that 45.8% of the lines are covered by the tests we have written, but 54.2% are still not under the tests. This is because we haven't covered the cruise motorbike and the family car with the tests. If you write those tests, the result should rise to around 70.8%.
Type assertion is also known as casting in other languages. When you have an interface instance, which is essentially a pointer to a struct, you just have access to the interface methods. With type assertion, you can tell the compiler the type of the pointed struct, so you can access the entire struct fields and methods.
We have learned how to write a factory of factories that provides us with a very generic object of vehicle type. This pattern is commonly used in many applications and libraries, such as cross-platform GUI libraries. Think of a button, a generic object, and button factory that provides you with a factory for Microsoft Windows buttons while you have another factory for Mac OS X buttons. You don't want to deal with the implementation details of each platform, but you just want to implement the actions for some specific behavior raised by a button.
Also, we have seen the differences when approaching the same problem with two different solutions--the Abstract factory and the Builder pattern. As you have seen, with the Builder pattern, we had an unstructured list of objects (cars with motorbikes in the same factory). Also, we encouraged reusing the building algorithm in the Builder pattern. In the Abstract factory, we have a very structured list of vehicles (the factory for motorbikes and a factory for cars). We also didn't mix the creation of cars with motorbikes, providing more flexibility in the creation process. The Abstract factory and Builder patterns can both resolve the same problem, but your particular needs will help you find the slight differences that should lead you to take one solution or the other.
3.138.179.100