Containment Delegation  «Prev  Next»

Lesson 1
Lesson 1

Reusability, Containment, and Delegation in COM

This module explores COM's approach to object reusability through containment and delegation, one of two primary composition mechanisms Microsoft's Component Object Model provides. While traditional object-oriented programming relies heavily on implementation inheritance to reuse code, COM takes a fundamentally different approach that favors composition over inheritance, enabling binary-level interoperability while maintaining strict interface contracts between components. Understanding containment/delegation proves essential for maintaining legacy COM codebases and appreciating how component-based architecture evolved from COM's pioneering work through COM+ to modern .NET dependency injection patterns. This introduction establishes why COM chose composition over inheritance, how containment/delegation differs from aggregation, and how these patterns influenced modern software architecture despite COM itself being legacy technology.

The Component Software Vision

Component software represents a development paradigm where applications assemble from pre-built, independently developed, binary-compatible components that communicate through well-defined interfaces. Unlike traditional software development where source code reuse requires compilation dependencies and language compatibility, component software enables reuse at the binary level—components written in different languages by different vendors can interoperate without source code access or recompilation. COM provided Microsoft's implementation of this vision, defining binary interface standards, object lifetime protocols, and composition mechanisms that made true component software possible on Windows. The vision promised reducing development costs through component reuse, improving quality through tested components, and enabling incremental application evolution by replacing components without rewriting entire systems.
// Component software vision: binary compatibility
// Component A written in C++
class ATL_NO_VTABLE spell_checker :
   public CComObjectRootEx<CComSingleThreadModel>,
   public ISpellChecker
{
public:
   STDMETHOD(CheckWord)(BSTR word, BOOL* correct);
   STDMETHOD(GetSuggestions)(BSTR word, SAFEARRAY** suggestions);
};

// Component B written in Visual Basic can use Component A
// Dim checker As ISpellChecker
// Set checker = CreateObject("SpellChecker.Component")
// Dim isCorrect As Boolean
// checker.CheckWord "recieve", isCorrect

// Component C written in Delphi can also use Component A
// var checker: ISpellChecker;
// var correct: WordBool;
// checker := CreateComObject(CLASS_SpellChecker) as ISpellChecker;
// checker.CheckWord('recieve', correct);

// Binary compatibility across languages and compilers

Why COM Avoids Implementation Inheritance

Traditional C++ implementation inheritance creates tight coupling between base and derived classes through shared implementation details. Derived classes access protected members, override virtual functions, and depend on base class implementation choices that might change in future versions. This coupling breaks binary compatibility—changing a base class implementation can break derived classes even when interfaces remain unchanged. Implementation inheritance also creates the fragile base class problem where seemingly safe base class changes break derived classes in unexpected ways. For component software requiring binary stability across language boundaries, implementation inheritance proves untenable. Different languages implement inheritance differently, vtable layouts vary, and name mangling schemes conflict. COM sidesteps these problems entirely by prohibiting implementation inheritance in favor of interface inheritance and composition through containment or aggregation.

Containment/Delegation: The Fundamental Concept

Containment/delegation enables one COM object (the outer object) to reuse another COM object's (the inner object) functionality by containing an instance of the inner object and forwarding method calls to it. The outer object implements the same interfaces as the inner but rather than duplicating implementation, its methods simply call corresponding methods on the contained inner object. From the client's perspective, the outer object provides the service. From the inner object's perspective, the outer is just another client. The inner object remains completely unaware it's being reused through containment. This transparency means inner objects require no special design to support containment—any COM object can be contained. Only the outer object knows about the containment relationship, maintaining clean separation of concerns.
// Containment/delegation basic pattern
class outer_object : public IUnknown, public IWorker {
private:
   ULONG ref_count;
   IWorker* inner_worker;  // Contained inner object

public:
   outer_object() : ref_count(1), inner_worker(nullptr) {}

   HRESULT init() {
      // Create and contain inner object
      HRESULT hr = CoCreateInstance(
         CLSID_InnerWorker,
         nullptr,  // NOT aggregating (no pUnkOuter)
         CLSCTX_INPROC_SERVER,
         IID_IWorker,  // Can request any interface
         (void**)&inner_worker
      );
      return hr;
   }

   ~outer_object() {
      if (inner_worker) {
         inner_worker->Release();
      }
   }

   // IWorker implementation - delegates to inner
   STDMETHOD(DoWork)() {
      if (!inner_worker) return E_UNEXPECTED;
      
      // Simple delegation - just forward the call
      return inner_worker->DoWork();
   }

   STDMETHOD(GetStatus)(BSTR* status) {
      if (!inner_worker) return E_UNEXPECTED;
      
      // Delegation with parameter forwarding
      return inner_worker->GetStatus(status);
   }

   // IUnknown implementation...
   STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
   STDMETHODIMP_(ULONG) AddRef();
   STDMETHODIMP_(ULONG) Release();
};

The Outer Object's Perspective

The outer object acts as both a COM object exposing interfaces to clients and a COM client using the inner object's interfaces. It creates the inner object during initialization, typically in an Init method called after construction. The outer implements interfaces that clients request, but rather than providing original implementations, it forwards method calls to the contained inner object. This forwarding can be direct pass-through, adding validation before delegation, transforming parameters, or combining results from multiple inner objects. The outer controls which inner interfaces to expose—it can selectively expose some interfaces while hiding others, or implement additional interfaces beyond those provided by the inner. The outer manages the inner object's lifetime, releasing it in the outer's destructor to ensure synchronized lifetimes.
// Outer object with selective exposure and enhancement
class enhanced_outer : public IUnknown, public IDataProcessor {
private:
   ULONG ref_count;
   IDataProcessor* inner_processor;
   BOOL logging_enabled;

public:
   enhanced_outer() 
      : ref_count(1)
      , inner_processor(nullptr)
      , logging_enabled(TRUE) {}

   // Delegation with enhancement: add logging
   STDMETHOD(ProcessData)(BSTR input, BSTR* output) {
      if (!inner_processor) return E_UNEXPECTED;

      // Pre-processing: log the call
      if (logging_enabled) {
         log_message(L"ProcessData called");
      }

      // Delegate to inner
      HRESULT hr = inner_processor->ProcessData(input, output);

      // Post-processing: log result
      if (logging_enabled) {
         log_result(hr);
      }

      return hr;
   }

   // Delegation with validation
   STDMETHOD(SetOption)(DWORD option, VARIANT value) {
      if (!inner_processor) return E_UNEXPECTED;

      // Validate parameters before delegating
      if (option > MAX_OPTION) {
         return E_INVALIDARG;
      }

      return inner_processor->SetOption(option, value);
   }

private:
   void log_message(const wchar_t* msg) {
      // Logging implementation...
   }

   void log_result(HRESULT hr) {
      // Log success/failure...
   }
};


The Inner Object's Perspective

The inner object in containment/delegation remains completely oblivious to being contained. It implements its interfaces according to COM specifications without any awareness that calls might come from a containing outer object rather than a regular client. This ignorance provides powerful decoupling—the same inner object can be used standalone, contained by different outer objects, or even aggregated (in which case it would need aggregation support). The inner object doesn't require special initialization, doesn't maintain references to the outer, and doesn't modify its behavior based on containment. This simplicity makes any COM object a candidate for containment without modification or special support, maximizing reusability.

The Client's Perspective

Clients interacting with an outer object containing an inner object remain completely unaware of the containment relationship. From the client's viewpoint, the outer object directly implements all exposed interfaces. Clients create the outer object through CoCreateInstance, query its interfaces through QueryInterface, call methods, and manage lifetime through AddRef and Release—exactly as they would with any COM object. The transparency of containment means clients don't need to know whether an object's implementation is original, delegated to a contained object, or some combination. This encapsulation enables the outer object to change its implementation strategy—switching from original implementation to containment, changing which inner object it contains, or even replacing containment with aggregation—without affecting clients at all.
// Client perspective: containment is invisible
void client_using_contained_object() {
   IDataProcessor* processor = nullptr;

   // Create object (don't know or care if it contains other objects)
   HRESULT hr = CoCreateInstance(
      CLSID_EnhancedProcessor,  // Might contain InnerProcessor
      nullptr,
      CLSCTX_INPROC_SERVER,
      IID_IDataProcessor,
      (void**)&processor
   );

   if (SUCCEEDED(hr)) {
      BSTR result = nullptr;
      
      // Call method - might delegate to inner, might not
      // Client doesn't know and doesn't need to know
      hr = processor->ProcessData(L"data", &result);

      if (SUCCEEDED(hr)) {
         // Use result...
         SysFreeString(result);
      }

      processor->Release();
   }
}

Containment vs Aggregation

COM provides two composition mechanisms with different trade-offs. Containment/delegation requires the outer object to implement forwarding methods for every exposed interface method, providing complete control over delegation but requiring code for each method. Aggregation eliminates forwarding code by exposing inner interfaces directly, improving performance and reducing code but providing less control since clients receive pointers directly into the inner object. Containment offers simplicity—no special IUnknown handling, no delegation loops to avoid, straightforward lifetime management. Aggregation offers efficiency—direct vtable dispatch without forwarding overhead, automatic exposure of all inner methods. Choose containment when you need to intercept calls, validate parameters, add logging, or selectively expose interfaces. Choose aggregation for transparent interface exposure without modification.
// Containment: outer implements and forwards
class container_approach : public IWorker {
   IWorker* inner;
public:
   // Must implement every method with forwarding code
   STDMETHOD(DoWork)() { return inner->DoWork(); }
   STDMETHOD(GetStatus)(BSTR* s) { return inner->GetStatus(s); }
   STDMETHOD(Cancel)() { return inner->Cancel(); }
   // ... every method requires forwarding code
};

// Aggregation: outer exposes inner directly
class aggregation_approach : public IUnknown {
   IUnknown* inner_nondelegate;
public:
   // QueryInterface delegates to inner for IWorker
   STDMETHOD(QueryInterface)(REFIID riid, void** ppv) {
      if (riid == IID_IWorker) {
         // Return pointer directly to inner's vtable
         return inner_nondelegate->QueryInterface(riid, ppv);
      }
      // No forwarding methods needed!
   }
};


Practical Example: Spaceship Simulation

Consider a spaceship simulation requiring both visual rendering and motion physics. Traditional C++ might use an Orbiter base class with derived Spaceship and Planet classes. Using COM containment, you create outer Spaceship and Planet classes implementing IVisual directly while containing an inner Orbiter object implementing IMotion. The outer classes delegate motion-related calls to the contained Orbiter while handling visual aspects themselves. This separation enables the Orbiter component to evolve independently—improving physics algorithms, adding relativistic effects, or optimizing performance—without touching Spaceship or Planet code. Different Orbiter implementations could provide Newtonian, relativistic, or simplified physics by exposing the same IMotion interface, with outer objects switching implementations through configuration rather than code changes.
// COM containment for spaceship simulation
class spaceship : public IUnknown, 
                  public IVisual,
                  public IMotion {
private:
   ULONG ref_count;
   IMotion* orbiter;  // Contained physics component
   
   // Visual state managed directly
   DWORD model_id;
   COLORREF color;

public:
   spaceship() : ref_count(1), orbiter(nullptr), model_id(0) {}

   HRESULT init() {
      // Contain Orbiter for physics
      HRESULT hr = CoCreateInstance(
         CLSID_NewtonianOrbiter,  // Could be RelativisticOrbiter
         nullptr,
         CLSCTX_INPROC_SERVER,
         IID_IMotion,
         (void**)&orbiter
      );
      return hr;
   }

   // IVisual - implemented directly
   STDMETHOD(Render)(HDC hdc) {
      // Draw spaceship model at current position
      draw_model(hdc, model_id, color);
      return S_OK;
   }

   STDMETHOD(SetModel)(DWORD id) {
      model_id = id;
      return S_OK;
   }

   // IMotion - delegated to contained Orbiter
   STDMETHOD(SetPosition)(double x, double y, double z) {
      return orbiter->SetPosition(x, y, z);
   }

   STDMETHOD(SetVelocity)(double vx, double vy, double vz) {
      return orbiter->SetVelocity(vx, vy, vz);
   }

   STDMETHOD(Update)(double delta_time) {
      return orbiter->Update(delta_time);
   }

   STDMETHOD(GetPosition)(double* x, double* y, double* z) {
      return orbiter->GetPosition(x, y, z);
   }

   ~spaceship() {
      if (orbiter) orbiter->Release();
   }

private:
   void draw_model(HDC hdc, DWORD id, COLORREF color) {
      // Rendering implementation...
   }
};


Advantages of Containment/Delegation

Containment/delegation provides several compelling benefits. Binary compatibility enables component replacement without recompilation—swap inner implementations while maintaining interface contracts. Language independence allows inner objects written in C++, Visual Basic, or Delphi to be contained by outer objects in any language. Selective exposure lets the outer choose which interfaces to expose and which to hide. Enhancement opportunities enable adding validation, logging, caching, or security without modifying the inner object. Simplified lifetime management avoids aggregation's complex dual-IUnknown patterns. Clear separation of concerns keeps interface contracts explicit and implementation details hidden. These advantages made containment/delegation the preferred reuse mechanism for most COM development despite requiring more forwarding code than aggregation.

Limitations and Trade-offs

Containment/delegation introduces its own costs. Code volume increases because every delegated method requires a forwarding implementation. Performance overhead comes from the extra function call layer, though this rarely matters compared to other factors. Maintenance burden grows when inner interfaces change, requiring updates to all forwarding methods. No "hooking" capability exists—the outer cannot intercept calls the inner makes to itself, unlike implementation inheritance where derived classes override protected methods. This limitation prevents certain design patterns possible with inheritance, though COM's specification explicitly acknowledged this as a future challenge requiring careful design to maintain interface contracts while enabling interception.

Evolution to Modern Patterns

COM's containment/delegation pattern influenced modern dependency injection and composition frameworks despite COM itself being superseded. .NET's object composition through constructor injection mirrors containment—outer classes receive inner dependencies and delegate operations. Interface-based programming in C# and Java directly descends from COM's interface-first approach. Wrapper patterns and decorators in design pattern literature formalize what COM practitioners discovered through containment/delegation. The principle "favor composition over inheritance" that dominates modern object-oriented design traces directly to COM's rejection of implementation inheritance. While .NET simplified the mechanics through garbage collection and unified type systems, the architectural wisdom—compose through interfaces, hide implementation, maintain binary contracts—persists across modern software engineering.

Module Learning Objectives

By completing this module, you will understand how component-based software reusability mechanisms differ from traditional source code reuse through inheritance. You'll grasp why COM chose composition over inheritance to achieve binary compatibility and language independence. You'll understand containment/delegation's mechanics—how outer objects contain inner objects, delegate method calls, manage lifetimes, and selectively expose interfaces. You'll appreciate the trade-offs between containment and aggregation, knowing when each mechanism provides advantages. You'll see how COM's containment pattern influenced modern dependency injection and composition frameworks. This foundation prepares you for implementing containment/delegation in COM components and recognizing these patterns in modern architectures.

Conclusion

Reusability through containment and delegation represents COM's primary object composition mechanism, enabling component software by favoring composition over implementation inheritance. The pattern's transparency—inner objects unaware of containment, clients unaware of delegation—provides powerful decoupling enabling true binary-compatible component reuse. While containment requires more code than aggregation due to forwarding methods, its simplicity and control made it the preferred choice for most COM development. The architectural lessons COM taught through containment/delegation—interface-based composition, binary contracts, selective exposure—transcend COM's status as legacy technology, appearing throughout modern software architecture in dependency injection, decorator patterns, and composition-over-inheritance principles. Understanding containment/delegation provides both practical skills for maintaining COM systems and architectural wisdom applicable to component design in any platform or language.

SEMrush Software