Patterns. Adapter pattern. Review and research. Examples of implementation in C++




Adapter pattern. Review and research. Examples of implementation in C++


Contents


Search other websites:

1. Assigning a pattern Adapter

The Adapter pattern belongs to structural patterns and is used to structure classes and objects. The need to use the Adapter pattern arises in cases when it is necessary to bring (adapt) one system to the requirements of another system.

The Adapter pattern can perform not only simple transformations (for example, changing names), but also implementing a completely different set of operations (substituting one code for another).

 

2. When is the Adapter pattern used? Cases of using the Adapter pattern

The Adapter pattern is used in the following cases:

  • if a certain class needs to be included in the developed program, but the interface of this class does not meet the requirements of the program;
  • if the program needs to create a class that is reused. In this case, the class must interact with other (unknown) classes that have interfaces incompatible with the program;
  • if you need to use multiple existing subclasses in one class. This is the case when an object adapter is used to accommodate these subclasses to a common parent class.

 

3. The structure of pattern Adapter (picture)

There are two types of adapter pattern

  • for the class (Figure 1). In this case, multiple inheritance is applied to adapt one interface to another;
  • for the object (Figure 2). In this case, object composition is applied.

The structures shown in Figure 1 and Figure 2 schematically show the implementation features of the Adapter pattern. The Adaptee class is a class that contains a SpecificRequest() method that needs to be adapted to the needs of the Client, which uses the Request() method. The Request() method is declared in the Target interface, which is referenced by the Client. A binder Adapter class is created between the Target interface and the Adaptee class and performs the necessary conversions.

The Client uses the Target interface to access the methods implemented in the Adapter class. The adapter, in turn, calls methods of the Adaptee class.

There are two ways to implement the Adapter class:

  • method of inheritance (Figure 1). This method is called the adapter for the class. In this way, the Adapter class inherits the methods of the Adaptee class and implements the methods of the Target interface;
  • method of objects composition. This method is called an adapter for an object. With this method, the Adapter class uses a reference (instance, object) of type Adaptee.

The structure of Adapter for classFigure 1. The structure of Adapter for class

The structure of Adapter for object

Figure 2. The structure of Adapter for object

 

4. An example of implementation of the Adapter pattern for a class in C++

The example implements an adapter for a class in C++, which corresponds to Figure 1 of this topic.

#include <iostream>
using namespace std;

// Adapter pattern for class.
// Adapts the Adaptee class to the Target class through the Adapter intermediate class.
// Abstract class Target
class Target
{
public:
  virtual void Request() = 0;
};

// The class whose method needs to be adapted to another system.
// In this case, adapting the name of the SpecificRequest () method to the Request () method
class Adaptee
{
public:
  // Some specific method
  void SpecificRequest()
  {
    cout << "Adaptee.SpecificRequest()" << endl;
  }
};

// Class Adapter - uses multiple inheritance
class Adapter :public Target, public Adaptee
{
public:
  void Request()
  {
    // Invokes SpecificRequest() from class Adaptee
    SpecificRequest();
  }
};

// Class Client - gets a reference (or pointer) to Target
class Client
{
public:
  // gets a reference to Target
  void ClientMethod(Target& target)
  {
    target.Request();
  }

  // gets a pointer to Target
  void ClientMethod(Target* target)
  {
    target->Request();
  }
};

void main()
{
  // Demonstration of the Adapter pattern for the class
  // you need to adapt the SpecificRequest() method of the Adaptee class to the needs of the client instance
  Client client;
  Target* target = (Target*) new Adapter();

  // pass an instance of adaptee to the client via the target interface
  client.ClientMethod(target); // Adaptee.SpecificRequest()
}

 

5. An example of implementation of the Adapter pattern for an object in C++

In the example, in C ++, the Adapter pattern is implemented for an object that corresponds to Figure 2 of this topic.

#include <iostream>
using namespace std;

// The Adapter pattern for an object.
// Abstract class Target
class Target
{
public:
  virtual void Request() = 0;
};

// The class in which the SpecificRequest() method needs to be passed to the client as the Request() method
class Adaptee
{
public:
  // Some specific method
  void SpecificRequest()
  {
    cout << "Adaptee.SpecificRequest()" << endl;
  }
};

// Adapter class - uses objects composition
class Adapter :public Target
{
private:
  Adaptee* adaptee;

public:

  // Constructor - receives a pointer to adaptee
  Adapter(Adaptee* _adaptee) :adaptee(_adaptee)
  {}

  void Request()
  {
    // Invokes SpecificRequest() from class Adaptee
    adaptee->SpecificRequest();
  }
};

// Class Client - receives a reference (or pointer) to Target
class Client
{
public:
  // receives a reference to Target
  void ClientMethod(Target& target)
  {
    target.Request();
  }

  // receives a pointer to Target
  void ClientMethod(Target* target)
  {
    target->Request();
  }
};

void main()
{
  // Demonstration of pattern Adapter for object
  // Instance of the Client class, which can receive the code of the SpecificRequest() method of the Adaptee class
  Client client;

  // An instance containing the SpecificRequest() method
  Adaptee adaptee;

  // Reference to the base class that will be passed to the client
  Target* target;

  // converting adaptee->target through Adapter binder class
  target = new Adapter(&adaptee);

  // Pass target to the client
  client.ClientMethod(target);
}

 

6. The results of the Adapter pattern for the class. Examples in C++

If the Adapter pattern is applied to a class (Figure 1), the following results can be obtained.

  1. If you need to adapt a class and its subclasses at the same time, the pattern will not work. Since, when inheriting, actions are delegated to a specific (only one) Adaptee class (see example 1).
  2. It is possible to replace some operations of the adapted Adaptee class with others. This is due to the fact that the Adapter class is a subclass of the Adaptee class (inheritance is used, see example 2).
  3. The pattern introduces only one new object (see example 3).

 The following examples demonstrate all of the listed results of the Adapter pattern for a class.

Example 1. The example demonstrates the impossibility of implementing the Adapter pattern for a class if the classes form a hierarchy.

...

// Adapter for class.
// The pattern does not work if you simultaneously adapt several classes that form a hierarchy.
// Class A - base class for class B
class A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "A.SpecifiedRequest()" << endl;
  }
};

// Class B - inherited from A
class B :A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "B.SpecifiedRequest()" << endl;
  }
};

// Class adapter - inherits classes A, B
class Adapter : private A, private B, public ITarget
{
public:
  void Request()
  {
    // Here the error is an ambiguous call (Adapter::SpecifiedRequest is ambiguous)
    // it is not clear what class the SpecifiedRequest() method needs to be called.
    SpecifiedRequest();
  }
};

void main(void)
{
}

 As you can see from the example, two classes A, B form a hierarchy. Class A is a base class, class B is a derived class. The Adapter class attempts to adapt the SpecifiedRequest() method of the A, B classes into the Request() method. However, with such a call

...

void Request()
{
  // Here the error is an ambiguous call (Adapter::SpecifiedRequest is ambiguous)
  // it is not clear what class the SpecifiedRequest() method needs to be called.
  SpecifiedRequest();
}

...

 the compiler will generate an error

Adapter::SpecifiedRequest is ambiguous

which means an ambiguous challenge. Here you need to choose one of two calls (classes)

A::SpecifiedRequest();

or

B::SpecifiedRequest();

If you write in the Request() method

A::SpecifiedRequest();
B::SpecifiedRequest();

then it will call two methods instead of one, which also does not fit the ideology of the pattern.

To implement the ability to work simultaneously with many adapted objects, the adapter for a class must be replaced with an adapter for an object (see below).

Example 2. The example demonstrates a code snippet in which a completely different replacement code is entered in the Request() method of the Adapter class. Since, when inheriting, the Adapter class is a subclass of the Adaptee class, this allows you to completely replace the operations of the adapted Adaptee class.

// Adapter class - inherits classes Adaptee, ITarget
class Adapter : private Adaptee, public ITarget
{
public:
  void Request()
  {
    // SpecifiedRequest();     - the method of the Adaptee class will not be called,
    // another code fits in instead
    cout << "Another program code" << endl;
  }
};

Example 3. The example demonstrates a client code snippet that shows that you do not need to create an object of the Adaptee class that adapts. This is due to the fact that all necessary replacement operations are implemented through the inheritance mechanism in the Adapter class.

...

void main()
{
  // Demonstration of the Adapter pattern for the class.
  // It is necessary to adapt the method SpecificRequest() of Adaptee class instance to client needs
  Client client;
  Target* target = (Target*) new Adapter();

  // pass an instance of target to the client
  client.ClientMethod(target);
}

In the above code snippet, the main() function is the client code. In main(), you don’t need to instantiate this class to get to the Adaptee class.

 

7. Results of the Adapter pattern for an object. Examples

If the Adapter pattern is applied to an object (see Figure 2), you can get the following results.

  1. The Adapter class can work simultaneously with many objects that need to be adapted. This also applies when the classes to be adapted form a hierarchy (see example 1 below).
  2. If the classes to be adapted form a hierarchy, the object adapter can add new (or change) functionality to all the objects that are being adapted.
  3. Difficulty in replacing operations (methods) of the Adaptee class, which is adaptable. To override a method (operation) in the Adapter class, you must inherit a subclass from Adaptee, and then use a reference to that subclass, not Adaptee (see example 2 below).

Example 1. The example demonstrates the possibility of simultaneous use of classes that form a hierarchy. Let there be given two classes that form a hierarchy: base class A and derived class B.

// Adapter for object.
// The pattern allows you to simultaneously adapt several classes that form a hierarchy.
#include <iostream>
using namespace std;

class ITarget
{
public:
  virtual void Request() = 0;
};

// Class A - base class
class A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "A.SpecifiedRequest()" << endl;
  }
};

// Class B - derived from class A
class B: public A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "B.SpecifiedRequest()" << endl;
  }
};

// Class Adapter - uses object composition,
// inherits the ITarget class
class Adapter : public ITarget
{
private:
  A* adaptee;

public:
  // Constructor, receives a reference to the top of the hierarchy,
  // the client can use this reference to pass any class in the hierarchy.
  // This is an advantage of the Adapter for objects pattern over the Adapter for classes.
  Adapter(A* adaptee)
  {
    this->adaptee = adaptee;
  }

  virtual void Request()
  {
    // Invokes method SpecifiedRequest()
    adaptee->SpecifiedRequest();
  }
};

// Class Client - uses adapter
class Client
{
public:
  // Client method - will call methods of different classes from the hierarchy
  void ClientMethod(ITarget* target)
  {
    target->Request();
  }
};

void main(void)
{
  // 1. Pass the SpecificRequest() method of class A to the client
  Client client; // Declare an instance of the Client class

  // Declare an instance of class A
  A objA;
  Adapter adapterA(&objA);

  // Call the client method - pass an instance of the class A
  client.ClientMethod((ITarget*)&adapterA); // A.SpecificRequest()

  // 2. Pass the SpecificRequest() method of class B to the client
  B objB;
  Adapter adapterB((A*)&objB);
  client.ClientMethod((ITarget*)&adapterB); // B.SpecificRequest()

  // 3. Another client use case
  ITarget* client2; // pointer to interface ITarget
  client2 = (ITarget*)&adapterA;
  client2->Request(); // A.SpecificRequest()

  client2 = (ITarget*)&adapterB;
  client2->Request(); // B.SpecificRequest
}

Example 2.  The example demonstrates overriding an operation in a class to be adapted for the Adapter pattern for objects. To replace the operation (method) SpecificRequest() in the Adaptee class, do the following:

  • the AdapteeSub class inherits from the Adaptee class;
  • the AdapteeSub class declares the SpecificRequest() method, which overrides the SpecificRequest() method of the Adaptee class;
  • in the Adapter class, a pointer (reference) to the derived AdapteeSub class is declared;
  • in the Adapter class, in the Request() method, the SpecificRequest() method of the AdapteeSub class is called.

 

// Object adapter.
// Demonstration of method replacement in the Adaptee class
// in the case of implementing an adapter for an object.
#include <iostream>
using namespace std;

class ITarget
{
public:
  virtual void Request() = 0;
};

// Class Adaptee - which need to be adapted
class Adaptee
{
public:
  void SpecifiedRequest()
  {
    cout << "Adaptee.SpecifiedRequest()" << endl;
  }
};

// Subclass of class Adaptee
class AdapteeSub : private Adaptee
{
public:
  void SpecifiedRequest()
  {
    cout << "Another operation: AdapteeSub.SpecifiedRequest()";
  }
};

// Adapter class - uses objects composition,
// inherits from the ITarget class
class Adapter : public ITarget
{
private:
  AdapteeSub* adapteeSub; // a reference to a subclass

public:
  // Constructor, gets a reference to the base Adaptee class
  Adapter(Adaptee* adaptee)
  {
    this->adapteeSub = (AdapteeSub*)adaptee;
  }

  virtual void Request()
  {
    // Invokes method SpecifiedRequest()
    adapteeSub->SpecifiedRequest();
  }
};

// Client class - uses an adapter
class Client
{
public:
  // Client method - calls methods of different classes from the ITarget hierarchy
  void ClientMethod(ITarget* target)
  {
    target->Request();
  }
};

void main(void)
{
  // 1. Pass the SpecificRequest() method of class A to the client
  Client client; // Declare an instance of the Client class

  // Declare an instance of the AdapteeSub class that implements
  // the override for the Adaptee :: SpecifiedRequest () operation
  AdapteeSub obj;
  Adapter adapter((Adaptee*)&obj); // Pass an obj instance to the adapter

  // Pass adapter to client
  client.ClientMethod((ITarget*)&adapter); // Another operation: AdapteeSub.SpecifiedRequest()
}

 


Related topics