![]() | Monkeys at Keyboards: Java-Fu © Michael James Heron | ||||
| Topic: Java Programming Level: 3 Version: beta | |||||
10 - Design Patterns | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to: |
The field of design patterns has grown out of an innate recognition that it is not knowledge of syntax or language minutiae that makes a good programmer - it is the ability to see and interpret patterns. Secondary to this is the ability to develop solid data structures and develop effective routines for manipulating such structures. Upon these two building blocks are the stones of software development lain. The ability to recognise patterns comes primarily from experience - it's an unwelcome truth, considering how hard people have fought to get to this stage in their development. This experience is hard-won, and involves spending a lot of time coding and applying development skill to a large range of different projects. This is important - only practise will make you Perfect. However, there are some shortcuts. These shortcuts are not a replacement for experience, but they make the learning process easier. The field of design patterns exists to codify those elements that contribute to successful software projects, and extract the central elements of their design. Design patterns represent a 'good' approach to software development, and their widespread adoption is a testament to their utility in the creation of complex software entities.
Most engineering disciplines have a handbook of some sort that describes successful solutions to known problems. Engineers don't build bridges using only the laws of physics as their guide - instead, they usually adapt from a range of previously successful models. Such models have a proven track record, and although they may not be 100% moulded to the requirements of the engineer, they represent a solid platform from which to modify. Software engineers have a similar 'handbook', and it is a library of 'design patterns'. These are solutions to known programming issues, written up as a generic framework that can be used within almost any software project. The handbook is far from complete - the field of design patterns is undergoing much research and productivity - but the basis is there and we can benefit from this handbook by understanding the key themes of design patterns and applying them to our own programming projects. Components must be designed properly, or they will not work - a mistake in a single custom software product can be costly, but it is a localised problem. If there is a mistake in a ubiquitous software component, the ramifications are much greater. There is an imperative that developers of components make the effort to get it right from the start. Design patterns can aid in this, by providing a reliable and adaptable framework that has been shown to work in the past. Combine this with formal contracts enforced between software entities, and a robust software engineering approach, and you can develop components that can be relied upon. Design patterns are an articulation of a very particular development philosophy. The underlying concept is this: success is more important than novelty. It's very easy to dismiss design patterns as something that 'real 'developers wouldn't use, but this is an incorrect argument. Real developers in the real world are under pressures to produce results - people are more interested in getting the software they are paying for than acting as validation for your learning experience. Design patterns are usually stated according to a formal specification - one day in the future a genuine handbook of design patterns may be a standard part of the software engineer's library, and so those developing patterns are conscious of the fact there needs to be a standard of documentation that has a high degree of clarity built into its expression. Design patterns arise from practical experience - they are not (mostly) theoretical abstractions. A solution to a particular recurring problem is a good candidate for adoption as a design pattern if it implements a novel solution in a way that is configurable, adaptable and maintainable. A secondary emphasis is placed on performance, because of the traditional trade-offs between maintainability and performance. Design patterns are not simply codified - usually they go through a trial by fire during which developers will debate the various strengths and weaknesses of a particular pattern. There is a constant evolution of patterns into new and more adaptable shapes. Design patterns are not pinned to a page like butterflies - they are living designs that adapt and change to circumstance. It is important to realise that design patterns are not intended to replace developer creativity - they are just tools that are available in a developer's toolkit, but they are not a straightjacket that forces you into a particular kind of development. They are an aid to design, not a mandatory replacement for innovation. So, what is it that actually defines a pattern? There are a number of traits that all patterns possess:
The first of these principles is perhaps the most important - if the pattern isn't a solution to a problem, it is of very limited use. If it's a problem that isn't likely to recur (albeit in a range of variable ways), then there's likely very little reason for the solution to be written up as a pattern - there needs to be a degree of editorial control over what is used as a pattern... if everything is a pattern it becomes impossible to sift through the irrelevant for the useful. The second principle requires there to be an identifiable context - an area to which the pattern can be successfully applied. As indicated above, the context must be something that recurs over a range of projects, and it must be clearly described as to its key elements and how the pattern interacts with the problem to provide a solution. A pattern must be general enough so that it can be applied to a range of problems within its context, but it must also be specialised enough to provide the understanding necessary to use it successfully. There is a fine balance to be struck here. Finally, a pattern needs a consistent and unique name - if people mention a particular pattern, it shouldn't be the case that there are four or five different versions of the pattern. The identifier must uniquely identify the pattern. With this checklist of traits, we can begin to look into some of the pattern libraries available, and how they can be used to develop solutions to difficult problems.
Design patterns all stem from a common guiding principle, but they are often applied to markedly different situations of problems. For this reason, patterns are broken up into a number of different families which give an indicator of their intention. Traditionally, there are three main families:
Creational patterns are used to handle the creation of objects - they abstract the mechanism of this away from the developer, meaning that you never have to instantiate an object directly. This gives the developer a better choice in exactly what objects are generated at particular times - hard-coded instantiations limit the ability to express ambiguity when a range of different objects are required. Structural patterns aid in building objects or components into larger, integrated structures. They emphasise interaction between entities into a highly cohesive unit of expanded functionality. Behaviour patterns are concerned primarily with communication, and the flow of logic through a program. There are examples of each of these patterns that should be familiar to you through your previous experience with Java (within this module and other modules), and general programming. As has been previously indicated, they represent 'good' practise, and this is based on the fact they are tried and tested... they have wide-spread adoption that shows they can be relied upon as solutions to particular kinds of problems.
In this module we've already seen an example of one of these - the Factory design pattern. We used this to create our XML and DOM parsers, but we didn't spend much time looking at the way the system was set up. A creation patterns deals with how instances of objects can be created in a structured and maintainable way. You are already well familiar with how this is done in Java - with the new keyword, which has served us well in the past. We've been able to obtain a level of configurability with hard-coded instantiations through the use of overloaded constructor methods - we can setup an object according to the current state of our programs by passing parameters to the constructor (in the case of a standard object), or by making property calls on a Javabean component. Sometimes it isn't quite that simple - there are times when you don't actually know what kind of object you want. Such situations arise quite frequently during the development of certain kinds of application. There is a (relatively) simple example of this in chapter twenty-seven of the Javanomicon. In this chapter, we look at a case study of a drawing application within which the user can select from a range of possible shapes, each of which is expressed as subclass of a base class called Shape. This was solved by hard-coding the object instantiations into the method responsible for generating the proper shape according to the user's choice - in a sense, this was a simplified example of a creational pattern, although it wasn't ever referenced as such. Creational patterns allow us to deal with the ambiguity present in much software development by formalising the interaction between instances of subclasses and a program which is required to deal with them. Most of these patterns make use of polymorphism - there must be a common structure in the objects provided to the user, whether it be expressed as an inheritance or as an implemented interface.
Traditionally we break a factory out into a separate class of its own - this class provides a method that returns a specialisation of a sub-class. Let's look at the drawing application example from the Javanomicon as an example of this. We have a base class called Shape - that class provides some base functionality for setting some attributes - primarily the X and Y co-ordinates of the top left corner, and the length and height of the shape. It also, importantly, defines an abstract method called drawShape - this is the method that all child classes use to actually display themselves on the screen. We are not particularly concerned with the code for these classes, so we will represent them only as simple UML class diagrams:
There are any number of specialised versions of this base class - the important bit is that they all provide the implemented code for drawShape, which causes them to draw themselves using the Graphics object passed as a parameter:
This is all very standard - the tricky part comes in how the drawing application actually works. It takes instances of each of these objects, and adds them to an ArrayList. Every time it is supposed to draw the shapes, it steps over every element in the ArrayList and calls the drawShape method on each. However, when the user picks a shape to draw, the application must correctly generate the shape according to the user's wishes. We don't know in advance what the user is going to request, and so we need to provide some mechanism for generating the appropriate subclass. We used a simplified factory pattern for this in our drawing application, but we can benefit from making this into a proper factory pattern within a class of its own:
This is our factory - rather than deal with the instantiation directly within the bulk of our program, our factory class abstracts it:
The improvement in this particular example is minimal, because it is already a factory - it's just not implemented as a separate class. However, the technique is very effective and something you can consider for inclusion in any program where you must deal with run-time ambiguity as to exactly what subclass is to be instantiated.
The factory pattern itself can be placed within the context of a larger pattern - that of the Abstract Factory pattern. Whereas a factory is responsible for creating particular instances of objects, an abstract factory is responsible for generating particular implementations of the factory pattern. In a previous chapter we looked at how we can set the 'look and feel' of a Java application - the abstract factory pattern shows how this can be done by having the individual component generation done by a factory, and the actual factory used being generated for the specific look and feel. Consider the following two simplified class hierarchies:
With these two class hierarchies, we can write a template for the generation of any supported component through the implementation of an abstract factory pattern:
Each factory is responsible for generating instances of a widget based on the type of component wanted - each provides its own implementation for getWidget that returns the correct object depending on what's passed in as a parameter... for example:
With this structure, and the power of polymorphism, we can have an immensely flexible framework for generating components with a minimum of code:
As can be seen, the application of design patterns can make even seemingly intractable problems soluble with minimal effort.
There are other patterns, but this chapter is of a necessity too short to cover all the patterns you are likely to encounter. Some notable ones worthy of investigation are:
Each of these has a role to play in the development of quality and reliable software. It would be a good idea to spend some time learning about their various qualities and how you can make use of them within your own programs.
Structural patterns devolve into two main types - object structural patterns and class structural patterns. Object structural patterns are concerned primarily with how objects interact to provide more complex functionality (a case of the whole being more than the sum of its parts). Class structural patterns are concerned with how best to model the interaction between various classes in terms of inheritance, encapsulation and interface implementation. Again, these won't be entirely new... some of these are covered in the Javanomicon (for example, the Adapter design pattern), and some you will have seen whilst developing Java programs and using the standard Java framework classes. One example of this in particular is the iterator design pattern which is used quite a lot when dealing with Java collections - the iterator object is consistent, even if it is has to do something different to step over elements in different kinds of collections. Most of these patterns are complex and aimed at very particular situations that we have not encountered much through the course of this module, so we will concentrate on only one of these - the facade pattern. Most of what we're doing in this chapter is bringing some patterns that you may find useful to your attention - there's not enough room to cover all of them. However, you may want to spend some time reading up on some of the following patterns:
Design patterns provide a very useful method for implementing good solutions to programming problems, but they often introduce a problem in that they often greatly increase the number of classes used within an application. Keeping track of these classes is difficult enough, but presenting them as programmatic interface for other developers is even more difficult. The facade design pattern allows you to abstract this difficulty by providing a single object that encapsulates a number of objects and provides a simplified interface that cuts down on the complexity of interaction. The facade deals with directing calls to those objects within its composition, sometimes by passing information between various objects and then parsing them, before returning the results to the calling application:
This is a very similar idea to how components work - a component represents a facade, and this facade is responsible for dealing with messages passed to and from its constituent objects. What we have previously referred to as a 'footprint' is more properly called a facade. Facades are black-box - they represent an abstraction away from the implementation details of how objects interact. Although they add a new class into any given project (the facade itself), they reduce the coupling and increase the cohesion of the project in the process - this is a Good Thing. Consider for example our abstract factory example above - as a very simple example of a facade, we could have an object that abstracts the AbstractFactory and the Factory into a single object:
Now, rather than the developer having to know what to do with the Factory object that we get out of the AbstractFactory, the developer just has to create an instance of this ComponentFacade and call getWidget on it to get his components:
Notice with this example that although this is an example of a facade, it is in certain respects also a Factory. Like components, patterns can be combined and interchanged to create very powerful programming structures.
Behavioural patterns deal with how objects communicate, and how they relate to each other to solve particular programming problems. Again, there are examples of this we are already familiar with, but we can benefit from spending some time talking about how they can be used in more general contexts. Behavioural patterns are profitably applied to a range of programming situations - they help reduce code complexity as well as provide strong foundations for further development. As with most design patterns, they introduce a little extra complexity into a program by increasing the number of classes required - but this small increase in complexity usually greatly offsets the complexity required to implement a bad solution. We've already seen one example of a particular behavioural pattern when we talked about bound properties - this was an implementation of a design pattern called Observer... with the use of bound properties, we could have a single Javabean that could be modified by a number of different interfaces, and yet still dynamically update the contents for each. In this section we'll talk about one further pattern - that of the Chain of Responsibility.
The chain of command pattern is concerned with passing calls for functionality along to a number of objects that may, potentially, be able to provide that functionality. Consider for example the way context sensitive help works in most Windows applications - there are often a range of possible objects that can provide help in general, but only one that will be able to provide the help that is being requested. The Chain of Command pattern is used to build up a chain of objects that may potentially be able to provide functionality. Messages are sent to this chain via an object that acts as a facade (see how it all links together?)... the facade object iterates over each of the objects in the chain until it finds one capable of handling the requested functionality. This may sound familiar - and it should... it's exactly how Java handles method calls through inheritance. It first checks to see if the child class can deal with the method call, and if it can't it checks that child's parent, and then the parent's parent, and so on until it finds something that it can use. The first thing we need to implement a chain of command is a chain... usually we'll use an ArrayList or some other equivalent structure, but there's no requirement - internally, it can be as complex as needed. We need a method that lets us add an object as a link in the chain, and another method that lets us send a message to every element. However, we need to be able to enforce a particular structure on objects that are to be added to the chain - we need a standard we can rely on to programmatically query them to see if they are able to deal with messages. For this, we'll need an interface. We'll call this interface ChainListener. It isn't going to be complicated - we only have one method that takes a string message, and returns a Boolean indicating whether it has dealt with that message:
We can make use of the Power of Polymorphism now to ensure that we can only add ChainListener objects to our Chain:
Our sendMessageToChain method is going to iterate over every element in our chain and call our heardMessage method on each - it'll do this until it finds one that returns true, and then it will stop:
We need each object that can act as a listener to implement the appropriate interface. For a very simple example:
Everything that goes onto the chain must implement ChainListener, but once we have a chain we can have messages handled by the appropriate objects by simply sending a message to the entire chain:
This particular example doesn't work quite like inheritance because there is no hierarchy - but as with all design patterns, what is above is an implementation - the pattern itself is only a guide. It is perfectly possible to use a chain of responsibility pattern that does indeed rattle messages up a stack. The important thing is the role the pattern plays in a particular program. This example terminates the search as soon as single object matches the requested functionality - you could have a chain that executed the command in all matching objects simply by removing the check on the Boolean variable when iterating.
As has mentioned above a number of times, you'll have seen a lot of design patterns already - some of these we've talked about previously, such as the observer pattern and the adapter pattern. Some of these we've just learned about, but made use of within Java, such as the Iterator. There are a whole host of other design patterns that will be familiar to us - they are not usually very complex ideas, they are just very adaptable ideas. This book is not just about components - it's about writing better programs through the use of components. Components are designed to be deployed in many different locations, and this puts the developer in a particularly difficult position - how do you write a truly generic piece of functionality when you can't be sure, in advance, where your application is going to be deployed? All of these patterns we've talked about in this chapter have special relevance for developing components. A facade lets you present a complex collection of classes as a simplified abstraction - this fulfils principle two of the component design principles outlined in chapter four. Factory patterns allow us to divorce context from object creation, meaning we can provide generalised solutions even when we don't know how they are to be implemented into a particular system. An abstract factory lets us adopt a flexible approach to generating factories, and a factory lets us generate generic components for adaptable deployment. The chain of command pattern allows us to handle messages amongst a range of unrelated objects, even when we have no idea (beyond the implementation of the ChainListener interface) what the structure of the objects are. All of these are very useful techniques that make the development of 'plug and play' components much easier. But the study of design patterns is an area well worth of study in itself, and not just in terms of how they relate to the flexible design of components. There are a huge range of patterns, and most of them are not only technically interesting, but they also open up whole new vistas of potential development. Interested students are invited to spend some time reading up on the subject.
A single chapter on this topic is not nearly enough to justify the depth of the subject area. We could easily spend an entire module on this subject and still have lots left to talk about. We've concentrated on those patterns most relevant to component design in this module, but all of the others are also useful, but mainly as simple examples of good program design. They all exist for a purpose, they have all been used in many different situations... and none of them involve any new code. Throughout this module and book we've mentioned a number of times that this module is where the really difficult part of programming is discussed - that of design. By the end of second year you know all of the tools you are ever likely to need. There are more, of course... but your toolbox is full of more tricks than a professional developer would have available ten or twenty years ago. You can write practically any program you could possibly imagine using nothing more than you've already learned in terms of tools (you'll have to learn new objects, but that's relatively easy). The thing that is likely to hold you back is program design - programs get exponentially more complicated the more functionality you add, and every new class, if not adopted using the correct strategies, can cause more problems than it solves. It seems bizarre, but sometimes it's easier in such situations just to cut and burn the whole project and start afresh. Bad design decisions have a ramification far beyond the immediate - they will propagate, and multiply, and have an impact even on pieces of functionality that are entirely unrelated. Spend some time thinking about design patterns - it's not the most obviously interesting topic, but it offers the benefit of a world of experience, and proper adoption will save you countless hours of hair-tearing frustration in the future. Further ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron