Principles of OOD -------------------------------------------------------------------- 1. Software entities (classes, modules, etc) should be open for extension, but closed for modification. (The open/closed principle -- Bertrand Meyer) -------------------------------------------------------------------- The open/closed principle is the single most important guide for an object oriented designer. Designing modules whose behavior can be modified without making source code modifications to that module is what yields the benefits of OOD. Consider the following: class Shape {public: virtual void Draw() const = 0;}; class Square : public Shape {public: virtual void Draw() const;}; class Circle : public Shape {public: virtual void Draw() const;}; typedef Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { for (int i=0; iDraw(); } Notice that the function 'DrawAllShapes' need not be modified when new kinds of shapes are added to the Shape hierarchy. In fact, the function need not even be recompiled. Yet, even though I don't make changes to the module, I *can* change its behavior. Although the function currently only draws circles and squares, I can enhance it to draw any other shape at all, so long as that shape is represented by an object that derives from the class Shape. So, I can extend the behavior of the module 'DrawAllShapes', without modifying it at all. Now, consider how this would have been done in a language like C. enum ShapeType {circle, square}; struct Shape {ShapeType itsType;}; struct Circle {ShapeType itsType; double itsRadius; Point itsCenter;}; struct Square {SahepType itsType; double itsSide; Point itsTopLeft;}; void DrawSquare(struct Square*); void DrawCircle(struct Circle*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; iitsType) { case square: DrawSquare((struct Square*)s); break; case circle: DrawCircle((struct Circle*)s); break; } } } It should be very clear that the 'DrawAllShapes' function, as written in C, cannot be extended without direct modification. If I need to create a new shape, such as a Triangle, I must modify the 'DrawAllShapes' function. This, by itself, would not be too bad. However, in a complex application the switch/case statement above is repeated over and over again for every kind of operation that can be performed on a shape. These statements are difficult to find, and it is easy to make mistakes when modifying them. Worse, every module that contains such a switch/case statement retains a dependency upon every possible shape that can be drawn, thus, whenever one of the shapes is modified in any way, the modules all need recompilation, and possibly modification. A major factor in software maintenance is the fact that for every change that is made, there is a risk that bugs will be introduced. This wreaks havoc with managers and users because old features that used to work just fine will sometimes break when a new, and apparently unrelated, feature is added. However, when the majority of modules in an application conform to the open/closed principle, then new features can be added to the application by *adding new code* rather than by *changing working code*. Thus, the working code is not exposed to breakage. This creates a significant amount of isolation between features, and allows for much easier maintenance. -------------------------------------------------------------------- 2. Derived classes must be usable through the base class interface without the need for the user to know the difference. (The Liskov Substitution Principle) -------------------------------------------------------------------- This rule is a logical extension of the open/closed principle. Consider function F that uses type T. Given S a subtype of T, F should be able to use objects of type S without knowing it. In fact, any subtype of T should be substitutable as an argument of F. If this were not true, then F would have to have tests to determine which of the various subtypes it was using. And this breaks the open/closed principle For example: Consider the problem of the Square and the Rectangle. Mathematically a Square *is* a Rectangle. This tempts us to invoke inheritance between them: class Rectangle { public: Rectangle(const Point& tl, double h, double w) : itsTopLeftPoint(tl) , itsHeight(h) , itsWidth(w) {} virtual void SetHeight(double h) {itsHeight=h;} virtual void SetWidth(double w) {itsWidth=w;} virtual double GetWidth() const {return itsWidth;} virtual double GetHeight() const {return itsHeight;} private: Point itsTopLeftPoint; double itsHeight; double itsWidth; }; class Square : public Rectangle { public: Square(const Point& tl, double s) : Rectangle(tl, s, s) {} virtual void SetWidth(double w) { Rectangle::SetWidth(w); Rectangle::SetHeight(w); } virtual void SetHeight(double h) { Rectangle::SetWidth(h); Rectangle::SetHeight(h); } }; In the example above, Square inherits from Rectangle, but the functions of Square are overridden to enforce that the height and width of the Rectangle are always the same. Now, consider a function which takes a Rectangle as an argument, and adjusts its width so that it has an aspect ratio of 1/2. i.e. the width is half the height. void HalfAspect(Rectangle& r) { r.SetWidth(r.GetHeight()/2); } This function fails, rather dismally, if a Square is passed into it as follows: Square s; HalfAspect(s); // this just shrinks both sides by 2 Since a user of Rectangle can malfunction when passed a Square, Square is not substitutable for Rectangle. To make a general fix, we would have to add code to HalfAspect to test the incoming Rectangle to see if it was a Square: void HalfAspect(Rectangle& r) throw(BadType) { if (typeid(r) == typeid(Square)) throw BadType; r.SetWidth(r.GetHeight()/2); } And this creates a dependency upon Square. Moreover, every time a new unsubstitutable derivative of Rectangle is created, a new test must be added to all functions that would malfunction. This violates the open/closed principle. -------------------------------------------------------------------- 3. Details should depend upon abstractions. Abstractions should not depend upon details. (Principle of Dependency Inversion) -------------------------------------------------------------------- This is the direction that all dependencies should take in an object oriented program. All high level functions and data structures should be utterly independent of low level functions and data structures. Consider a program that controls a home furnace. Furnace() { while(;;) { while (ReadTemp() > theMinTemp) wait(1); StartFurnace(); while (ReadTemp() < theMaxTemp) wait(1); StopFurnace(); } } This is clearly a simple minded algorithm, but it will serve to demonstrate our point. Notice that this algorithm will apply to any furnace at all. Thus this high level algorithm is an abstraction, it is independent of the type of furnace we have. However, most of the time, programmers will take a high level policy and code it in terms of the low level details. #define THERMOMETER 0x86 #define FURNACE 0x87 #define FURNACE_ON 0x01 #define FURNACE_OFF 0x00 Furnace() { while(;;) { while (in(THERMOMETER) > theMinTemp) wait(1); out(FURNACE, FURNACE_ON); while (in(THERMOMETER) < theMaxTemp) wait(1); out(FURNACE, FURNACE_OFF); } } It should be pretty clear that this high level function is polluted by the details of the actual furnace controller. In fact, the abstraction depends upon the details. It should also be clear that this function cannot be reused with a different kind of furnace. When abstractions depend upon details, the abstractions cannot be reused. We might think that we can improve this by employing structured design as follows: #include "furnace.h" Furnace() { while(;;) { while (ReadTemp() > theMinTemp) wait(1); StartFurnace(); while (ReadTemp() < theMaxTemp) wait(1); StopFurnace(); } } --------------furnace.h------------ double ReadTemp(); void StartFurnace(); void StopFurnace(); -----------------furnace.c------------- #include "furnace.h" #define THERMOMETER 0x86 #define FURNACE 0x87 #define FURNACE_ON 0x01 #define FURNACE_OFF 0x00 double ReadTemp() {return in(THERMOMETER);} void StartFurnace() {out(FURNACE, FURNACE_ON);} void StopFurnace() {out(FURNACE, FURNACE_OFF);} However, while this does break the direct dependency of the Furnace function upon the details, there is still a one to one binding from the name 'StartFurnace' to the function implemented in "furnace.h". Thus, although we can some reusability by recoding "furnace.c", we cannot gain reusability in the same program or application. However, consider the following object oriented design. -----------furnace.h------------- class Furnace { public: virtual void Start() = 0; virtual void Stop() = 0; }; ---------thermometer.h---------- class Thermometer { public: virtual double Read() = 0; }; ------------ regulator.h ------------ class Furnace; class Thermometer; class Regulator { public: Regulator(Furnace& f, Thermometer& t); void Regulate(double minTemp, double maxTemp); private: Furnace& itsFurnace; Thermometer& itsThermometer; }; ---------------- regulator.cc ------------ #include "regulator.h" #include "furnace.h" #include "thermometer.h" Regulator::Regulator(Furnace& f, Thermometer& t) : itsFurnace(f) , itsThermometer(t) {} void Regulator::Regulate(double minTemp, double maxTemp) { while(;;) { while (itsThermometer.Read() > theMinTemp) wait(1); itsFurnace.Start(); while (itsThermometer.Read() < theMaxTemp) wait(1); itsFurnace.Stop(); } } -------------------detailedFurnace.h--------------------- #include "furnace.h" class DetailedFurnace : public Furnace { public: virtual void Stop(); virtual void Start(); }; -------------------detailedFurnace.cc------------------- #define FURNACE 0x87 #define FURNACE_ON 0x01 #define FURNACE_OFF 0x00 void DetailedFurnace::Start() {out(FURNACE, FURNACE_ON);} void DetailedFurnace::Stop() {out(FURNACE, FURNACE_OFF);} -----------detailedThermometer.h -------------- class DetailedThermometer : public Thermometer { public: virtual double Read(); }; ------------detailedThermometer.cc------------- #define THERMOMETER 0x86 double DetailedThermometer::Read() {return in(THERMOMETER);} ------------------------------------------------------------- Notice that now the high level algorithm depends only upon abstract classes. Notice also that the details depend only upon the abstractions. Notice that the Regulator class can be reused with any derivative of a Furnace and a Thermometer. Morever, any application can have many instance of Regulators, Thermometers, and Furnaces. Thus, we could reuse this algorithm in, say, an application that controls the reaction temperature of many different vessels.... Notice also that there is much more code in the OO version. It takes code to invert dependencies. This is the price we pay for the reusability and maintainability that dependency inversion affords. -------------------------------------------------------------------- 4. The granule of reuse is the same as the granule of release. Only components that are released through a tracking system can be effectively reused. -------------------------------------------------------------------- Reuse does not come automatically. One cannot simply write a class and claim that it is reusable. First, a reusable class probably has several classes that it collaborates with. Thus the class is not reusable in isolation, it must be reused in conjunction with other classes. Second, reusers do not want the classes that they are reusing to change out from under them. So they will require some kind of release tracking system to be put into place. It should be stated at this point that "code copying" is an inferior form of code reuse. Code copying puts the burden of maintenance upon the reuser, not upon the author. In such cases, the only thing that is being reused is the initial design of the code. Yet we all know that initial design is worth a small percentage of the total effort through the lifecycle of a project. Thus we want reusable code to be maintained by the author, or by a common maintainer. We want all the reusers to benefit from the single source of maintenance. In order to achieve reuse of this kind, the reusable software must be put into a form that the reusers can manage. Specifically, this means that it must be segregated into "chunks" which are relatively independent of each other, and given version numbers so that the reusers can track which version of a chunk they are currently using. The chunks must be small enough so that reusers can pick and choose the software that they want to reuse. Reuse is not worth much when it is an all-or-nothing proposition. When changes are made to these released "chunks", new version numbers must be assigned, and notification must be given to all reusers. Reusers will then decide if they wish to use the new release immediately, delay such use until their software can be made compatible, or reject such use and take over maintenance of the previous version themselves. In the absence of a release strategy and version numbering scheme, reuse of commonly maintained software modules is extremely difficult, if not impossible. Thus, we say that "the granule of reuse is the same as the granule of release." Now this has implications for the way that object oriented programs should be structured. One such structuring mechanism is employed by Booch. He organizes a group of cohesive classes into an entity called a class category. Class categories can be used as the granule of release, and therefore the granule of reuse. -------------------------------------------------------------------- 5. Classes within a released component should share common closure. That is, if one needs to be changed, they all are likely to need to be changed. What affects one, affects all. (The principle of Common Closure). -------------------------------------------------------------------- Released components are the targets for dependencies. In fact, the reason that they are "released" is to limit the affect of changes upon the users of the component. This implies that releases should not occur often, and that they should occur in isolation where possible. The worst case scenario is when a change to the system causes modification of all the released components, such that they all need to be re-released. When changes occur, we want those changes to affect the smallest possible number of released components. In OOD, the released coponent is the "category". And the principle we are discussing is the principle of "Common Closure within Categories." This priciple is the first and most important of the three rules for category cohesion. A category is cohesive, if the classes within it are all closed to the same kinds of modifications. The open closed principle states that a class should be open for extension but closed for modification. This is an ideal that cannot be completely achieved. Regardless of the design of a class, there will always exist some kind of change that will force the class to be opened. However, do not have to protect ourselves from every kind of modification. We can anticipate the kinds of modifications that are likely to be requested, and protect ourselves from those. Thus, we design our classes to be closed to the most likely kinds of changes that we can forsee. This means that the closure of a class is partial, and specifically targeted at certain kinds of changes. The principle of common closure states that, given a particular kind of change, either all of the classes within a category are closed to it, or they are all open to it. When this principle can be achieved, changes do not propogate through the system. Rather they focus upon one, or a few categories, while the rest of the categories remain unaffected. This greatly lessens the number of categories that are affected by a change, and reduces the frequency with which categories must be released. -------------------------------------------------------------------- 6. Classes within a released componen should be reused together. That is, it is impossible to separate the components from each other in order to reuse less than the total. -------------------------------------------------------------------- When a user decides to reuse a component, he creates a dependency upon the whole component. When a new release of that component is created, the user must reintroduce that new release into his existing applications. Therefore, the user will want each component to be as focused as possible. If the component contains classes which the user does not make use of, then the user may be placed in the position of accepting the burden of a new release which does not affect any of the classes within the component that he is using. Thus, comoponents should be kept relatively narrow. If the user uses one of the classes in the component, then it should be extremely likely that he should reuse all the other classes in the component. Generally, this kind of focus is enforced by the relationships that exist between classes that are reused together. Those relatioships represent dependencies that force the user to bring along all the classes within a component. It is important that dependencies that are associated with reuse be encapsulated within a single component where possible. If the reuse of a particular class within a component forces the reuse of other components, then the user has a bigger reuse burden. -------------------------------------------------------------------- 7. The dependency structure for released components must be a DAG. There can be no cycles. -------------------------------------------------------------------- Consider an application that is divided up into many different class categories. A given class category can be released, only when it is compatible with those categories that it depends upon. This ability to release an application in peices facilitates development. If applications cannot be released in pieces, then the developers interfere with each other as they are making changes to their code. The typical horror story is of an engineer who works all day to get his stuff working. He goes home a night satisfied. However upon returning the next morning, he is dismayed to find that his stuff no longer works. The reason: Sombody stayed later than he did and changed something that he depended upon. If this happened only once in a while, it would not be so bad, but in large projects it can happen on a daily (even hourly) basis. Thus we need a way to release the application in pieces so that developers can choose to depend upon older releases of the categories that they depend upon. However, when there are dependency cycles, then this rational scheme for developing applications breaks down. All of the categories within the cycle must be released simultaneously. Consider the following diagram: +---------------+ | Control Panel | +---------------+ / \ +-----------+ +---------+ | Transport | | Revenue | +-----------+ +---------+ / \ \ +----------+ +----------+ +----------+ | Elevator | | Conveyor | | Database | +----------+ +----------+ +----------+ \ / +-------+ | Alarm | +-------+ The developers that are working in the 'Transport' category choose to work with older, more stable, versions of Elevator and Conveyor, which in turn depend upon an old version of Alarm. When Transport wants to make a release, they simply test their category with the appopriate releases of the dependent categories, and then release it. However, if Alarm called a function that belonged to 'Control Panel', thus creating a cyclic dependency, then Transport could not work without old and stable versions of Elevator and Conveyor, which could not work without a stable verion of Alarm, which could not work without a stable version of Control Panel, which needs a stable version of Transport. Since we are trying to make changes to Transport, all of the other categories are suddenly invalidated, and the entire cycle must be released at the same time. Thus, cycles in the dependency structure interfere with the ability to release an application in pieces. However there is even more harm that they can do. Consider again the diagram above. The developers working in 'Elevators' want to run some unit tests. They must link with Alarm. However, if we assume that the same cycle exists, (i.e. Alarm calls a function in Control Panel) then the unit test must link with Control Panel, which must link with Revenue, which must link with the Database. And the database doesn't work. The developers of 'Elevator' are really angry. They don't need the database. They don't want to link with the database. The database code is 14 megabytes and takes 45 minutes to link in; so why do they have to use it? The 'Elevator' developers go find the developer in 'Alarm' who created the cyclic dependency upon 'Control Panel' and take him out back for a little lesson in software engineering. *** How can the cycle be broken. Simple. Whenever there is a cycle in the dependency graph of class categories, that cycle can be broken by splitting one of the categories into two. In this case, we could split the 'Control Panel' category into two categories as shown below: +---------------+ --------| Control Panel | / +---------------+ / / \ / +-----------+ +---------+ / | Transport | | Revenue | | +-----------+ +---------+ | / \ \ | +----------+ +----------+ +----------+ | | Elevator | | Conveyor | | Database | | +----------+ +----------+ +----------+ | \ / | +-------+ | | Alarm | \ +-------+ \ / +---------------+ | Control Panel | | Utilities | +---------------+ The new category 'Control Panel Utilities' contains the function that Alarm wants to call. -------------------------------------------------------------------- 8. Dependencies between released categories must run in the direction of stability. The dependee must be more stable than the depender. -------------------------------------------------------------------- One could view this as a axiom, rather than a principle, since it is impossible for a category to be more stable than the categories that it depends upon. When a category changes it always affects the dependent categories (even if for nothing more than a retest/revalidation). However the principle is meant as a guide to designers. Never cause a category to depend upon less stable categories. What is stability? The probable change rate. A category that is likely to undergo frequent changes is instable. A category that will change infrequently, if at all, is stable. There is an indirect method for measuring stability. It employs the axiomatic nature of this principle. Stability can be measured as a ratio of the couplings to classes outside the category. A category which many other categories depend upon is inherently stable. The reason is that such a category is difficult to change. Changing it causes all the dependent categories to change. On the other hand, a category which depends on many other categories is instable, since it must be changed whenever any of the categories it depends upon change. A category which has many dependents, but no dependees is ultimately stable since it has lots of reason not to change and no reason to change. (This ignores the categories intrinsic need to change based upon bugs and feature drift). A category that depends upon many categories but has no dependents is ultimately instable since it has no reason not to change, and is subject to all the changes coming from the categories it depends upon. So this notion of stability is positional rather than absolute. It measures stability in terms of a category's position in the dependency hierarchy. It says nothing about the subjective reasons that a category might need changing, and focuses only upon the objective, physical reasons that facilitate or constrain changes. To calculate the Instability of a category (I) count the number of classes, outside of the category, that depend upon classes within the category. Call this number Ca. Now count the number of classes outside the category that classes within the category depend upon. Call this number Ce. I = Ce / (Ca + Ce). This metric ranges from 0 to 1, where 0 is ultimately stable, and 1 is ultimately instable. In a dependency hierarchy, wherever a category with a low I value depends upon a category with a high I value, the dependent category will be subject to the higher rate of change of the category that it depends upon. That is, the category with the high I metric acts as a collector for all the changes below it, and funnels those changes up to the category with the low I metric. Said another way, a low I metric indicates that there are many relatively many dependents. We don't want these dependents to be subject to high rates of change. Thus, if at all possible, categories should be arranged such that the categories with high I metrics should depend upon the categories with low I metrics. -------------------------------------------------------------------- 9. The more stable a class category is, the more it must consist of abstract classes. A completely stable category should consist of nothing but abstract classes. -------------------------------------------------------------------- The stability being referred to in this principle, is again the I metric which is based upon "positional" stability. That is, a module that is in a highly stable position in the dependency graph should also be highly abstract. The justification for this principle is based upon the idea that executable code (The implementations of methods) changes more often than the interfaces between modules. Therefore interfaces have more intrinsic stability than executable code. In C++, it is more likely that you will change a .cc file than a .h file. In some languages, the division between implementation and interface is very clear cut. But in others (like C++) it is blurred. The stability of the .h file is not as great as it could be because private functions and private variables are likely to need changing as the implementation evolves. In such languages, the maximum stability comes from pure interfaces. A class that contains pure interfaces is an abstract class. Thus, we can measure the abstraction of a category by computing the ratio of abstract classes to total classes: A = (# abstract classes) / (# of classes). For example, if a class category contains 10 classes, of which 6 are abstract then A = .6. A will always range from [0,1]. Principle 9 says that categories that are placed into positions of stability ought to be abstract. The reason for this that the higher the abstraction of the category, the higher its intrinsic stability. And since intrinsic stability is limited by positional stability, intrinsically stable modules should also have positional stability. Consider a category with very low abstraction. Such a category has mostly concrete classes in it. If it is put in a place of stability, then many other categories will depend upon it. And thus, it will be difficult to change its implementation. On the other hand, consider a category with very high abstraction. Such a category consists mostly of abstract classes. If we put this category into a position of very low stability, then very few other categories will depend upon it. And then, of what use is the abstract interface? But there is another reason to put abstractions in positions of stability. And that reason goes back to principle #1; the open closed principle. We want concrete categories to depend upon abstract categories so that the derivatives of those abstract categories can be controlled by the concrete categories. This is reuse. The concrete categories can be reused with different derivatives of the abstract categories. Also, when the implementations of those derivative categories change, the abstract category is not affected, and so the concrete categories that depend upon it are also unaffected. Thus, the concrete categories are open to be extended, but do not need to be modified in order to achieve that extension. The open closed principle leads us to the realization that we do not want all of our categories to be stable. Stability is inflexibility. A stable category is difficult to change. And we do not want our applications to be difficult to change. But the open closed principle allows that a module that is highly stable (closed for modification) can also be open for extension. Yet this can only happen if the extension takes place in a derivative of the stable abstraction. Thus, in the ideal world our models would consist of two kinds of categories. Completely abstract and stable categories that are depended upon by completely concrete and instable categories. We are now in a position to define the relationship between stability (I) and abstractness (A). We can create a graph with A on the vertical axis and I on the horizontal axis. If we plot the two "good" kinds of categories on this graph, we will find the categories that are maximally stable and abstract at the upper left at (0,1). The categories that are maximally instable and concrete are at the lower right at (1,0). But not all categories can fall into one of these two positions. Categories have degrees of abstraction and stability. For example, it is very common that one abstract class derives from another abstract class. The derivative is an abstraction that has a dependency. Thus, though it is maximally abstract, it will not be maximally stable. Its dependency will decrease its stability. Consider a category with A=0 and I=0. This is a highly stable and concrete category. Such a category is not desirable because it is rigid. It cannot be extended because it is not abstract. And it is very difficult to change because of its stability. Consider a category with A=1 and I=1. This category is also undesirable (perhaps impossible) because it is maximally abstract and yet has no dependents. It, too, is rigid because the abstractions are impossible to extend. But what about a category with A=.5 and I=.5? This category is partially extensible because it is partially abstract. Moreover, it is partially stable so that the extensions are not subject to maximal instability. Such a category seems "balanced". Its stability is in balance with its abstractness. Consider again the A-I graph (below). We can draw a line from (0,1) to (1,0). This line represents categories whose abstractness is "balanced" with stability. Because of its similarity to a graph used in astronomy, I call this line the "Main Sequence". | | 1= (0,1) |\ | \ Abstractness| \ The | \ Main | \ Sequence | \ | \ | \ (1,0) +--------:-- 1 Instability A category that sits on the main sequence is not "too abstract" for its stability, nor is "too instable" for its abstractness. It has the "right" number of concrete and abstract classes in proportion to its positional stability. Clearly, the most desirable positions for a category to hold are at one of the two endpoints of the main sequence. However, in my experience only about half the categories in a project can have such ideal characteristics. Those other categories have the best characteristics if they are on or close to the main sequence. This leads us to another metric. If it is desirable for categories to be on or close to the main sequence, we can create a metric which measures how far away a category is from this ideal. D : Distance : |(A+I-1)/root2| : The perpendicular distance of a category from the main sequence. This metric ranges from [0,~0.707]. (One can normalize this metric to range between [0,1] by using the simpler form |(A+I-1)|. I call this metric D'). Given this metric, a design can be analyzed for its overall conformance to the main sequence. The D metric for each category can be calculated. Any category that has a D value that is not near zero can be reexamined and restructured. Statistical analysis of a design is also possible. One can calculate the mean and variance of all the D metrics within a design. One would expect a conformant design to have a mean and variance which were close to zero. The variance can be used to establish "control limits" which can identify categories that are "exceptional" in comparison to all the others.