Large Scale C++ Software Design
John Lakos It would be ludicrous to attempt to erect a 50 story office building using the same materials and techniques a carpenter would use to build a single family home. Experience gained from small projects does not always extend to large development efforts.
Large Scale C++ Software Design
Motivation Object Oriented programs in their most general form are fundamentally more difficult to test than their procedural counterparts. Their ability to alter internal behavior via virtual functions may invalidate essential class invariants. As programs get larger, many different forces come into play. Most large projects fail due to poor physical design.
Large Scale C++ Software Design
Why do large projects Fail ? Cyclic Link-Time Dependencies Excessive Link-Time Dependencies Excessive Compile-Time Dependencies
Cyclic Link-Time Dependencies
Cyclic Link-Time Dependencies
Any Single module does not make sense independently Unable to start anywhere. Each module requires the definition of the other No one component can be used or tested without the other two
Excessive Link-Time Dependencies
Ever wanted to use a small functionality which took hours to link? What happens when unnecessary functionality turns a lean concise class into huge dianosaurs. What happens when functionality of all clients cramped into one module. Each time a new feature is added,causes rest of clients increased instance size, code size, runtime and physical dependencies.
Excessive Link-Time Dependencies
//str.h class String{ char* d_string_p; int d_length; int d_Size; int d_Count; //... public: String(); String(const String&); String(const char*); String(const char c); ~String(); // //...27 pages omitted // int isPalindrome(); int isNameofFamousActor(); };
//str.cpp #include "str.h" #include "sun.h" #include "Asterix.h" //... //lots of includes //... #include "everyone.h" #include "theirmother.h" String::String() :d_String(0) ,d_length(0) ,d_size(0) ,d_Count(0) //... //...
Excessive Link-Time Dependencies
Excessive Compile-Time Dependencies
Ever developed a multi-file program? Changing a header file can potentially cause several translation units to recompile. In large projects, not only is the time to recompile the entire system increasing, but so is the time to compile even individual translation units. Soon, refusal to modify low level units make the design unmaintainable and practically useless
Excessive Compile-Time Dependencies
//myerror.h #ifndef MY ERROR H #define MY ERROR H struct MyError { enum Codes { SUCCESS = 0, WARNING, IO ERROR, // . . . READ ERROR, WRITE ERROR, // . . . BAD STRING, BAD FILENAME, // . . . MARTIANS HAVE LANDED, // . . . } } #endif
Reuse is
not without cost!!
If several programmers are attempting to use the same standard component without demanding functional changes, the reuse is probably reaonable and justified. If several clients working on different programs, attempt to reuse a component for a different purpose, an enhancement for a client may possibly disrupt the other. Unlike a program, a system has no main. It is a collection of interdependent components that support a domain. One must find a way to resuse part of a system needed to implement a particular program without having to link in the rest of the system.
To Summarize
What are Dependencies? Notations
What are Dependencies?
IsA class B{/*...*/}; class D : public B{/*...*/};
D inherits from B
Uses In the Interface //Imagine in a class D int operator==(const A&,const A&)
D uses A in D’s interface
What are Dependencies?
Uses In the Implementation
//Intset class int operator==(const Intset&,const Intset&) { IntsetIter iter = ... }
The ”DependsOn” relation
Implied Dependencies
Compile-Time Dependencies
//str.h #include "chararray.h" class String{ charArray d_array; //Has A //... };
Visible that class String has CharArray as data member. It is known from C that if a struct has an instance of a user defined type as a member, it is necessary to know the size and layout of the data member to parse through the definition of the struct.
Link-Time Dependencies
//word.h #ifndef INCLUDED_WORD #define INCLUDED_WORD
//str.h #ifndef INCLUDED_STR #define INCLUDED_STR
#ifndef INCLUDED_STR #include "str.h" #endif
class CharArray;
class Word{ String d_string; //HasA //... public: Word(); //... }; //endif -------------------------//word.c #include "word.h" //....
class String{ CharArray* pd_Array; //HoldsA //... public: String(); //... }; #endif --------------------------------//str.c #include "str.h" #include "charArray" //... //...
Link-Time Dependencies
A component need not be dependent on another at compile time to be dependent at link time. Compiling charArray.c requires charArray.h Both str.h and charArray.h are reuired to compile str.c and Finally word.h and str.h is required to compile word.c Notice, charArray.h is not required to compile word.c. However, word still exhibits a physical dependency on charArray. Except for inline functions, all class member functions and static data members in C++ have external linkage. A compile time dependency almost always implies a link time dependency but not vice versa.
Ensure Reliable Quality Software Physical Heirarchy Heirarchy among components as defined by the DependsOn realtion is analogous to logical heirarchy implied by layering.(Physical heirarchy is not “logical inheritance“ among classes altough inheritance implies physical dependencies. Avoiding cyclic physical dependencies is core to effective understanding,maintainability testing and re-use of code. Well designed interfaces are small, easy to understand, easy to use, yet these interfaces make user-level testing expensive. Complex software systems should be designed to have low level objects with well defined interfaces, allowing each object to be tested in isolation. these objects must be integrated via layering into a sequence of increasing complex sub systems again allowing each sub system to be tested in isolation
Acyclic Physical Dependencies
Acyclic Physical Dependencies
//c1.h #ifndef INCLUDED_C1 #define INCLUDED_C1 class C1{ //... public: C1 f(); }; #endif
//c2.h #ifndef INCLUDED_C2 #define INCLUDED_C2 class C1
//c3.h #ifndef INCLUDED_C3 #define INCLUDED_C3 class C2
class C2{ //... public: C1 g(); }; #endif
class C3{ //... public: C1 h(const C2&); }; #endif
//c1.h #ifndef INCLUDED_C1 #define INCLUDED_C1 class C2; class C1{ //... public: //C1 f(); //old //C2 f(); //new }; #endif
Decompose design into units with manageable complexity.In case of APD, there is atleast one reasonable order to go about testing the system. C1 is tested first as it depends on nobody, then the additional C2 functionality As both C1 and C2 work properly, finally C3 can be tested
High Quality Designs are Levelizable
High Quality Designs are Levelizable
Idea of partioning components into equivalence classes based on physical dependencies. Components whcih are external to the system and which have already been tested and are reliable have level 0 Component with no local physical dependencies have level 1 A component that depends physically on another component at level N-1, but not higher has level N With a level diagram components can designed to be tested in isolation. Not possible with graphs that contain cyclic dependencies.
Is this Design Levelizable ?
Yes