Abstract Factory - a factory of factories

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?

Description

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.

The objectives

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:

  • Provide a new layer of encapsulation for Factory methods that return a common interface for all factories
  • Group common factories into a super Factory (also called a factory of factories)

The vehicle factory example, again?

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.

Acceptance criteria

The following are the acceptance criteria for using the Vehicle object's Factory method:

  • We must retrieve a Vehicle object using a factory returned by the abstract factory.
  • The vehicle must be a concrete implementation of a Motorbike or a Car that implements both interfaces (Vehicle and Car or Vehicle and Motorbike).

Unit test

This is going to be a long example, so pay attention, please. We will have the following entities:

  • Vehicle: The interface that all objects in our factories must implement:
    • Motorbike: An interface for motorbikes of the types sport (one seat) and cruise (two seats).
    • Car: An interface for cars of types luxury (with four doors) and family (with five doors).

  • VehicleFactory: An interface (the Abstract Factory) to retrieve factories that implement the VehicleFactory method:
    • Motorbike Factory: A factory that implements the VehicleFactory interface to return vehicle that implements the Vehicle and Motorbike interfaces.
    • Car Factory: Another factory that implements the 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.

Implementation

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%.

Tip

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.

A few lines about the Abstract Factory method

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.199.184