Let me remind you that my purpose in writing these articles is to try to broaden my understanding, both by having to make the effort to express myself and from the resulting corrections from readers. I am doing my part, are you doing yours?
I had just finished a thoughtful reread of the section of the GoF's Design Patterns on the Abstract Factory pattern (well actually I started by reading the Bridge pattern but that referred to Abstract Factory which led me to study that) when I had to go to cook the supper. It doesn't take much intellectual effort to slice carrots so I continued to think about what I had read and how I might present it for your edification.
I thought about my 'motherboard' example but as I thought round the problem it became clear that it was rather too complicated for what I wanted. Next I thought about the Christmas presents my wife had received. Among those were some electrical tools. Those seemed a little too simple (drill bits are reasonably portable between different manufacturers.)
Finally my eyes fell on the food-mixer and I realised that this was just what I wanted.
Modelling a Food-mixer
Each manufacturer has their own specific food-mixer tool with an entire range of add-ons. It is no good buying a bean slicer from one manufacturer and trying to plug it into another manufacturer's base unit. In addition there are free-standing bean slicers.
Let us focus on bean slicers for a moment. Clearly we have a perfectly good abstraction. You feed beans in and sliced beans come out - providing you have supplied an appropriate power supply. The bean slicer will need cleaning, resharpening etc. If you want to slice some beans you probably do not care what slicer is used. In general you will not line up half a dozen different slicers and then choose one; you will use the one you have got. In other words you will have an instance of some concrete class derived from the abstraction 'BeanSlicer'.
Some of those concrete classes will be add-on tools for a food-mixer. FoodMixer will be another abstraction. In your kitchen you may have an instance of a concrete type derived from the abstraction 'BeanSlicer' that has nothing to do with your food-mixer. In other words the concept of a bean slicer is completely independent of the concept of a food-mixer.
Consider the following conversation:
- Customer:
-
I would like to buy a bean slicer.
- Sales Person:
-
Do you have any particular one in mind?
- C:
-
One that will attach to my food mixer.
- SP:
-
What kind of food-mixer do you have?
- C:
-
One of those made by XYZ.
- SP:
-
Do you know which model?
etc.
The purpose of the dialogue is to establish exactly which type of bean slicer the customer wants. At the conclusion the customer will go away with a bean slicer that was made for his/her food-mixer.
How can we represent these ideas in software?
Start with Abstractions
First we will need to create abstract base classes to represent all the different types of kitchen tool that might be attached to our abstract concept of a food-mixer (itself a kitchen tool). It will be useful to be as complete as possible because that will ensure that our code will be stable and not repeatedly force recompilation of application code. Typically we might write:
class KitchenTool { public: KitchenTool(){}; // this is a base class designed for // derivation virtual ~KitchenTool()throw(){}; // can make a new one virtual KitchenTool & void clone() = 0; void destroy(){delete this;} virtual KitchenTool & clean() = 0; virtual KitchenTool & use() = 0; virtual KitchenTool & store() = 0; protected: // Must not be publicly copied // (though the clone function KitchenTool(KitchenTool const &){}throw(); private: // cannot be assigned, so declare the // following but do not implement virtual KitchenTool & operator= (KitchenTool const &); };
Note that I have to define a default constructor because I need to constrain copy construction by making it protected (though, in the absence of data, there is nothing to do). The latter is necessary because the concept of a KitchenTool is clearly not something that should be passed by value. The existence of the clone function results in a need to have a way of destroying the resulting dynamic instance.
I think it is clear nonsense to allow default assignment and so have chosen to make it a non-implemented private function.
I thought about adding a few other member functions but then decided that I should encapsulate such things as attach() and detach() in the more general abstractions of use() and store() . In practice I would probably want to do quite a bit more.
class Whisk : public KitchenTool { public: Whisk(){}; // this is a base class designed for // derivation ~Whisk()throw(){}; // can make a new one Whisk & void clone() = 0; void destroy(){delete this;} Whisk & clean() = 0; Whisk & use() = 0; Whisk & store() = 0; protected: // Must not be publicly copied // (though the clone function Whisk(Whisk const &){}throw(); private: // cannot be assigned, so declare the // following but do not implement virtual Whisk & operator= (Whisk const &); };
You may be surprised by the return types on those overriders for the virtual functions from the base. However C++ supports covariant return types exactly to support this kind of thing. Whether your compiler knows that is another problem entirely.
Note that Whisk is an abstract base class derived from another abstract base class. You might want to add some specifically Whisk behaviour at this stage.
Next let us distinguish between a free standing Whisk and one that can be attached (to a food-mixer). The concept of being attachable seems to be a mixin type abstraction, so I might add:
class Attachable{ public: struct Failed {}; virtual attach(KitchenTool &)throw (Failed) = 0; virtual detach(KitchenTool &)throw (Failed) = 0; protected: ~Attachable(){} };
You may be puzzled by that protected destructor and think I should be providing a public virtual one instead. However anytime you see a public virtual destructor you should expect to be able to destroy the object through a pointer of the class type. That is usually a mistake for a mixin (pure interface) type. Types that inherit from this mixin will be able to call the destructor, but we will not be able to publicly destroy these objects through a pointer to their interface. This is another place where you should re-examine transmitted wisdom even if you eventually decide to go along with it.
There is an issue that some of you might like to think about, and then write in about. Should Attachable 's member functions be provided like that? The process of attachment involves two objects. Perhaps the functions should be at namespace scope rather than class scope so that the symmetry of the operation can be catered for. ( Attach and detach are rather like adding and subtracting - however I would resist any temptation to provide overloaded operators.)
Now we can write:
class AttachableWhisk: public Whisk, public Attachable { // what, if anything, do you think goes here? }
And now I can provide some concrete classes derived from AttachableWhisk.
I can follow up with similar sub-hierarchies for beaters, pastry hooks, etc.
Among the entire range of kitchen tools one will be the food-mixer. This is also provided as an abstraction.
At this stage I might sit down and implement a complete set of tools from some manufacturer.
The Wedding List - The Need for an Abstract Factory
Imagine that you are about to get married and that you and your chosen life partner are sitting down preparing one of those now traditional list of potential wedding presents. Under the heading of Kitchen Gadgets you list all the things you would like to have in your kitchen. At some stage you will need to decide which manufacturer's product line you are going to ask for. In one way it does not matter as long as you are consistent in so far as a base unit (food-mixer) and attachments are concerned.
You can decide what gadgets you want and which manufacturer as almost independent decisions.
The same should apply in your software. Having decided on the supplier you should not have to track that decision for each purchase.
So what we write is an abstract factory class that basically consists of a large number of abstract factory functions. Something like:
class MakeKitchenTools { public: virtual FoodMixer * makeFoodMixer() = 0; virtual Whisk * makeWhisk() = 0; // rest of tools virtual ~MakeKitchenTools(){}; }; and then provide suitable concrete instances: class ABCfactory: public MakeKitchenTools { public: ABCfactory(){} FoodMixer * makeFoodMixer() { return new ABCFoodMixer();} Whisk * makeWhisk() { return new ABCWhisk;} // rest of tools ~ABCfactory(){} // default ctor, copy ctor and assignment OK };
Note that ABCFoodMixer etc will be concrete classes derived from an appropriate base.
Now I can write code such as:
int main() { MakeKitchenTools const & toolmaker = new ABCfactory; Whisk * whisk = toolmaker.makeWhisk(); BeanSlicer * beanSlicer = toolmaker.makeBeanSlicer(); // other code whisk->destroy(); beanSlicer->destroy(); };
There are many refinements and improvements that could be made to the above code (I have even deliberately left in a few poor choices to encourage you to read critically) and I hope that several of you will feel able to contribute your ideas and insights. However the fundamental idea is centred on that abstract factory class that can be used as a base for specific factory classes. Your program can then use instances of these to make instances of a range of items that must work consistently together.
If possible, decisions should only be recorded once in a program. That means a change will promulgate through the whole program. We should avoid unnecessary coupling between items. For example we should avoid coupling a whisk and a food-mixer. However we do need methods to ensure compatibility between objects. One of the main tasks of an abstract factory is to provide exactly that.
Or did I miss the point? ☺