Published Sunday, March 27, 2005 9:27 AM by andykr
Here's another one that I get asked frequently. Just how should you go about designing classes. Of course, there is no single ‘right’ answer, but I have always found it helpful, when considering creating a new class, to ask myself three questions:
· Is the object going to be re-used?
· Will the class simplify the task of managing complexity?
· Is it worth the effort?
Just for clarity, let me expand these a little:
Re-Usability
This is probably the commonest reason for creating a class and the achievement of re-usability is, after all, one of the primary goals of OOP. At its simplest level this can mean as little as setting up your own personal preferences for objects – Font, Color and Style for example so that all objects of your class are created with the correct settings in place. Things get a little trickier when you consider functionality. How often, in practice, do you do exactly the same thing, in exactly the same way, to achieve exactly the same results, in exactly the same environment? The answer is probably “not very often” and you may begin to wonder whether re-usability actually is so valuable after all. This leads us neatly to the second criterion.
Managing Complexity
It is actually very rare that anything other than the simplest of functional classes can simply be re-used ‘as-is’. There are almost always differences in the environment, the input or the output requirements and sometimes all of them. This apparent complexity can sometimes obscure the issue, which is that the desired functionality remains constant, even though the implementation may differ in detail between applications.
By applying the rules for designing classes outlined in the next topic, it should be easier to decide whether using a class will ease the management of this complexity or not. Even if the creation of a class could ease the management of the complexity, we still have to consider our third criterion.
Is it Worth the Effort
An object should be encapsulated so that it contains within itself all of the information needed to complete its task, and indeed it is axiomatic that no object should ever rely on the internal implementation of another object. Clearly this can make life extremely difficult. It may mean that your class will have to check dozens of possible conditions to determine (for itself) exactly what the state of the system is before it can perform its allotted function. When considering the creation of a new class, it is important to be sure that it is actually worth the effort of doing so.
What sort of class do we want?
Having decided that I really do want to create a new class, the next question to answer is “What type of class should I design?”. There are two fundamental models for class design which are the “Real World” or concrete model and the “Component” or abstract model. Neither is inherently better than the other, they are just different.
Real world (concrete) model
In this model classes are designed to deliver the required functionality in its entirety. Business Objects are usually based on this model. For example, we could define a “person” business object class that has the common characteristics (e.g. Properties for Left Eye, Right Eye, Nose and so on) and behaviors of a person (e.g. Methods for Breathing, Eating, etc). This person class can then be sub classed to model the functionality of special types of people such as “customers” or “friends”. It is readily apparent here that the real world model relies heavily on inheritance and, as a result, most of the functionality is explicitly defined at design time.
Component (abstract) model
In this model classes deliver specific behaviors rather than functionality. Each class defines a generic function or action and, in isolation, has limited usability. Clearly there will be little reliance on inheritance, instead, the model depends upon aggregation (essentially a run-time operation) and composition (both design and run time). The equivalent of our real world person business object is a composite of separate objects: two instances of the eye class, one instance of the nose class, etc.
Identifying ResponsibilitiesHaving decided on the model to use, the next task is to be absolutely sure that you know what the class is actually going to do. This may sound obvious, but there is a subtle trap here. It is very easy to build so much into a class that, before you realize it, you have built something that is actually not re-usable because it does too much! The best approach is to identify, and categorize, the ‘Responsibilities’ of the class before you start writing any code. A responsibility can be defined, very simplistically, as some element of functionality which has to be completed.
Categorizing Responsibilities (The “Must, Could Should” rules)
The purpose of the categorization is to ensure that I can correctly identify where, in the class hierarchy, each responsibility belongs. I do this by using three categories which I call “Must Do”, “Could Do” and “Should be Done”
The purpose of the categorization is to ensure that I can correctly identify where, in the class hierarchy, each responsibility belongs. I do this by using three categories which I call “Must Do”, “Could Do” and “Should be Done”
Responsibilities that fall into the first, 'Must Do', category are those things which an object derived from the class would have to do in every situation and which could not be done by any other object. They are, therefore, clearly the direct and sole responsibilities of the class. These must form part of the root class definition. For example, any object that moves the record pointer in a table, cursor or view, MUST check for EOF() and BOF() conditions. So this functionality belongs in the root class.
Responsibilities which fall into the 'Could Do' category are indicators that the class may require one or more subclasses (or, possibly, the cooperation of objects of another class). In other words these are the things that it would be possible for the class to do, but which would not actually be required in every situation. Typically it is the failure to recognize these 'Could Do' items early enough in the design process that leads to the inappropriate placement of functionality in the class hierarchy and which limits re-usability. For example, an object that displays a list COULD disable itself once a selection has been made. However, that is not essential to its function and is not behavior that would be required in every instance. It therefore belongs in a specialized sub-class.
The final (‘Should be Done’) category is very important indeed. This is the category that defines the 'assumptions' that a class must have fulfilled in order to function properly. Items listed here are definitely not the sole responsibility of the class in question but must, nonetheless, be done somehow. For example, in order for a control to be bound to a data source, the data source SHOULD be opened before the control is instantiated. However, managing that operation clearly cannot be the responsibility of the control itself – if only because it must be done before the control is instantiated.
Having defined and categorized the Responsibilities of the new class, you can then define its Public Interface by deciding what Properties and Methods it will require, and how it will reveal itself to other objects with which it will interact. Only when all this is done can you begin to think about the actual code and there are a couple of things to remember about this too.
Code in Methods, not Events
When coding classes I believe firmly in avoiding placing code directly in native Visual FoxPro events whenever I can do so. Instead, I create custom methods and call those from the event. There is, admittedly, no requirement to do things this way, but I find it makes life easier for three reasons.
· It allows me to give meaningful names to method code. This may sound trivial, but really does make code easier to read and maintain when the code that produces some form of output is called by ‘This.GenerateOutput()’ rather than ‘Thisform.Pageframe1.Page1.CmdButton1.Click()’
· It allows me to change the interface, if necessary, by simply re-locating the single line of code which calls the method instead of having to cut and paste functional code
· It allows me to break complex methods up into multiple, (and potentially re-usable) methods. The event acts merely as the initiator for the associated code.
Methods should do one thing and one thing only
Methods that do “everything” are difficult to reuse. Remember; a single line of code is always reusable. If I have to hit PAGE DOWN more than once to view my method code, the method is probably too long and trying to do too many things. The solution is to break such monolithic methods up into smaller, better focused, methods and use a control method to call them individually if necessary. (Ask yourself how many times you have found yourself duplicating, within a method, a block of code that already exists as part of another method).
Beware of putting functionality too high in the class hierarchy
This is a trap that I have fallen into many times over the years and still occasionally stub my toe on. You will generally know when you have placed functionality too high because you will spend a lot of time in you subclasses trying to over-ride all the great functionality that you put in the root class. One of the major benefits of the “Must, Could, Should” approach is that it helps to avoid this trap.
Use template methods for a consistent interface
I stated that we need to avoid placing functionality too high in the hierarchy. However, this doesn’t mean that we shouldn’t define template methods in our root class. By creating empty, or placeholder, methods at the highest level of the hierarchy I can ensure that all subclasses share a common interface. As long as subclasses share a common interface, they are interchangeable. Even if an empty method gets called, it won’t hurt anything, because all Visual FoxPro methods return .T. by default – even when they contain no code.
No comments:
Post a Comment