Another interesting usage for the factory method pattern is to link parallel inheritance hierarchies. Before getting into this let's examine what parallel inheritance hierarchies are (for a general discussion of the pattern itself please read here):
During design we sometimes have to create inheritance hierarchies which are very similar and pretty much follow the same structure but are designed for different concepts. For example consider the following diagram:
The above diagram might be a simple and effective way of explaining the power of inheritance and polymorphism to someone who is starting to learn object oriented concepts but it's definitely poor design in a real system. Let me elaborate: One major concern for the above design is the fact that cohesion (and more specifically technology cohesion) has been ignored. We have implementation code packaged inside a class (i.e. the Circle or Rectangle) class, that although related from a domain perspective (all circle functions in one place and all rectangle functions in another class), are absolutely unrelated from a technology perspective. The technology and techniques needed to save an object to a database (the SaveToDB method) and what is required to display an object on a windows form using the Graphics class (the Draw method) are completely irrelevant to each other. So it looks like we have a design at the logical cohesion level (when unrelated code is implemented in one place due to an arbitrary reason, in this case the arbitrary reason is that they are doing work on the same problem domain concept (i.e. Circle) ).
Although the above design might be considered good from a "domain cohesion" perspective where the designer is only concerned about cohesion from a problem domain perspective, it is an absolute nightmare from a "technology cohesion" perspective. Unfortunately most designers only think in terms of "domain cohesion" and don't bother with "technology cohesion" and this causes many problems down the road. For example once they want to distribute their application on multiple servers or create other similar changes they are stuck with assemblies that don't work in other environments. Consider the fact that the above design basically means that we have got to have database related libraries, windows graphics related libraries, and any other technology that we are using in the above code all as dependencies of our assembly. And if we want to send the above objects (serialized) to some other place where these libraries aren't available we are going to have a problem. It also means that once someone involved in the maintenance of the code needs to change something related to lets say the Draw method they are basically involved in the code related to a lot of different technologies which they might be completely unfamiliar with causing problems during maintenance. And similar issues that arise when you write bad code, period! J
So how can we solve the above problem? One of the solutions that can be used to create technology cohesive design is to put the implementation that uses different technologies into separate but similar inheritance hierarchies. This is only one method to solve it, there are other methods that work a lot better in similar situations, but we are going to investigate this method because it's completely related to this posts discussion. By following this technique we have a set of core objects that contain the data and behaviour related to what the object's true nature is and then other code such as code to save it to a DB or draw it as output on a windows form will be in their own separate classes. So the above diagram becomes something like this:
To complete the above design each class in the secondary hierarchies is going to accept as input to it's constructor an object of the core type and will be working on that to do what ever it's expected to do. So for example the RectangleDrawer class will be something like this:
class RectangleDrawer : IDrawer
{
private Shape WorkOn;
public RectangleDrawer(Shape s)
{
WorkOn = s;
}
public void Draw()
{
//use the WorkOn object’s data to draw it.
}
}
In other cases you might decide to pass the WorkOn object to the Draw method, you have to make a decision based on other design factors.
So once we start designing this way the only big question is how do we relate these objects to each other? If you look at the original design drawing an array of Shape objects would have been very simple:
Shape[] shapes = //loaded somehow
foreach(Shape s in shapes)
s.Draw();
What happens in the new design? Here is where the Factory Method pattern can become useful. As mentioned one of the less known but useful uses of this pattern is to relate parallel hierarchies of inheritance. If we create abstract factory methods in the Shape class and hold each subclass accountable for the creation of the related class from the other hierarchy our problem is solved! For example the Shape & Rectangle core objects would look like this:
class Shape
{
public abstract IDrawer CreateDrawer();
public abstract IDBAccess CreateDBAccess();
}
class Rectangle : Shape
{
/* rectangle related code */
public override IDrawer CreateDrawer()
{
return new RectangleDrawer(this);
}
public override IDBAccess CreateDBAccess()
{
return new RectangleDBAccess(this);
}
}
So with the above code, creating similar functionality as the above foreach loop is very easy:
Shape[] shapes = //loaded somehow
foreach(Shape s in shapes)
s.CreateDrawer().Draw();
Now the above example is kind of excessive in its use of this pattern and in a similar situation (a CAD application or a Drawing app) you probably have other issues involved that would prevent such a design but it provides a good example of what parallel hierarchies of inheritance are and how to relate them.
As a final note I should mention that the above code doesn't fix the un-needed assembly reference issue mentioned as the problems with bad technology cohesion, since our core classes (Shape, Rectangle, etc.) will need to reference the assembly containing the Drawer (RectangleDrawer, …) classes & the DBAccess (RectangleDBAccess, …) classes and those assemblies would in turn access the technology related libraries. So in other words the above example has only fixed the problem from a code separation point of view (putting technology specific code in its own assembly), but it hasn't solved the dependency issue.
Solving the dependency issue can be achieved by using another pattern in between the core objects and the actual classes doing the technology related work which will eventually use reflection to load the technology specific assemblies that it needs on demand. A good example would be the prototype pattern so the core objects would only ask a general purpose prototype factory to create what they want. The prototype factory and the abstract interface (such as IDrawer & IDBAccess) would be in an independent assembly that the technology specific assemblies will use. In this solution the core objects are only dependent on some independent assemblies that can be taken anywhere and are not physically related to the technology specific assemblies. This diagram better shows the assemblies and their relations:
Obviously the technology specific assemblies must be present for the technology specific functions (such as Draw or Save) to work. But the good thing is that we would only have to deploy them on the hardware or location that they will actually be used. So for example the back end server doesn't need the Drawer assembly while the front end UI (client side) probably doesn't need the DBAccess assembly.
1 comment:
This helped me get a conceptual understanding about something that's not felt right in my code for some time now, thanks.
Post a Comment