Journal Articles

CVu Journal Vol 12, #6 - Dec 2000 + Programming Topics
Browse in : All > Journals > CVu > 126 (17)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: OOD and Testing using the Dependency Inversion Principle

Author: Administrator

Date: 03 December 2000 13:15:41 +00:00 or Sun, 03 December 2000 13:15:41 +00:00

Summary: 

I learned one of the most important OOD techniques 10 years ago on an OOD workshop. It has since become known as the Dependency Inversion Principle.

Body: 

Design

The Dependency Inversion Principle

I learned one of the most important OOD techniques 10 years ago on an OOD workshop. It has since become known as the Dependency Inversion Principle (see www.objectmentor.com/publications/dip.pdf). It eliminates most of the dependencies between classes, which yields three major advantages:

  • The impact of design changes is minimised; the design is thus both more flexible and more robust

  • The scope for re-use of classes is increased

  • Unit testing of classes is facilitated

This Principle pervades many of the latest OO technologies, e.g. the use of interface and class in Java, the use of interfaces and co-classes in COM. The Principle can be realised in C++ by means of abstract classes.

A simple class

Consider a C++ class that simply stores a floating-point number. We can declare its interface in an abstract class as follows:

#ifndef ASTORE_H
#define ASTORE_H
class Astore {
public:
    virtual void Put(double d) = 0;
    virtual double Get() const = 0;
    virtual ~AStore() {}
};
#endif // ASTORE_H

This corresponds to an interface in Java and COM. I have used the convention of prefixing the class name with A to indicate that it is an abstract class. There are no data members or constructors. The methods which make up the abstraction, Put and Get, are declared pure virtual. The standard virtual destructor is also provided (note that this must be implemented, not pure).

We can then implement this abstraction by inheriting from it. For the purposes of illustration, a trivial implementation is shown below (using such a CStore object would be no different from using a double so it is an unnecessarily complicated abstraction). Examples of more realistic implementations are when the value needs to be protected against concurrent access, or when the value needs to be mapped to a hardware address.

#ifndef STORE_H
#define STORE_H
#include "AStore.h"
class CStore : public Astore {
protected:
   CStore() : m_d(0.0) {}
public:
   static AStore *New() { return new CStore; }
   static int main();
   virtual void Put(double d) { m_d = d; }
   virtual double Get() const { return m_d; }
   virtual ~CStore() {;}
private:
   double m_d;
};
#endif // STORE_H

This corresponds to a class in Java and a co-class in COM. Implementations are provided for the abstract methods Put and Get, together with the standard virtual destructor. The main method is used for testing and is discussed later.

Because this realisation of the Dependency Inversion Principle depends upon the use of virtual functions, it only works if pointers to objects of this class are used. Hence there are no public constructors; a static function provided will construct an object of the class and return a pointer to it. I have used the convention of naming this function New, by analogy with the C++ keyword new. For each constructor declared by the class there should be a corresponding New with the same arguments as the constructor.

A class depending upon another class

Now consider a class that depends upon this class, for example we might use the store to hold the value of a simple pocket calculator memory. Again we abstract the interface from the implementation:

#ifndef AMEMORY_H
#define AMEMORY_H
class Amemory {
public:
   virtual void Save(double d) = 0;
   virtual void Add(double d) = 0;
   virtual void Subtract(double d) = 0;
   virtual void Cancel() = 0;
   virtual double Recall() const = 0;
   virtual ~AMemory() {;}
};
#endif // AMEMORY_H

#ifndef MEMORY_H
#define MEMORY_H
#include "AMemory.h"
#include "AClassFactory.h"
#include "AStore.h"
class CMemory : public Amemory {
protected:
   CMemory() : pStore(0) {;}
   void Create(AClassFactory *pClassFactory){
      pStore = pClassFactory->CreateStore();
   }
public:
   static AMemory *New(AclassFactory
                               *pClassFactory) {
      CMemory *pMemory = new CMemory();
      pMemory->Create(pClassFactory);
      return pMemory;
   }
   static int main();
   virtual void Save(double d){pStore->Put(d);}
   virtual void Add(double d) 
            { pStore->Put(d + pStore->Get()); }
   virtual void Subtract(double d){Add(0 - d);}
   virtual void Cancel(){ pStore->Put(0); }
   virtual double Recall() const 
                        { return pStore->Get(); }
   virtual ~CMemory(){if(pStore)delete pStore;}
private:
   AStore *pStore;
};
#endif // MEMORY_H

The essential point of the Dependency Inversion Principle is that the classes AMemory and CMemory must only depend upon abstract classes. Hence the member variable pStore is of class pointer to AStore, the abstraction, not CStore. This means that any change to the implementation of AStore has no impact at all upon this class. CStore itself could be modified, or we could replace it with another class derived from AStore (say CProtectedStore or CMappedStore corresponding to the previously enumerated more realistic examples). The Principle has exactly the same benefits if a member function of a class uses abstract classes for the types of its arguments and return values (not shown in this example).

This leaves the issue of how classes initialise member variables that are pointers to objects declared abstract. This can be solved by means of a class factory that encapsulates all the New static member functions. The class factory is passed to any New static member function which needs it, and the New in turn calls a protected member function (conventionally named Create) to use the class factory methods to initialise member variables which are pointers to objects declared abstract.

The class factory is declared and implemented using the Dependency Inversion Principle just like the other classes:

#ifndef ACLASSFACTORY_H
#define ACLASSFACTORY_H
#include "AMemory.h"
#include "AStore.h"
class AclassFactory {
public:
   virtual AStore *CreateStore() = 0;
   virtual AMemory *CreateMemory(
            AClassFactory *pClassFactory) = 0;
};
class CNullClassFactory : public AclassFactory {
public:
   virtual AStore *CreateStore() { return 0; }
   virtual AMemory *CreateMemory(
       AClassFactory *pClassFactory){return 0;}
};
#endif // ACLASSFACTORY_H

#ifndef CLASSFACTORY_H
#define CLASSFACTORY_H
#include "AClassFactory.h"
#include "Store.h"
#include "Memory.h"
class CClassFactory : public AclassFactory {
public:
   virtual AStore *CreateStore() {
      return CStore::New();
   }
   virtual AMemory *CreateMemory(
               AClassFactory *pClassFactory){
      return CMemory::New(pClassFactory);
   }
};
#endif // CLASSFACTORY_H

Hence to change the implementation of a class being used, all that is required is to modify the implementation of the class factory; there is no impact at all upon the client classes. The null class factory is useful when testing and is discussed later.

Testing

A convention for unit testing

The static member functions named main are used for unit testing. The convention that every class should have such a main was proposed by Francis Glassborow in EXE magazine (May 1999). An example implementation for the CStore class is:

#ifdef
      TESTING #include "Store.h" #include <cassert> int CStore::main() {
      AStore *pStore = New(); assert(0.0 == pStore->Get());
      pStore->Put(3.0); assert(3.0 == pStore->Get());
      pStore->Put(2.0); assert(2.0 == pStore->Get()); delete pStore;
      return 1; } #endif // TESTING

This convention makes it trivial to write a test harness for multiple classes:

#include "Store.h" #include "Memory.h"
      #include <cassert> int main(int argc, char* argv[]){
      assert(CStore::main()); assert(CMemory::main()); return 0;
      }

The Dependency Inversion Principle and unit testing

The thing I like the most about the Dependency Inversion Principle is that it facilitates unit testing. Stub versions of classes that a class under test depends upon are easily incorporated into the test harness (though writing the stubs is not always easy, of course). All we need to do is implement a class factory that will return pointers to stub objects.

Consider unit-testing CMemory with a stub implementation of AStore named CStoreStub. Glassborow's convention is that classes used for unit testing a particular class should be declared in a file named <ClassUnderTest>Tester.h. So in MemoryTester.h we could have:

#ifndef MEMORYTESTER_H #define MEMORYTESTER_H #include
      "AClassFactory.h" #include <cassert> class CStoreStub : public
      Astore { public: CStoreStub() : m_nTestCase(0) {;} virtual void
      Put(double d) { m_nTestCase++; switch(m_nTestCase) { case 2: assert(d ==
      3.0); break; case 5: assert(d == 5.0); break; case 8: assert(d == 2.0);
      break; case 10: assert(d == 0.0); break; default: assert(0); break; } }
      virtual double Get() const { m_nTestCase++; switch(m_nTestCase) { case
      1: return 0.0; break; case 3: case 4: return 3.0; break; case 6: case 7:
      return 5.0; break; case 9: return 2.0; break; case 11: return 0.0;
      break; default: assert(0); return 0.0; break; } } virtual ~CStoreStub()
      {;} private: mutable int m_nTestCase; }; class CStubClassFactory :
      public CNullClassFactory { public: virtual AStore *CreateStore() {
      return new CStoreStub; } }; #endif // MEMORYTESTER_H

The stub class factory is derived from the null class factory so that we only have to implement the methods we actually need for these unit tests. In this case it is just the CreateStore method, and we implement this to return a pointer to a CStoreStub object. We then use the stub class factory in the unit tests for CMemory:

#ifdef TESTING
      #include "Memory.h" #include "MemoryTester.h" #include <cassert>
      int CMemory::main(){ CStubClassFactory StubClassFactory; AMemory
      *pMemory = New(&amp;StubClassFactory); assert(0.0 ==
      pMemory->Recall()); pMemory->Save(3.0); assert(3.0 ==
      pMemory->Recall()); pMemory->Add(2.0); assert(5.0 ==
      pMemory->Recall()); pMemory->Subtract(3.0); assert(2.0 ==
      pMemory->Recall()); pMemory->Cancel(); assert(0.0 ==
      pMemory->Recall()); delete pMemory; return 1; } #endif //
      TESTING

Editorial Notes:

Unfortunately, since having my article published in EXE it has come to my notice that while main is not reserved in C it is in C++ and so strictly speaking it should not be used other than for the entry point to a program.

I have taken liberties with the code format to get this article to fit.

Notes: 

More fields may be available via dynamicdata ..