![]() | Monkeys at Keyboards: Java-Fu © Michael James Heron | ||||
| Topic: Java Programming Level: 3 Version: beta | |||||
4 - Component Architecture | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
Component-based programming is just another way of looking at software development - in its purest form, it doesn't usually mean learning anything new in terms of tools or syntax. It means adopting a particular mindset towards software development that puts the emphasis on reusable development. Around this central idea have been built a number of component technologies - it is important to realise that these are just particular implementations of the component philosophy - it is possible to write component-based solutions without ever looking at an established component architecture. In this chapter, we'll be looking at some of the thinking that has gone into the idea of component based programming, and examine its relevance to what we do as software engineers. We'll also discuss some of the major 'players' in component architecture and where they fit (or indeed, where they don't).
Obviously there has to be a reason to think in terms of components - if there wasn't, then it wouldn't be a popular technique. However, it's not all roses and lollipops - as with most things in life, it involves a tradeoff. Components are functionally discreet - they perform (usually) a single task. How they perform that task internally is of no interest to us. All we are interested in is how we can send relevant information to the component, and how we can get relevant information back. Components provide an easy way to slot generic pieces of functionality into a software product. However, it's a 'vanilla' solution, a case of 'one size fits all'. The decision to use a component means that we sacrifice some flexibility for some convenience - we get an easy solution, but it may not be exactly the solution we would want. One of the component architectures you will already be familiar with is Swing - all of the GUI widgets that we have used in the past are components. These components are usually very configureable in terms of what values we can set on them, but still they are a 'vanilla' solution. This is not always a bad thing - there's very few gains to be made by making user interfaces different from application to application. However, if there is some piece of very specific functionality we need from a GUI widget, the vanilla component may not do what we want as a standard option. For example, let's say we wanted an image that changes based on the passage of time. It's very easy to implement this in code, but that's custom functionality that we must provide outside of the component. Sometimes we may want a component to do something that is simply not possible - for example, using the AWT component library means that we cannot make use of menu bars within an applet without some serious fudging. Sometimes the correct response is to simply use a different component- instead of using AWT, we can use Swing. Sometimes the correct response is to ignore components entirely in favour of a custom solution. It all depends on the amount and complexity of the functionality required. It's an application specific decision - you, as a developer, are the one that must decide whether an approximate solution is 'good enough'. However, custom software also comes at a price, mainly in terms of the 'cost' of development. This cost can be both financial (in the real world), or simply in terms of how much time it will take. The initial outlay can be very significant, and if developing a piece of ancillary functionality, it may not be justifiable. Components on the other hand have had the initial development already put in - the initial outlay for a developer to integrate a component is negligible. This is an argument that is also used in favour of 'object oriented programming' in general, but it is more convincing with components since they are more substantial functional elements. Tied to the idea of the time to develop a custom piece of functionality is the cost that goes into modifying it, particularly with regards to expansions. With a component, you can either slot in a different component that does a better job, or slot further components into the component itself. Again, the cost of using a component is greatly reduced. Further elements of cost that need to be considered are the costs to support - custom code requires customised support (costly), especially if multiple software projects use custom code that implements similar functionality. Provided a component is well-tested at the start, it requires less maintainance in the long-run because of the simpler coupling mechanisms used to link it into a project. Only the coupling between the component and the developer's software project need to be maintained. Custom software often lacks a 'brand building' strength, mainly because it is usually written for a very particular niche interest and expanding that interest can be more costly than the benefits likely to be reaped. Components, because of their generic nature, are more likely to be adopted in multiple projects and therefore gain the benefits of brand recognition - such as increased support and an impetus for further development. Most developers, in the end, don't go with a 'pure' solution. Very few developers will simply slot existing components together and present it as a finished application. Likewise, very few developers will want to 'reinvent the wheel' with every application and so will make use of a library of useful components in their own projects. Finding a balance between 'off the shelf' and custom software elements is the key to productive development.
In order to develop components that can easily be stitched into a custom software project, there are a number of design elements that must be considered. If a component is presented as a holistic whole, but lacks certain aspects of thought in its design, then it is no more useful than any given random collection of classes. Consider the software projects you may have written during your second year programming course - those are all finished products that provide a certain aspect of functionality. If you gave one of those to someone and said 'this will solve your problem', you won't actually be helping them much. This is not a problem with your projects, but a consequence of the design. There is no agreed standard for what aspects all components must implement, but this is the list we will adopt for this particular module. All components will adhere (at least in part) to all of these design principles:
We will look at each of these design elements during the course of this book. It should be emphasised at this time that we will be learning how to use lots of new tools during the coming semester, but not all of them are actually components in themselves. For example, later we will look at the XML parsing libraries that can be used to develop XML documents (which satisfy the 'well documented file format' requirement of component design) - these are a suite of classes, but they are not a component in themselves. As developers, we don't care how a component works internally. They are ultimately black box tools - all we want to know is what we have to send to the component, and what kind of information we get back. The process of setting up the IO between a software project and a component will be referred to within this module as stitching. Because we don't care what is going on within, we need to be very sure of our stitching - unless we have access to the code for a component, and the will to learn how to modify it, we will be unable to follow the processing that goes on within. We have to make the assumption that a component has already been exhaustively tested... therefore, if there is a problem we look at the stitching code we have in place to ensure that we are sending and receiving the right information. As can be imagined, this makes debugging much, much easier - we don't need to do unit testing, we just need the integration testing. The connections between our application and the component are the only thing we need to worry about getting wrong. A single external footprint is not just a syntactical niceity - it's not just a case that 'oh, it's less effort to create a single object, so that's what all components have to do'. It's fundamental to the idea of component based programming, and a message that we will emphasis throughout the module: a component is not just a collection of related classes. A component requires a single object footprint to ensure that the work of integrating various internal classes has already been done... requiring a developer to do this undermines the whole point of writing components. Consider a simple component - the JButton. Externally, we instantiate this as a single object. Internally, it is a vast, complex web of classes and interfaces. We don't need to worry about that - we just need to know what data to send, and how we get information back out of it. A JButton is an immensely useful component - it would be all but useless if you had to integrate all its various classes yourself. Finally, any data that is persistant should be stored in a transferable format. Usually a component provides a piece of specialised functionality, and further pieces of functionality are provided by other components - if there's no way to ensure a progression of information from one component to the next, then the whole design philosophy breaks down. Sometimes this is handled by parameter passing, but other times (for very complex data structures) it is handled by persistant data formats. Imagine if a component stores a piece of data in a three dimensional mathematical matrix - what are the chances that any other component is going to be able to interpret that unless it's stored in a very well-defined formal standard? The act of converting the data between one component and another then becomes a complex computational task that must be accomplished by a piece of code that sits between the two components - this is known as a translation layer. Consider the situation where you have two diplomats, one who speaks only English and one who speaks only French. If these two try to communicate they will get nowhere since they don't understand each other. There needs to be someone who can translate from one to the other and back again - it needs someone who understands both English and French to act as a go-between. Consider a very simple example of this - a component that produces a tic-tac-toe grid, and another component that displays such a grid on the screen. Provided they are both using a common data format, this is simple. But what if one is using a rows and columns notation based on 1s and 0s and dashes, and the other is using a columns and rows notations based on Xs and Os and dashes? In order for component 1 to communicate with component 2, it needs a translation layer that will translate one format into the other:
The necessity for this is removed by ensuring a consistent data format from the very start, and should be insisted upon for good component design. Components should reduce the amount of development required, not add to it.
This ties (a little) into the formal field of design patterns. It's not the knowledge of a programming language that makes someone a successful software developer - it's the ability to recognise patterns and to develop software structures that can manipulate those structures in the appropriate way. The study of design patterns is an attempt to formalise the 'experience' aspect of software development by documenting the design strategies that can effectively solve certain kinds of problems. A pattern, put simply, is a solution to a problem that occurs frequently through the course of normal program development. Patterns, like components, can be stitched together to provide a comprehensive strategy for developing powerful software solutions to what may seem like intractable problems. Patterns, because of their widespread adoption and constant revision, represent the 'best' (or at least, 'a good') practise for writing certain kinds of programs. An algorithm may seem to fit the definition of a design pattern, but they are not generally considered to fall into the category since they are concerned primarily with implementation details rather than genuine design strategies. Usually a design pattern is used to denote a tight interaction of a set of classes or objects. There is a formal notation used to describe particular patterns - the intention behind the field of study is to develop a handbook of common solutions in the same way that other engineering disciplines do. The design pattern that most closely maps onto the idea of component based programming is that of the Scripted Component, as described at http://www.doc.ic.ac.uk/~np2/patterns/scripting/scripting.html. The terminology of the pattern is somewhat different from the terminology we will be using within the module, and the pattern somewhat stricter than we will be using. It is worthwhile reading around the subject area however, since will be taking a look at a number of tools that make use of standard design patterns (such as the Factory pattern). It will help gain a sense of deeper context for those students interested in the reasons behind certain implementation decisions taken by the developers of tools external to the core Java libraries. Perhaps the most important core idea of the design patterns philosophy is this: success is more important than novelty. It sometimes rankles to make use of a standard component rather than write your own code - this is a mindset that it is important to drop when dealing with real world problems. A pragmaticism towards software development is necessary to ensure that the continuing software crisis is dealt with.
We use the term 'component' to describe a range of different tools - the only thing that connects them is that they are largely modular, and do not rely on their implementation context (which means, it doesn't matter where they are deployed). We have already looked at one particular component architecture - that of the JavaBean, which is the most instantly accessible to us considering our development backgrounds. The first 'real' component architecture that made its way into real world development was the VBX component, which allowed developers to plug expanded functionality into Visual Basic programs. Perhaps the most widely used component architecture is that of ActiveX, which followed VBX... this is fading into obsolence however since it has been largely superceded by Microsoft's .NET platform. ActiveX gained its enviable ubiquity largely on the back of Internet Explorer - the ability to embed an ActiveX component within a web-page opened up a world of new functionality, as well as increased risk to browsers. We will not be looking at ActiveX within this module - it is no longer relevant, and the skills needed in order to appreciate the complexities of its design model are not transferable enough to warrant inclusion in the module content. However, ActiveX does have one very substantial consequence - its adoption on the internet highlighted the fact that components are not necessarily local to a particular application. Given the appropriate underlying technologies, a component can just as easily be web-based - this has ramifications on the content of this module, as we will have cause to look at various technologies that this perception shift have brought within the remit of 'component based programming'. We will be looking at both local components, and remote components within this module. We have already looked at the JavaBean component architecture, and we will be seeing this many times throughout the module. We will also be taking a closer look at some of the standard Java components with which we are already familiar. For remote components, we will be looking at SOAP based clients and servers, as well as Java Servlets. The benefit of using remote components is that, provided they support some common messaging system (like SOAP), then it doesn't matter how they are implemented. It's perfectly possible for a Java based program to make use of a .NET based server, or for a C++ application to make use of a Java Servlet, provided there is a way to tie them together. Coincidentally, we we also be looking at tying the two together! We will look at XML and its various parsing routines - this allows for a common, language independent bridging data structure that we will see in action when we talk about SOAP. We won't have an opportunity to cover many other component architectures - mostly there are becoming less popular in the face of an increasingly pitched battle between Java and .NET for control of the hearts and minds of developers everywhere. There are some other interesting technologies that look as if they will be of interest for the future - for example, Mozilla's XPCOM. Interested students are directed towards this is they want to see a little more of what is going on in the field.
This module will make quite a big deal out of 'interchangeability', but this is something quite difficult to quantify. What is it that makes component interchangeability substantively different from class interchangeability, for example? It's easier, sure - but is that enough to justify an entire development paradigm? The real benefit lies in the reduced the impact that a change will have on the rest of a software application - because everything is handled internally, there is no cleanup required to ensure that all the connections between the component and its context are still correctly coupled. It may be necessary to change the connection code, but because all of the functionality is neatly encapsulated into a component it greatly lessens any impact. The internal structure of a component is black box, so even if the component itself is radically changed in a later version, it should have absolutely no impact on your application except for the changes in the component functionality. This is a giant benefit, particularly in large and complex systems. In standard software design jargon, components have low coupling and high cohesion - they epitomise what most software design strategies aim towards.
The three-tier model is a standard software design strategy, and works by separating out the three fundamental aspects of a finished piece of functionality into separate subsystems so that they can be easily exchanged if necessary. This is sometimes referred to as the PAD Architecture (Presentation, Application, Data). The Three Tier architecture neatly articulates the benefits of component based programming in a microcosmic sense - ideally a component itself will be broken up into these three tiers and allow for other components to fulfil the role of any tier as required. Obviously this can be a recursive structure that eventually terminates at some minimal point of functionality. The three tier structure breaks a program down into three main areas:
In The Javanomicon we looked at a design methodology called model-view-controller, which worked along similar lines except that presentation was dealt with as two different aspects, and no separation of data was enforced. Part of this book's aim is to help you think about program design, not just in terms of designing components, but in terms of writing programs that are more modular. If your applications are not properly designed, components will not help you - if your programs are properly designed, then components will indeed be a useful tool to help your development, but more importantly your own programs will be much better and easier to maintain and develop. A combination of the MVC design strategy and the PAD strategy can provide a useful framework for developing your application in preparation for making use of remote or local components:
At the beginner and intermediate levels, you can get away without proper class design - at the beginner level you are learning syntax, and at the intermediate level you are learning tools. However, both of those are the easy parts of programming (although in the intermediate level on this site there was a big emphasis on actually using the tools to solve problems). From now on, you're going to be dealing with the hard bit of programming - design. Learning to use the syntax and using the syntax to use the tools is the bit you need to learn before you have to start doing the hard stuff. Sure, there are always more tools to learn, but it's design that's the really difficult part. The first step in writing an application is design - if you don't get the design right, then the rest of the program is going to suffer as a result. Choosing a solid design framework should be your first step, especially if you are hoping to be able to incorporate off-the-shelf components into your application. The model suggested above is only a general guideline - at its most basic, you can think of it as a four-class framework. You should have a separate class for your user interface, a second for your event handling logic, a third for handling the actual functionality, and a fourth for handling data routines (if you have data persistence). All functionality should be encapsulated within its relevant class. This serves as a useful starting point, but it is unlikely that any real application will have as few as four classes - the model will usually be handled by a number of different classes that combine into a single useful object - maybe even a component itself. Of course, as your application becomes more complex and incorporates more classes, the benefit of a UML based notation will start to become more obvious, even if only to keep the relationship clear in your own mind. Software Analysis and Design is often taught as a separate subject out of necessity - the kind of projects that it is best suited too are far too complex to cover in a first or second year programming module. Understanding the interrelation of classes in the example would greatly complicate the task of learning the analysis and design tools. However, there is one common thread throughout all design methods, and that is that they are all about understanding a computer system, either one being proposed or one already existing. It is highly recommended for this module that you adopt a formal design process for the applications you design for your assessment. It is not necessary (for the grading) to document everything according to any given method, but you should find it a valuable exercise is understanding how to write better programs. Part of the assessment for this module is based on how well your programs are designed - not just how well they actually work. We discussed encapsulation, polymorphism and interfaces in general terms in the last book, but now they start to become genuinely important tools - we'll look at these again in the next chapter in relation to the way the Java class libraries actually work.
Software components are designed to be reused in many places, in many different contexts and by many different projects. As such, it's very important that developers get it right at the start. There is a programming language called Eiffel that incorporates a process called programming by contract in an effort to cut down on the problems normally encountered when developing programming solutions. Although most languages don't have support for this built into the language, there is nothing to stop the same process being used in other languages. The central idea behind the paradigm is that software entities (such as components, objects and methods) have obligations to other entities, and these obligtations are formalised into a contract between them. As part of the design phase of an entity, a contract is written up for each before it is ever coded. This contract usually states the following information:
The first three are usually handled as part of sensible development anyway -formalising them in a contract offers an audit trail, but little else. Side effects are those changes to the internal state of variables that occur as a consequence of a method call, but only those changes that were not part of the intended functionality. For example, a set accessor method that changes the state of a variable is not a side-effect. If however we have a method called setBing and during the execution of that method it changes the state of a variable called bong, then that would be a side-effect. The most important part about designing by contract lies in the preconditions, postconditions and invariants. Essentially they are a guarantee that the state of an entity will never violate an agreed specification. For example, consider a method designed to search through an ArrayList of strings for a particular element and then return the index of that string:
We can write a framework class for handling checks of this kind - we create three new exceptions (A PreconditionException, a PostconditionException, and an InvariantException) and a class that provides the static methods that lets you check the value of a boolean expression:
Now this method will throw a PreconditionException whenever the precondition is violated - in this way you can enforce the contract you specify in your design phase with only minimal extra coding. You won't be expected to ensure this in any of the code you will be developing throughout this module, but it is provided as a useful concept that you may find helpful in later projects - especially those projects where it is vital that they are properly designed and comprehensively tested.
We've talked a little above about what benefits software components provide over custom software solutions - but we haven't discussed when to make use of them. In some cases, it's a simple choice - when the benefits outweight the drawbacks. The problem is that an ad hoc philosophy leads to an ad hoc development process. The best thing to do is to sit down and go through each requirement of the application and examine potential candidates for solving with components. Although we will be talking quite a bit about the techniques we need when writing our own components, the focus of the field is not on writing new components - it's about getting maximum use out of the existing components, in exactly the way that the engineers of more 'hands on' disciplines attempt to get the most out of their standard components. An electrical engineer doesn't design a custom capacitor for every circuit board - instead, the intention is to use the existing, reliable tools available. Not all requirements will be suitable candidates for solving with a component based solution - depending on how domain specific the requirement is, it may be too specialised for anyone to have developed an off-the-shelf component. In such cases, the only option is to develop a custom solution. Of course, that solution could be developed as a component in itself, allowing for others to benefit from your initial outlay of effort. Sometimes the component available will be a commercial product, and then it becomes a financial decision as to whether the outlay can be justified in terms of the benefits. Or it may be a free component, but available under a license that is too restrictive for your requirements. Many freely available components, for example, are free only for 'non commercial' deployments. If there is an available component, it's necessary to check if it is compatible - we've discussed the importance of interchangeability throughout this chapter, but in many cases it is honoured more in the breach than in the observance. Sometimes a component requires data to be in a particular format - if your data is in a different format, you may need to develop a translation layer (which may be complex, or impossible). Sometimes there is a component available under a suitable license, but it doesn't do quite what you might like. Depending on how the component is distributed, it may be possible for you to specialise the component through inheritance or via the development of a wrapper class that goes around the component... such developments are inherently risky since they greatly increase the complexity of integrating the modified component with internal and external projects. It also means that your component may not benefit from future maintainance.
You may not believe it from this chapter, but this is not intended to be a 'thinky' book... the emphasis is very much on doing things, especially during the latter half of the term. However, it is important that we discuss some of the context before we go on to the new tools and techniques. In the next chapter we're going to have a deeper look at the Swing components that we have been using so often, and see how the Java interface structure enforces a structure on Swing components, and indeed on all objects in Java. Exciting times ahead!
Exercise oneConsider the idea of designing by contract - this maps quite closely onto the idea of a UML 'business process model'. Try making use of this in the implementation of a simple UML design. Exercise twoConsider the translation layer example in the chapter text - write the code for converting between the two notations.
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