Concepts and constructs of object-oriented programming (OOP). Selected
articles and excerpts from MSDN, Wikipedia, Wikibooks, works of B. Stroustrup.
Concepts and constructs of object-oriented programming (OOP) Selected articles and excerpts from MSDN, Wikipedia, Wikibooks, works of B. Stroustrup and Internet sources. If not explicitly stated as copyrighted, materials used in this work are from public domain.
Compiled and edited by Sergey Chepurin, December, 2011
1
Contents Introduction .........................................................................................................................4 What is "OOP" and what's so great about it? ...............................................................5 1. Polymorphism ..................................................................................................................8 2. Encapsulation ..................................................................................................................11 Friends ...........................................................................................................................11 3. Interfaces .........................................................................................................................12 When to use interfaces ...................................................................................................12 4. Inheritance .......................................................................................................................14 Do we really need multiple inheritance? .......................................................................14 When to use inheritance .................................................................................................14 Liskov Substitution Principle .........................................................................................19 5. Composition .....................................................................................................................20 6. Nested classes ..................................................................................................................21 7. Templates .........................................................................................................................26 8. Virtual functions ..............................................................................................................29 9. Exception safety: concepts and techniques .....................................................................31 Exception safety .............................................................................................................31 Exception-safe implementation techniques ....................................................................32 Resources and resource leaks ........................................................................................33 Class invariants .............................................................................................................34 Writing exception safe code ...........................................................................................37 Resource Aquisition is Initialisation (RAII)...................................................................38 ScopeGuard....................................................................................................................39 Summary ........................................................................................................................40 10. Design principles ...........................................................................................................42 Principles of Object Oriented Class Design ..................................................................42 The Open Closed Principle (OCP) ......................................................................................... 42 Dynamic polymorphism........................................................................................................ 42 Static polymorphism ............................................................................................................. 44 Architectural goals of the OCP ............................................................................................ 45 The Dependency Inversion Principle (DIP) ........................................................................... 45 Depending upon abstractions ............................................................................................... 45 Object creation ..................................................................................................................... 45 The Interface Segregation Principle (ISP) ............................................................................. 46 What does client specific mean?........................................................................................... 46 Changing interfaces ............................................................................................................. 46
Principles of Package Architecture ...............................................................................47 The Release Reuse Equivalency Principle (REP)................................................................... 47 The Common Closure Principle (CCP)................................................................................ 47 The Common Reuse Principle (CRP) ................................................................................... 47 Tension between the Package Cohesion Principles ............................................................. 48 The Package Coupling Principles .......................................................................................... 48 The Acyclic Dependencies Principle (ADP)......................................................................... 48 The Stable Dependencies Principle (SDP) ........................................................................... 48 The Stable Abstractions Principle (SAP) ............................................................................. 48
11. Design patterns ..............................................................................................................49 Creational patterns ........................................................................................................50 Builder .................................................................................................................................... 50 Factory ................................................................................................................................... 53 Abstract Factory ................................................................................................................... 53 Factory Method .................................................................................................................... 55 Prototype ................................................................................................................................ 58 Singleton ................................................................................................................................. 62
2
Rules of thumb for a pattern to apply ..................................................................................... 64
Structural patterns .........................................................................................................65 Bridge ..................................................................................................................................... 65
Behavioral patterns ........................................................................................................67 Template Method .................................................................................................................... 67
Software based on Design Patterns ...............................................................................70 References ............................................................................................................................72
3
Introduction The key to maintainable, efficient, and evolvable programs isn’t particular language features. It is the ability to develop concepts needed for a solution and to express them clearly in a program. Language features exist to make such expression simple and direct. Bjarne Stroustrup "Why C++ is not just an object-oriented programming language"
Introductory note: Take into account that the concepts and constructs of object-oriented programming (OOP) described in this document are common for all object-oriented languages, but some may have slightly different meaning or more broad definition. The information provided mostly with consideration of C++ with other languages (Java, C#, VB.NET) serving for comparison, or because they have better examples. This is done not because C++ is a "better language", but simply trying to avoid messing up different implementations of common OOP concepts and structures. (Though, there is also included for clarity an MSDN article on interface implementation in VB.NET in separate code block using statement Interface and keyword Implements.) The main OOP concepts (also called "design ideals") – based on Bjarne Stroustrup's article What's so great about classes?: •
Abstraction – providing some form of classes and objects. Generally, the ability to represent concepts directly in a program and hide incidental details behind well defined interfaces – is the key to every flexible and comprehensible system of any significant size.
•
Polymorphism - providing a single interface to entities of different types. Virtual functions provide dynamic (run-time) polymorphism through an interface provided by a base class. Overloaded functions and templates provide static (compiletime) polymorphism.
•
Encapsulation - the enforcement of abstraction by mechanisms that prevent access to implementation details of an object or a group of objects except through a well-defined interface. C++ enforces encapsulation of private and proteced members of a class as long as users do not violate the type system using casts. (Wikipedia) Hiding the internals of the object protects its integrity by preventing users from setting the internal data of the component into an invalid or inconsistent state. A benefit of encapsulation is that it can reduce system complexity, and thus increases robustness, by allowing the developer to limit the interdependencies between software components. Almost always, there is a way to override such protection - usually via reflection API (Ruby, Java, C#, etc.), or by special keyword usage like friend in C++.
•
Inheritance - (Wikipedia) a way to reuse code of existing objects, establish a subtype from an existing object, or both, depending upon programming language support. In classical inheritance where objects are defined by classes, classes can inherit attributes 4
and behavior (i.e., previously coded algorithms associated with a class) from preexisting classes called base classes. The new classes are known as derived classes. The relationships of classes through inheritance gives rise to a hierarchy. •
Generic programming (genericity by B. Stroustrup) – the ability to parameterize types and functions by types and values. Essential for expressing typesafe containers and a powerful tool for expressing general algorithms.
Though placed among main OOP concepts, inheritance is not defined as such by Stroustrup himself in his Glossary. Generally, this is a way (code constructs - a derived class is said to inherit the members of its base classes) provided by the language to reuse the existing objects and their attributes and behavior – i.e. to provide polymorphism. But hierarchy built on inheritance is an abstract concept. The language concepts differ from the techniques or code constructs by the level of abstraction (in general meaning of abstraction). If you want to distinguish the general concept from the mechanism or construct provided by the language, ask yourself "what for?" the term in question is used. For example, inheritance has no abstract sense by itself, but only when applied to reuse existing objects and their attributes. Friends have no abstract meaning whatsoever but only when applied to provide some kind of encapsulation. Interface in C++ has a concrete implementation combining class members with public access and friends. The constructs and mechanisms used to realize the OOP concepts: • Inheritance -> polymorphism; • Interface -> encapsulation in C++; • Templates –> static (compile-time) polymorphism, generic programming in C++; • Virtual functions –> dynamic (run-time) polymorphism through an interface provided by a base class C++; • Abstract classes –> encapsulation (the functionality of interface in other languages), polymorphism in C++. Abstract class - a class defining an interface only; used as a base class. Use of abstract classes is one of the most effective ways of minimizing the impact of changes in a C++ program and for minimizing compilation time (B.Stroustrup's Glossary); A type that is not abstract is called a concrete type1 ; • Overloaded functions –> static (compile-time) polymorphism in C++; • Composition -> polymorphism; • Nested classes -> encapsulation, polymorphism; • Friends -> encapsulation in C++.
What is "OOP" and what's so great about it? There are lots of definitions of "object oriented", "object-oriented programming", and "objectoriented programming languages". For a longish explanation of what I think of as "object oriented", read Why C++ isn't just an object-oriented programming language. That said, object-oriented programming is a style of programming originating with Simula (about 40 years ago!) relying of encapsulation, inheritance, and polymorphism. In the context of C++ (and many other languages with their roots in Simula), it means programming using class hierarchies and virtual functions to allow manipulation of objects of a variety of types through well-defined interfaces and to allow a program to be extended incrementally through derivation. 1
A concrete type is a type without virtual functions, so that objects of the type can be allocated on the stack and manipulated directly (without a need to use pointers or references to allow the possibility for derived classes). Often, small self-contained classes. [21]
5
See What's so great about classes? for an idea about what great about "plain classes". The point about arranging classes into a class hierarchy is to express hierarchical relationships among classes and use those relationships to simplify code. To really understand OOP, look for some examples. For example, you might have two (or more) device drivers with a common interface: class Driver { // common driver interface public: virtual int read(char* p, int n) = 0;//read max n characters from device to p // return the number of characters read virtual bool reset() = 0; // reset device virtual Status check() = 0;}; // read status
This Driver is simply an interface. It is defined with no data members and a set of pure virtual functions. A Driver can be used through this interface and many different kinds of drivers can implement this interface: class Driver1 : public Driver { // a driver public: Driver1(Register); // constructor int read(char*, int n); bool reset(); Status check(); private: // implementation details, incl. representation }; class Driver2 : public Driver { // another driver public: Driver2(Register); int read(char*, int n); bool reset(); Status check(); private: // implementation details, incl., representation };
Note that these drivers hold data (state) and objects of them can be created. They implement the functions defined in Driver. We can imagine a driver being used like this: void f(Driver& d) // use driver { Status old_status = d.check(); // ... d.reset(); char buf[512]; int x = d.read(buf,512); // ... }
The key point here is that f()doesn't need to know which kind of driver it uses; all it needs to know is that it is passed a Driver; that is, an interface to many different kinds of drivers. We could invoke f() like this: void g() { Driver1 d1(Register(0xf00)); Driver2 d2(Register(0xa00));
// // // //
// ... int dev; cin >> dev; if
(dev==1)
6
create a Driver1 for with device register create a Driver2 for with device register
device at address 0xf00 device at address 0xa00
f(d1);
// use d1
f(d2);
// use d2
else // ... }
Note that when f() uses a Driver the right kind of operations are implicitly chosen at run time. For example, when f() is passed d1, d.read() uses Driver1::read(), whereas when f() is passed d2, d.read()uses Driver2::read(). This is sometimes called runtime dispatch or dynamic dispatch. In this case there is no way that f() could know the kind of device it is called with because we choose it based on an input. Please note that object-oriented programming is not a panacea. "OOP" does not simply mean "good" - if there are no inherent hierarchical relationships among the fundamental concepts in your problem then no amount of hierarchy and virtual functions will improve your code. The strength of OOP is that there are many problems that can be usefully expressed using class hierarchies - the main weakness of OOP is that too many people try to force too many problems into a hierarchical mould. Not every program should be object-oriented. As alternatives, consider plain classes, generic programming, and free-standing functions (as in math, C, and Fortran). (From http://www2.research.att.com/~bs/bs_faq.html)
7
1. Polymorphism C++ In C++ - polymorphism is providing a single interface to entities of different types. Virtual functions provide dynamic (run-time) polymorphism through an interface provided by a base class. Overloaded functions and templates provide static (compile-time) polymorphism. (From B. Stroustrup's Glossary http://www.research.att.com/~bs/glossary.html) Example (http://www.java2s.com/Tutorial/Cpp/0180__Class/Usevirtualfunctionsandpolymorphism.ht m): Using virtual functions and polymorphism #include #include using namespace std; class Shape { double width; double height; char name[20]; public: Shape() { width = height = 0.0; strcpy(name, "unknown"); } Shape(double w, double h, char *n) { width = w; height = h; strcpy(name, n); } Shape(double x, char *n) { width = height = x; strcpy(name, n); } void display() { cout AddFriend(GetName(), newFriend.GetName()); // Add the new friend to local cache friends_.push_back(&newFriend); }
37
The problem with this code is that, if vector::push_back() throws, the database will have a new item, but the local cache will not. The system becomes inconsistent. If the database function is known to not throw, the problem can be fixed by simply reordering the operations. void User::AddFriend(User& newFriend) { friends_.push_back(&newFriend); pDB_->AddFriend(GetName(), newFriend.GetName()); }
Now, if vector::push_back() throws, the system is left in the same state as it was before User::AddFriend() was called. Suppose, however, that UserDatabase::AddFriend() may also throw. The function can be made exception safe using a try-catch block. void User::AddFriend(User& newFriend) { friends_.push_back(&newFriend); try { pDB_->AddFriend(GetName(), newFriend.GetName()); } catch (...) { friends_.pop_back(); throw; } }
Resource Aquisition is Initialisation (RAII) Resource aquisition is initialisation (RAII) [7] is a fundamental design idiom for exception safe code. It is a technique whereby constructors and destructors are used to automate release of resources: a resource is acquired in the constructor and released in the destructor. C++ stack-unwinding semantics ensure that the resource is automatically released in the event of an exception. For example, given a class Mutex, used to lock shared objects in multithreaded code, a class ScopedLock is devised to manage the locking and unlocking operations. class ScopedLock { public: explicit ScopedLock (Mutex& m) : mutex(m) { mutex.lock(); locked = true; } ~ScopedLock () { if (locked) mutex.unlock(); } void unlock() { locked = false; mutex.unlock(); } private: ScopedLock (const ScopedLock&); ScopedLock& operator= (const ScopedLock&); Mutex& mutex; bool locked; };
The mutex is locked when the ScopedLock object is created, and unlocked either by an explicit unlock() or implicitly when the object goes out-of-scope. { ScopedLock locker(mtx); // mtx is locked } // mtx automatically unlocked
RAII is not just for exception-safety. It also protects against programmer error by ensuring symmetry between acquire-release operations; the programmer no longer has to remember to release resources. Common resource operations are obtaining-releasing dynamic memory, locking and unlocking mutexes, opening-closing files, registering-deregistering with services, etc. Dynamic memory management is arguably the most prominent of these, and the standard 38
C++ library provides the auto_ptr class as an RAII class for memory. { std::auto_ptr m(new MyClass); m->do_something(); } // memory released
The Boost C++ library provides a number of more advanced forms of memory handle classes, such as smart_ptr. When implementing RAII classes, one must ensure that constructors perform their own resource cleanup given an exception constructors should not leak resources. It is also important that destructors do not throw. Exceptions do not nest, and if a destructor throws an exception during stack-unwinding from another exception, the program will abort.
ScopeGuard Proposed by Andrei Alexandrescu and Petru Marginean in the article "Generic: Change the Way You Write Exception-Safe Code - Forever", 2000 (http://drdobbs.com/cpp/184403758). Prerequisites:
"Writing correct code in the presence of exceptions is a not an easy task. Exceptions establish a separate control flow that has little to do with the main control flow of the application. Figuring out the exception flow requires a different way of thinking, as well as new tools." Lets apply all exception handling techniques to the member function User::AddFriend described above. 1. Using try-catch The code works, but at the cost of increased size and clumsiness. The two-liner just became a ten-liner. This technique isn't appealing; imagine littering all of your code with such try-catch statements. Moreover, this technique doesn't scale well. Imagine you have a third operation to do. In that case, things suddenly become much clumsier. You can choose between equally awkward solutions: nested try statements or a more complicated control flow featuring additional flags. These solutions raise code bloating issues, efficiency issues, and, most important, severe understandability and maintenance issues. 2. Using RAII This approach works just fine, but in the real world, it turns out not to be that neat. You must write a bunch of little classes to support this idiom. Extra classes mean extra code to write, intellectual overhead, and additional entries to your class browser. Moreover, it turns out there are lots of places where you must deal with exception safety. Let's face it, adding a new class every so often just for undoing an arbitrary operation in its destructor is not the most productive. 3. Using ScopeGuard technique (From "C++ Exceptions" ch.6, by Tim Bailey, 2006, http://www-personal.acfr.usyd.edu.au/tbailey/seminars/) Writing small handle classes for every type of resource can be tedious and can introduce new bugs of their own (e.g., in their constructor, destructor, copy-constructor, etc). A recent 39
article [11] presents a generic class to overcome this problem and, particularly, to simplify writing exception-safe resource management. The class is called ScopeGuard and is basically a generic implementation of RAII. As such, it may be used to perform normal automatic release operations. For example, { void *buffer = std::malloc(1024); ScopeGuard freeIt = MakeGuard(std::free, buffer); FILE *fp = std::fopen("afile.txt"); ScopeGuard closeIt = MakeGuard(std::fclose, fp); // ... } // file closed and memory freed
However, ScopeGuard also extends the RAII concept; it provides a Dismiss() operation. This is a significant innovation, greatly enhancing its value as an exception tool. The Dismiss() operation permits roll-back procedures to be invoked in the event of an exception, so as to meet exception-safe guarantees, and all without resorting to try-catch blocks. A rollback function is registered with the ScopeGuard object before performing an unsafe operation. If the operation succeeds, the roll-back function is dismissed but, if an exception is thrown before reaching Dismiss(), roll-back is invoked. Resuming the database example from the previous section, ScopeGuard ensures the strong guarantee as follows. void User::AddFriend(User& newFriend) { friends_.push_back(&newFriend); ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back); pDB_->AddFriend(GetName(), newFriend.GetName()); guard.Dismiss(); }
If no exception is thrown, Dismiss()is called at the end of the block and UserCont::pop_back() is not invoked. However, if UserDatabase::AddFriend() throws, Dismiss() is never called, and UserCont::pop_back() is called from guard's destructor. Editor's note: The problem with ScopeGuard technique is that it introduces complexity of its own. Now you would have to write a class ScopeGuard consisting of members used to handle almost every function that can throw (to roll-out the operation if exception is thrown). And this class can be a very large one. (See for example templated ScopeGuard class [22].)
Summary (From "Exception Safety: Concepts and Techniques", ch.3.4, by B. Stroustrup, 2001 http://www2.research.att.com/~bs/papers.html) The approach of gaining exception safety through ordering and the "resource acquisition is initialization" technique tends to be more elegant and more efficient than explicitly handling errors using try-blocks. More problems with exception safety arise from a programmer ordering code in unfortunate ways than from lack of specific exception-handling code. The basic rule of ordering is not to destroy information before its replacement has been constructed and can be assigned without the possibility of an exception. Exceptions introduce possibilities for surprises in the form of unexpected control flows. For a piece of code with a simple local control flow, such as the operator=(),safe_assign(), and push_back() examples, the opportunities for surprises are limited. It is relatively simple
40
to look at such code and ask oneself "can this line of code throw an exception, and what happens if it does?" For large functions with complicated control structures, such as complicated conditional statements and nested loops, this can be hard. Adding try-blocks increases this local control structure complexity and can therefore be a source of confusion and errors. I conjecture that the effectiveness of the ordering approach and the "resource acquisition is initialization" approach compared to more extensive use of try-blocks stems from the simplification of the local control flow. Simple, stylized code is easier to understand and easier to get right. (From "Exception Safety: Concepts and Techniques", ch. 4, by B. Stroustrup, 2001 http://www2.research.att.com/~bs/papers.html) When writing new code, it is possible to take a more systematic approach and make sure that every resource is represented by a class with an invariant that provides the basic guarantee. Given that, it becomes feasible to identify the critical objects in an application and provide roll-back semantics (that is, the strong guarantee – possibly under some specific conditions) for operations on such objects. The basic techniques for dealing with exceptions, focusing on resources and invariants, also help getting code correct and efficient. In general, keeping code stylish and simple by using classes to represent resources and concepts makes the code easier to understand, easier to maintain, and easier to reason about. Constructors, destructors, and the support for correct partial construction and destruction are the language-level keys to this. "Resource acquisition is initialization" is the key programming technique to utilize these language features. Most applications contain data structures and code that are not written with exception safety in mind. Where necessary, such code can be fitted into an exception-safe framework by either verifying that it doesn’t throw exception (as was the case for the C standard library) or through the use of interface classes for which the exception behavior and resource management can be precisely specified. Editor's note: In C++ world exist different approaches to handling of errors and exceptions. Mostly, it means using error codes and assertions instead of complex techniques of exception handling. Notable example is Google coding style [24]. Also, exception handling is rarely (if ever) used in real-time embedded systems (usually replaced there by error codes). For details see "Abstraction and the C++ machine model", by B. Stroustrup, 2004 (http://www2.research.att.com/~bs/). On the obvious question "Why there is no automatic garbage collection in C++?", see "Evolving a language in and for the real world: C++ 1991-2006", by B. Stroustrup, 2006, ch. 5.4 "Automatic Garbage Collection" [12].
41
10. Design principles Knowing all that was said above, you can write freeware or applications with low commercial potential from small to middle size. You can manage such a project by yourself without or with little trouble when in need to recompile, to update, or to extend the code. The problem arises when the team of programmers is hired to write some large commercial product, later to maintain and support it with future updates and with possible extension in mind. Recompilations can take from hours to nights, code is difficult to maintain, errors are hard to detect because you don't know exactly in what module it was generated, updates can take weeks or longer. And worse of all you are never sure if deadline would be met. That is when Design Patterns based on Design Principles can help. (MSDN, http://msdn.microsoft.com/en-us/library/ee817670.aspx) Design patterns are very useful software design concepts that allow teams to focus on delivering the very best type of applications, whatever they may be. The key is to make proper and effective use of design patterns.
Principles of Object Oriented Class Design Excerpts from "Design Principles and Design Patterns", by Robert C. Martin.
The Open Closed Principle (OCP) A module should be open for extension but closed for modification. A module should be open for extension but closed for modification. Of all the principles of object oriented design, this is the most important. It originated from the work of Bertrand Meyer. It means simply this: "We should write our modules so that they can be extended, without requiring them to be modified. In other words, we want to be able to change what the modules do, without changing the source code of the modules." This may sound contradictory, but there are several techniques for achieving the OCP on a large scale. All of these techniques are based upon abstraction. Indeed, abstraction is the key to the OCP. Several of these techniques are described below.
Dynamic polymorphism Consider Listing 2-1. the LogOn function must be changed every time a new kind of modem is added to the software. Worse, since each different type of modem depends upon the Modem::Type enumeration, each modem must be recompiled every time a new kind of modem is added. Listing 2-1 Logon, must be modified to be extended. struct Modem { enum Type {hayes, courrier, ernie) type; }; struct Hayes
42
{ Modem::Type type; // Hayes related stuff }; struct Courier { Modem::Type type; // Courier related stuff }; struct Ernie { Modem::Type type; // Ernie related stuff }; void LogOn(Modem& m,string& pno, string& user, string& pw) { if (m.type == Modem::hayes) DialHayes((Hayes&)m, pno); else if (m.type == Modem::courrier) DialCourier((Courier&)m, pno); else if (m.type == Modem::ernie) DialErnie((Ernie&)m, pno) // ...you get the idea }
Of course this is not the worst attribute of this kind of design. Programs that are designed this way tend to be littered with similar if/else or switch statement. Every time anything needs to be done to the modem, a switch statement if/else chain will need to select the proper functions to use. When new modems are added, or modem policy changes, the code must be scanned for all these selection statements, and each must be appropriately modified. Worse, programmers may use local optimizations that hide the structure of the selection statements. For example, it might be that the function is exactly the same for Hayes and Courier modems. Thus we might see code like this: if (modem.type == Modem::ernie) SendErnie((Ernie&)modem, c); else SendHayes((Hayes&)modem, c);
Clearly, such structures make the system much harder to maintain, and are very prone to error. As an example of the OCP, consider Figure 2-13. Here the LogOn function depends only upon the Modem interface. Additional modems will not cause the LogOn function to change. Thus, we have created a module that can be extended, with new modems, without requiring modification. See Listing 2-2.
43
Figure 2-13
Listing 2-2: class Modem { public: virtual void Dial(const string& pno) = 0; virtual void Send(char) = 0; virtual char Recv() = 0; virtual void Hangup() = 0; }; void LogOn(Modem& m,string& pno, string& user, string& pw) { m.Dial(pno); // you get the idea. }
Logon is closed for modification through static polymorphism.
Static polymorphism Another technique for conforming to the OCP is through the use of templates or generics. Listing 2-3 shows how this is done. The LogOn function can be extended with many different types of modems without requiring modification. Listing 2-3 template void LogOn(MODEM& m, string& pno, string& user, string& pw) { m.Dial(pno); // you get the idea. }
44
Architectural goals of the OCP By using these techniques to conform to the OCP, we can create modules that are extensible, without being changed. This means that, with a little forethought, we can add new features to existing code, without changing the existing code and by only adding new code. This is an ideal that can be difficult to achieve, but you will see it achieved, several times, in the case studies later on in this book. Even if the OCP cannot be fully achieved, even partial OCP compliance can make dramatic improvements in the structure of an application. It is always better if changes do not propagate into existing code that already works. If you don’t have to change working code, you aren’t likely to break it.
The Dependency Inversion Principle (DIP) Depend upon Abstractions. Do not depend upon concretions. If the OCP states the goal of OO architecture, the DIP states the primary mechanism. Dependency Inversion is the strategy of depending upon interfaces or abstract functions and classes, rather than upon concrete functions and classes. This principle is the enabling force behind component design, COM, CORBA, EJB, etc. Procedural designs exhibit a particular kind of dependency structure. As Figure 2-17 shows, this structure starts at the top and points down towards details. High level modules depend upon lower level modules, which depend upon yet lower level modules, etc.. A little thought should expose this dependency structure as intrinsically weak. The high level modules deal with the high level policies of the application. These policies generally care little about the details that implement them. Why then, must these high level modules directly depend upon those implementation modules? An object oriented architecture shows a very different dependency structure, one in which the majority of dependencies point towards abstractions. Moreover, the modules that contain detailed implementation are no longer depended upon, rather they depend themselves upon abstractions. Thus the dependency upon them has been inverted. See Figure 2-18.
Depending upon abstractions The implication of this principle is quite simple. Every dependency in the design should target an interface, or an abstract class. No dependency should target a concrete class.
Object creation One of the most common places that designs depend upon concrete classes is when those designs create instances. By definition, you cannot create instances of abstract classes. Thus, to create an instance, you must depend upon a concrete class. Creation of instances can happen all through the architecture of the design. Thus, it might seem that there is no escape and that the entire architecture will be littered with dependencies upon concrete classes. However, there is an elegant solution to this problem named abstract factory - a design pattern that we’ll be examining in more detail towards the end of this chapter.
45
The Interface Segregation Principle (ISP) Many client specific interfaces are better than one general purpose interface The ISP is another one of the enabling technologies supporting component substrates such as COM. Without it, components and classes would be much less useful and portable. The essence of the principle is quite simple. If you have a class that has several clients, rather than loading the class with all the methods that the clients need, create specific interfaces for each client and multiply inherit them into the class.
What does client specific mean? The ISP does not recommend that every class that uses a service have its own special interface class that the service must inherit from. If that were the case, the service would depend upon each and every client in a bizarre and unhealthy way. Rather, clients should be categorized by their type, and interfaces for each type of client should be created. If two or more different client types need the same method, the method should be added to both of their interfaces. This is neither harmful nor confusing to the client.
Changing interfaces When object oriented applications are maintained, the interfaces to existing classes and components often change. There are times when these changes have a huge impact and force the recompilation and redeployment of a very large part of the design. This impact can be mitigated by adding new interfaces to existing objects, rather than changing the existing interface. Clients of the old interface that wish to access methods of the new interface, can query the object for that interface as shown in the following code.
Figure 2-20: Separated Interfaces void Client(Service* s) { if (NewService* ns = dynamic_cast(s)) { // use the new service interface } }
46
As with all principles, care must be taken not to overdo it. The specter of a class with hundreds of different interfaces, some segregated by client and other segregated by version, would be frightening indeed.
Principles of Package Architecture Classes are a necessary, but insufficient, means of organizing a design. The larger granularity of packages are needed to help bring order. But how do we choose which classes belong in which packages. Below are three principles known as the Package Cohesion Principles, that attempt to help the software architect.
The Release Reuse Equivalency Principle (REP) The granule of reuse is the granule of release. A reusable element, be it a component, a class, or a cluster of classes, cannot be reused unless it is managed by a release system of some kind. Users will be unwilling to use the element if they are forced to upgrade every time the author changes it. Thus. even though the author has released a new version of his reusable element, he must be willing to support and maintain older versions while his customers go about the slow business of getting ready to upgrade. Thus, clients will refuse to reuse an element unless the author promises to keep track of version numbers, and maintain old versions for awhile. Therefore, one criterion for grouping classes into packages is reuse. Since packages are the unit of release, they are also the unit of reuse. Therefore architects would do well to group reusable classes together into packages.
The Common Closure Principle (CCP) Classes that change together, belong together. A large development project is subdivided into a large network of interrelated packages. The work to manage, test, and release those packages is non-trivial. The more packages that change in any given release, the greater the work to rebuild, test, and deploy the release. Therefore we would like to minimize the number of packages that are changed in any given release cycle of the product. To achieve this, we group together classes that we think will change together. This requires a certain amount of prescience since we must anticipate the kinds of changes that are likely. Still, when we group classes that change together into the same packages, then the package impact from release to release will be minimized.
The Common Reuse Principle (CRP) Classes that aren’t reused together should not be grouped together. A dependency upon a package is a dependency upon everything within the package. When a package changes, and its release number is bumped, all clients of that package must verify that they work with the new package - even if nothing they used within the package actually changed. We frequently experience this when our OS vendor releases a new operating system. We have to upgrade sooner or later, because the vendor will not support the old version forever. So even though nothing of interest to us changed in the new release, we must go through the effort of upgrading and revalidating. The same can happen with packages if classes that are not used together are grouped together. Changes to a class that I don’t care about will still 47
force a new release of the package, and still cause me to go through the effort of upgrading and revalidating.
Tension between the Package Cohesion Principles These three principles are mutually exclusive. They cannot simultaneously be satisfied. That is because each principle benefits a different group of people. The REP and CRP makes life easy for reusers, whereas the CCP makes life easier for maintainers. The CCP strives to make packages as large as possible (after all, if all the classes live in just one package, then only one package will ever change). The CRP, however, tries to make packages very small. Fortunately, packages are not fixed in stone. Indeed, it is the nature of packages to shift and jitter during the course of development. Early in a project, architects may set up the package structure such that CCP dominates and development and maintenance is aided. Later, as the architecture stabilizes, the architects may refactor the package structure to maximize REP and CRP for the external reusers.
The Package Coupling Principles The next three packages govern the interrelationships between packages. Applications tend to be large networks of interrelated packages. The rules that govern these interrelationship are some of the most important rules in object oriented architecture.
The Acyclic Dependencies Principle (ADP) The dependencies between packages must not form cycles.
The Stable Dependencies Principle (SDP) Depend in the direction of stability.
The Stable Abstractions Principle (SAP) Stable packages should be abstract packages.
48
11. Design patterns MSDN, http://msdn.microsoft.com/en-us/library/ee817669.aspx Although not a magic bullet (if such a thing exists), design patterns are an extremely powerful tool for a developer or architect actively engaged in any development project. Design patterns ensure that common problems are addressed via well-known and accepted solutions. The fundamental strength of patterns rests with the fact that most problems have likely been encountered and solved by other individuals or development teams. As such, patterns provide a mechanism to share workable solutions between developers and organizations. Regardless of where these patterns find their genesis, patterns leverage this collective knowledge and experience. This ensures that correct code is developed more rapidly and reduces the chance that a mistake will occur in design or implementation. In addition, design patterns offer common semantics between members of an engineering team. As anyone who has been involved in a large-scale development project knows, having a common set of design terms and principles is critical to the successful completion of the project. Best of all, design patterns—if used judicially—can free up your time. From http://en.wikibooks.org/wiki/C%2B%2B_Programming/Code/Design_Patterns Software design patterns are abstractions that help structure system designs. While not new, since the concept was already described by Christopher Alexander in its architectural theories, it only gathered some traction in programming due to the publication of Design Patterns: Elements of Reusable Object-Oriented Software book in October 1994 by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, known as the Gang of Four (GoF), that identifies and describes 23 classic software design patterns. A design pattern is neither a static solution, nor is it an algorithm. A pattern is a way to describe and address by name (mostly a simplistic description of its goal), a repeatable solution or approach to a common design problem, that is, a common way to solve a generic problem (how generic or complex, depends on how restricted the target goal is). Patterns can emerge on their own or by design. This is why design patterns are useful as an abstraction over the implementation and a help at design stage. With this concept, an easier way to facilitate communication over a design choice as normalization technique is given so that every person can share the design concept. Depending on the design problem they address, design patterns can be classified in different categories, of which the main categories are: •
•
•
Creational Patterns Creational design patterns are design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation. Structural Patterns Structural design patterns are design patterns that ease the design by identifying a simple way to realize relationships between entities. Behavioral Patterns Behavioral design patterns are design patterns that identify common communication patterns between objects and realize these patterns. By doing so, these patterns increase flexibility in carrying out this communication.
49
Patterns are commonly found in objected-oriented programming languages like C++ or Java. They can be seen as a template for how to solve a problem that occurs in many different situations or applications. It is not code reuse, as it usually does not specify code, but code can be easily created from a design pattern. Object-oriented design patterns typically show relationships and interactions between classes or objects without specifying the final application classes or objects that are involved. Editor's note: You must take into account that 23 fundamental patterns mentioned above were created 17 years ago. These structures correspond to real projects existed or developed at that time, thus nowadays there are new patterns added. But the main three categories Creational, Structural and Behavioral - remain the same.
Creational patterns In software engineering, creational design patterns are design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation. As we will see there are several creational design patterns, and all will deal with a specific implementation task, that will create a higher level of abstraction to the code base, we will now cover each one. Some examples of creational design patterns include: •
Abstract factory pattern: centralize decision of what factory to instantiate
•
Factory method pattern: centralize creation of an object of a specific type choosing one of several implementations
•
Builder pattern: separate the construction of a complex object from its representation so that the same construction process can create different representations
•
Prototype pattern: used when the type of objects to create is determined by a prototypical instance, which is cloned to produce new objects
•
Singleton pattern: restrict instantiation of a class to one object
Builder The Builder Creational Pattern is used to separate the construction of a complex object from its representation so that the same construction process can create different objects representations. Problem We want to construct a complex object, however we do not want to have a complex constructor member or one that would need many arguments. Solution Define an intermediate object whose member functions define the desired object part by part before the object is available to the client. Build Pattern lets us defer the construction of the object until all the options for creation have been specified.
50
UML notation - Each class is represented by a box which is labeled with the class name. Inheritance between two classes is illustrated by a directed line drawn from the derived class to the base class. A line with a diamond shape at one end depicts composition (i.e., a class object is composed of one or more objects of another class). The number of objects contained by another object is depicted by a label (e.g., n). #include #include using namespace std; // "Product" class Pizza { public: void setDough(const string& dough) { m_dough = dough; } void setSauce(const string& sauce) { m_sauce = sauce; } void setTopping(const string& topping) { m_topping = topping; } void open() const { cout createNewPizzaProduct(); m_pizzaBuilder->buildDough(); m_pizzaBuilder->buildSauce(); m_pizzaBuilder->buildTopping(); } private: PizzaBuilder* m_pizzaBuilder; }; int main() { Cook cook; PizzaBuilder* hawaiianPizzaBuilder = new HawaiianPizzaBuilder; PizzaBuilder* spicyPizzaBuilder = new SpicyPizzaBuilder; cook.setPizzaBuilder(hawaiianPizzaBuilder); cook.constructPizza(); Pizza* hawaiian = cook.getPizza(); hawaiian->open(); cook.setPizzaBuilder(spicyPizzaBuilder); cook.constructPizza();
52
Pizza* spicy = cook.getPizza(); spicy->open(); delete delete delete delete
hawaiianPizzaBuilder; spicyPizzaBuilder; hawaiian; spicy;
}
Applicability (From "Design Patterns: Elements of Reusable Object-Oriented Software", 1994) Use the Builder pattern when • the algorithm for creating a complex object should be independent of the parts that make up the object and how they're assembled. • the construction process must allow different representations for the object that's constructed.
Factory A utility class that creates an instance of a class from a family of derived classes
Abstract Factory Definition: Abstract factory provides an interface for creating families of related or dependent objects without specifying their concrete classes. This is a utility class that creates an instance of several families of classes. It can also return a factory for a certain group. Example C++http://en.wikipedia.org/wiki/Abstract_factory_pattern: The output should be either "I'm a WinButton" or "I'm an OSXButton" depending on which kind of factory was used. Note that the Application has no idea what kind of GUIFactory it is given or even what kind of Button that factory creates.
53
/* GUIFactory example -- */ #include using namespace std; class Button { public: virtual void paint() = 0; virtual ~Button(){ } }; class WinButton: public Button { public: void paint() { cout paint(); delete button; delete factory; } }; GUIFactory * createOsSpecificFactory() { int sys; cout sys; if (sys == 0) { return new WinFactory(); } else { return new OSXFactory(); } }
54
int main(int argc, char **argv) { Application * newApplication = new Application(createOsSpecificFactory()); delete newApplication; return 0; }
Factory Method Definition: Factory method defines an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. The Factory Design Pattern is useful in a situation that requires the creation of many different types of objects, all derived from a common base type. The Factory Method defines a method for creating the objects, which subclasses can then override to specify the derived type that will be created. Thus, at run time, the Factory Method can be passed a description of a desired object (e.g., a string read from user input) and return a base class pointer to a new instance of that object. The pattern works best when a well-designed interface is used for the base class, so there is no need to cast the returned object. Problem We want to decide at run time what object is to be created based on some configuration or application parameter. When we write the code, we do not know what class should be instantiated. Solution Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. Applicability Use the Factory Method pattern when • a class can't anticipate the class of objects it must create. • a class wants its subclasses to specify the objects it creates. • classes delegate responsibility to one of several helper subclasses, and you want to localize the knowledge of which helper subclass is the delegate. In the following example, a factory method is used to create laptop or desktop computer objects at run time.
Let's start by defining Computer, which is an abstract base class (interface) and its derived classes: Laptop and Desktop. 55
class Computer { public: virtual void Run() = 0; virtual void Stop() = 0; }; class Laptop: public Computer { public: virtual void Run(){mHibernating = false;} virtual void Stop(){mHibernating = true;} private: bool mHibernating; // Whether or not the machine is hibernating }; class Desktop: public Computer { public: virtual void Run(){mOn = true;} virtual void Stop(){mOn = false;} private: bool mOn; // Whether or not the machine has been turned on };
The actual ComputerFactory class returns a Computer, given a real world description of the object. class ComputerFactory { public: static Computer *NewComputer(const std::string &description) { if(description == "laptop") return new Laptop; if(description == "desktop") return new Desktop; return NULL; } };
Let's analyze the benefits of this design. First, there is a compilation benefit. If we move the interface Computer into a separate header file with the factory, we can then move the implementation of the NewComputer() function into a separate implementation file. Now the implementation file for NewComputer() is the only one that requires knowledge of the derived classes. Thus, if a change is made to any derived class of Computer, or a new Computer subtype is added, the implementation file for NewComputer() is the only file that needs to be recompiled. Everyone who uses the factory will only care about the interface, which should remain consistent throughout the life of the application. Also, if there is a need to add a class, and the user is requesting objects through a user interface, no code calling the factory may be required to change to support the additional computer type. The code using the factory would simply pass on the new string to the factory, and allow the factory to handle the new types entirely. Another example: #include #include #include class Pizza { public: virtual int getPrice() const = 0; };
56
class HamAndMushroomPizza : public Pizza { public: virtual int getPrice() const { return 850; } }; class DeluxePizza : public Pizza { public: virtual int getPrice() const { return 1050; } }; class HawaiianPizza : public Pizza { public: virtual int getPrice() const { return 1150; } }; class PizzaFactory { public: enum PizzaType { HamMushroom, Deluxe, Hawaiian }; static Pizza* createPizza(PizzaType pizzaType) { switch (pizzaType) { case HamMushroom: return new HamAndMushroomPizza(); case Deluxe: return new DeluxePizza(); case Hawaiian: return new HawaiianPizza(); } throw "invalid pizza type."; } }; /* * Create all available pizzas and print their prices */ void pizza_information( PizzaFactory::PizzaType pizzatype ) { Pizza* pizza = PizzaFactory::createPizza(pizzatype); std::cout