0% found this document useful (0 votes)
12 views53 pages

Chapter 7 OOP Analysis and Design

The document explores three key design patterns: Bridge, Prototype, and External Polymorphism, emphasizing their roles in reducing dependencies and improving software maintainability. It highlights the advantages and performance impacts of these patterns, particularly focusing on how they facilitate abstraction and decoupling in software design. Additionally, it discusses the shortcomings and considerations for implementing these patterns effectively.

Uploaded by

yesoj68482
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views53 pages

Chapter 7 OOP Analysis and Design

The document explores three key design patterns: Bridge, Prototype, and External Polymorphism, emphasizing their roles in reducing dependencies and improving software maintainability. It highlights the advantages and performance impacts of these patterns, particularly focusing on how they facilitate abstraction and decoupling in software design. Additionally, it discusses the shortcomings and considerations for implementing these patterns effectively.

Uploaded by

yesoj68482
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 53

The Bridge, Prototype,

and
External Polymorphism
Design Patterns
Exploring Three Key Design Patterns
• Build Bridges to Remove Physical
Dependencies

• Understanding Performance Impact of


Bridges

• Abstract Copy Operations with


Prototype

• Reducing Dependencies with External


Exploring the Bridge
Design Pattern
We’ll see how the Bridge design pattern can improve
maintainability and flexibility in software design.

• A concept of connecting and uniting, yet in software


design, it's about reducing dependencies.
• Keeps different parts working together without needing
to know too much about each other.
Electric Car Example
ElectricEngine.h

• Every file that includes the <ElectricCar.h>


header will physically depend on the
<ElectricEngine.h> header.

• Implementation details expose (may be a


problem if you need to provide ABI stability)

ElectricCar.h

ElectricCar.cpp
A slightly better approach:
Only store a pointer to ElectricEngine
ElectricEngine.h
• The physical dependency is gone.

• But what if we want to use PowerEngine instead of


ElectricEngine ? Change is to be expected. Therefore, we
need abstraction.

ElectricCar.h ElectricCar.cpp
Using Abstract Class

ElectricEngine.h Engine.h

ElectricCar.h ElectricCar.cpp
“Decouple an abstraction from its implementation so that the two
can vary independently.”

In our example,

the ElectricCar class represents the “abstraction,”

while the Engine class represents the “implementation”.

• What if we want to have different cars ?

The UML representation of the basic Bridge design pattern


Adding Car Base Class
Car.h

ElectricCar.cpp ElectricCar.cpp
The Pimpl Idiom
Pointer to Implementation
• Used in C and C++
• Make Bridge design pattern simpler to implement
• Increase ABI stability
ABI(Application Binary Interface)
Stability
• Binary interface of the language is guaranteed to remain stable across different versions of the
compiler and platforms

• Libraries, frameworks, and executables compiled with version A of the compiler can be used with
version B of the compiler.

• You no longer need to make sure that the C++ version of the library or framework your project uses
matches the project's C++ version.
Before Pimpl
• Person class needs to be
extended or changed over time
• Whenever Person changes, the
users of Person have to
recompile their code.
• To hide all changes to the
implementation details of Person,
you can use the Bridge design
pattern.
Implementing Pimpl 1
• No need to provide an abstraction as
a base class
• Just introduce a private, nested class
called Impl (12). Note: Impl is only
declared not defined.
• Only data member remaining in the
Person class is the std::unique_ptr to
an Impl instance (13)
• All other data members, and non-
virtual helper functions, will moved
into the Impl class (14)
• All implementation details are hidden
from the users of Person
Implementing Pimpl 2
• Pimpl idiom is bridge design pattern in
its simplest form: local, nonpolymorphic
• The Person constructor initializes the
pimpl_ data member by
std::make_unique() (15).
• Normally, make_unique automatically
deals with allocated dynamic memory,
• But compiler searches destructor of Impl
in Person.h file.
• Solution is define a destructor in header
and declare it in class via =default. (16)
Implementing Pimpl 3
• std::unique_ptr cannot be copied, it's designed to have only one owner
• So we must implement copy constructor(17) and copy assingment
operator(18)

• Move constructor assuming pimpl_ always valid by calling make_unique


after std::move(19)

• year_of_
• birth(21)
Strategy Bridge

Category Behavioral Structural

Class's knowledge about


Doesn't know details Knows details
implementation

Flexible (via constructor or


Configuration of Behavior Internally set
setter)

Reduction of Physical
Primary Focus Reduction of Logical dependencies
dependencies
Strategy Example
•The actual type
of
DatabaseEngine
is passed in
from the outside
(22)
Strategy Dependency Graph
• Database class is on the same architectural level as the DatabaseEngine abstraction
• DatabaseEngine implements the behaviour
• Database is depending only on the abstraction(DatabaseEngine), not on implementation
Bridge Example
• Instead of accepting an engine from outside, the
constructor sets it internally
Bridge Dependency Graph
• Database class is on the same architectural level as the ConcreteDatabaseEngine class
• Bridge physically decoupled via abstraction but logical coupling to implementation remains
Shortcomings of Bridge
• Bridge design pattern reduces physical dependencies but adds
performance overhead.

• The use of a pimpl pointer introduces indirection and cost.


• Virtual function calls, inlining issues, and dynamic memory
allocation can further impact performance.

• Code complexity increases, affecting readability.


• Deciding to use Bridge should be based on performance benchmarks and
considered case by case.
Performance of Bridge
Design
Person 1
Person1.h
Person 2
Person2.h Person2.cpp
Size Table

Person Implementation Clang 11.1 Size GCC 11.1 Size

Person1 152 Bytes 200 Bytes

Person2 8 Bytes 8 Bytes

25,000xPerson1 3MB 4MB

25,000xPerson2 200 KB 200 KB


Performance Table – 1

Person Implementation GCC 11.1 Clang 11.1

Person1 1.0 1.0

Person2 1.1099 1.1312

Difference +11.0% +13.1%

(The results are normalized.)


Person 3
Person3.h Person3.cpp
Performance Table - 2

Person Implementation GCC 11.1 Clang 11.1

Person1 1.0 1.0

Person2 1.1099 1.1312

Person3 0.8597 0.9353

Difference(Person2) +11.0% +13.1%

Difference(Person3) -14.0% -6.5%

(The results are normalized.)


Prototype Design Pattern
What if we need an abstract copy?
Shortcomings of the Prototype Design Pattern
• No value semantics solution
• Negative performance impact that comes with the indirection due to
pointers
• Possible fragmented memory issues caused by dynamic memory
External Polymorphism
Separation of Concerns Design Principle – std::function Solution

Strategy Design Pattern


Strategy Design Pattern
#include <Shape.h>
#include <memory> Circle.h
#include <functional>
#include <utility>
class Circle : public Shape
{
public:
using DrawStrategy = std::function<void(Circle const &, /*...*/)>;
explicit Circle(double radius, DrawStrategy drawer)
: radius_(radius), drawer_(std::move(drawer))
{
}
void draw(/*some arguments*/) const override
{
drawer_(*this, /*some arguments*/);
} class Shape
double radius() const { return radius_; } Shape.h
{
private: public:
double radius_;
DrawStrategy drawer_; virtual ~Shape() = default;
};
virtual void draw(/*some arguments*/) const = 0;
};
THE EXTERNAL POLYMORPHISM DESIGN
PATTERN
Intent: “Allow C++ classes unrelated by inheritance and/or having no virtual methods
to be treated polymorphically. These unrelated classes can be treated in a common
manner by software that uses them.”
Shapes Revise
class Circle Circle.h class Square Square.h
{ {
public: public:
explicit Circle(double radius) explicit Square(double side)
: radius_(radius) : side_(side)
{ {
/* Checking that the /* Checking that the given
given radius is valid */ side length is valid */
} }
double radius() const { return radius_; } double side() const { return side_; }
private: /* Several more getters and
double radius_; square-specific utility functions */
/* Several more data members */ private:
}; double side_;
/* Several more data members */
};
ShapeConcept and ShapeModel
template <typename ShapeT> Shape.h
class ShapeModel : public ShapeConcept
{
public:
using DrawStrategy = std::function<void(ShapeT const &)>;
explicit ShapeModel(ShapeT shape, DrawStrategy drawer)
: shape_{std::move(shape)}, drawer_{std::move(drawer)}
{
/* Checking that the given 'std::function' is not empty */
}
void draw() const override { drawer_(shape_); }
// ... Potentially more polymorphic operations
private: #include <functional>
ShapeT shape_; #include <stdexcept> Shape.h
DrawStrategy drawer_; #include <utility>
}; class ShapeConcept
{
public:
virtual ~ShapeConcept() = default;
virtual void draw() const = 0;
// ... Potentially more polymorphic operations
};
Std::function Alternative
template <typename ShapeT, typename DrawStrategy>
class ShapeModel : public ShapeConcept
{
public:
explicit ShapeModel(ShapeT shape, DrawStrategy drawer)
: shape_{std::move(shape)}, drawer_{std::move(drawer)}
{
}
void draw() const override { drawer_(shape_); }

private:
ShapeT shape_;
DrawStrategy drawer_;
};
Std::function Alternative: Default Template
struct DefaultDrawer
{
template <typename T>
void operator()(T const &obj) const
{
draw(obj);
}
};
template <typename ShapeT, typename DrawStrategy = DefaultDrawer>
class ShapeModel : public ShapeConcept
{
public:
explicit ShapeModel(ShapeT shape, DrawStrategy drawer = DefaultDrawer{})
// ... as before
};
STD::FUNCTION VS DEFAULT TEMPLATE
PARAMETER IMPLEMENTATION
• Fewer runtime indirection.
• Not artificially augment shape classes with a template argument to
configure the drawing behavior.
• Not force additional code into a header file by turning a regular class
into a class template.
#include <Circle> OpenGLDrawStrategy.h
#include <Square>
#include /* OpenGL graphics library headers */
class OpenGLDrawStrategy
{
public:
explicit OpenGLDrawStrategy(/* Drawing related arguments */);
void operator()(Circle const &circle) const;
void operator()(Square const &square) const;

private:
/* Drawing related data members, e.g. colors, textures, ... */
};
#include <Circle.h>
#include <Square.h>
#include <Shape.h>
#include <OpenGLDrawStrategy.h>
#include <memory>
#include <vector>
int main()
{
using Shapes = std::vector<std::unique_ptr<ShapeConcept>>;
using CircleModel = ShapeModel<Circle, OpenGLDrawStrategy>;
using SquareModel = ShapeModel<Square, OpenGLDrawStrategy>;
Shapes shapes{};
// Creating some shapes, each one
// equipped with an OpenGL drawing strategy

Application Driver shapes.emplace_back(


std::make_unique<CircleModel>(
Circle{2.3}, OpenGLDrawStrategy(/*...red...*/)));
shapes.emplace_back(
std::make_unique<SquareModel>(
Square{1.2}, OpenGLDrawStrategy(/*...green...*/)));
shapes.emplace_back(
std::make_unique<CircleModel>(
Circle{4.1}, OpenGLDrawStrategy(/*...blue...*/)));
// Drawing all shapes
for (auto const &shape : shapes)
{
shape->draw();
}
return EXIT_SUCCESS;
}
ADVANTAGES
• Due to separating concerns and extracting the polymorphic behavior from the shape types, you
remove all dependencies on graphics libraries, etc. This creates a very loose coupling and
beautifully adheres to the SRP.
• The shape types become simpler and nonpolymorphic.
• Able to easily add new kinds of shapes even be third-party types, as no longer required to
intrusively inherit from a Shape base class or create an Adapter Thus, perfectly adhere to the OCP.
• Significantly reduce the usual inheritance-related boilerplate code and implement it in exactly one
place, which very nicely follows the DRY principle.
• Since the ShapeConcept and ShapeModel class belong together and together form the abstraction,
it’s much easier to adhere to the DIP.
• By reducing the number of indirections by exploiting the available class template, you can improve
performance.
Comparison Between External Polymorphism and
Adapter
• Adapter design pattern is focused on standardizing interfaces and adapts a type
or function to an existing interface
• The External Polymorphism design pattern creates a new abstraction for the
purpose of treating a set of existing types polymorphically
Analysing the Shortcomings of the External
Polymorphism Design Pattern
• This design pattern is key to loose coupling.
• It is not more widely known, because many developers have not fully embraced
the separation of concerns and tend to put everything into only a few classes.

• Disadvantage:
ØThere is only one major disadvantage, though:
vThe External Polymorphism design pattern does not really fulfil the expectations of a
clean and simple solution, and definitely not the expectations of a value semantics–based
solution.
• It does not help to reduce pointers,
• It does not reduce the number of manual allocations,
• It does not lower the number of inheritance hierarchies,
• It does not help to simplify user code.

• If we consider this a severe drawback, The thought "This should be automated


somehow" emerges. Solution:
“Guideline 32: Consider Replacing Inheritance Hierarchies with Type Erasure”
Reminders
1. External Polymorphism does not save you from thinking about a
proper abstraction.
2. Be aware that External Polymorphism, just as the Adapter design
pattern, makes it very easy to wrap types that do not fulfil the
semantic expectations.
First Reminder
With Artificial Dependency
“Guideline 3: Separate Interfaces to Avoid Artificial Coupling”
(Without Artificial Dependency)

For all code requiring only the


exportToJSON() functionality, it introduces
the artificial dependency on ByteStream
Second Reminder
• Like the duck typing example in “Guideline 24: Use
Adapters to Standardize Interfaces”, where we
pretended that a turkey is a duck, we also pretended that
an int is a shape.
• Any failure to completely fulfil the expectations may lead
to (potentially subtle) misbehaviour. (“Guideline 6:
Adhere to the Expected Behavior of Abstractions”).
• ShapeConcept base class doesn’t really represent an
abstraction of a shape. It is reasonable to argue that shapes
are more than just drawing. We should have named the
abstraction Drawable, and the LSP would have been
satisfied.
(Chapter 2: “The Art of Building Abstractions.” )
Summary
• Although the External Polymorphism design pattern may not satisfy your
expectation in a simple or value-based solution, it must be considered a very
important step toward decoupling software entities.
• With this design pattern, we can nonintrusively equip any type with polymorphic
behavior, e.g., virtual functions, so any type can behave polymorphically, even a
simple value type such as int. This realization opens up a completely new, exciting
design space, which we will continue to explore in the next chapter.

You might also like