Decorator

Problem

  • Modifying an interface to add functionality is inappropriate

    • … maybe because there is no consensus that the functionality should be part of the interface

    • … maybe simply because the interface is not in your hands

  • ⟶ add small wrapper classes with the added functionality

  • Instances of wrapper classes should be usable as instances of the original interface

Solution

  • Introduce a wrapper class that contains an object of the original interface.

  • The wrapper class implements the original interface. In other words, objects of the wrapper class can be used as object that implement the original interface.

  • Additionally (that is the point), the wrapper class implements the functionality that cannot be part of the original interface.

Variant 1: Aggregate Base Class Objects

../../../../../_images/decorator-assoc-base.png

Variant 2: Aggregate Conrete Class Objects

../../../../../_images/decorator-assoc-concrete.png

Exercise

Motivation

Implement decorators that add the following functionality to all classes in our Sensor hierarchy:

  • A name attribute

    • Intention: every sensor in a system should carry a name with it

    • “We want it simple!”

  • A description attribute

    • Intention: every sensor should be able to describe what it does and what its configuration is

    • “We want it debuggable!”

Step 1: Realize Current Situation

The original interface is used in current code. It is that current code that must be left unmodified - ideally, current code must continue to work even without recompilation.

A somewhat contrived (but representative) example of “current code” is the average() function that computes an average value across a set of sensors.

#include <gtest/gtest.h>

#include <sensor-const.h>
#include <sensor-utils.h>


TEST(decorator_suite, average_with_base_sensor)
{
    ConstantSensor c1(42);
    ConstantSensor c2(666);

    std::vector<Sensor*> sensors{&c1, &c2};

    ASSERT_FLOAT_EQ(average(sensors.begin(), sensors.end()), 354);
}

Step 2: Decorate ConstantSensorDecoratedConstantSensor

Create a wrapper that adds the required name and description attributes, in a way that makes the following test pass.

The intermediate class DecoratedSensor enables the proponents of the added functionality to attach their own debugging/diagnostics code to the system - without any modification/pollution of existing code.

#include <gtest/gtest.h>

#include <sensor-decorated-const.h>


TEST(decorator_suite, decorated_sensor_const_added_functionality)
{
    DecoratedConstantSensor c("my-funny-const-sensor", 38.6);

    ASSERT_EQ(c.description(), "ConstantSensor(38.6)");        // <--- appears to be a DecoratedSensor
    ASSERT_EQ(c.name(), "my-funny-const-sensor");              //                -"-

    DecoratedSensor* ds = &c;                                  // <--- is-a DecoratedSensor
    ASSERT_EQ(ds->description(), "ConstantSensor(38.6)");      // <--- used as-a DecoratedSensor
    ASSERT_EQ(ds->name(), "my-funny-const-sensor");            // <---           -"-
}

Step 3: Decorate RandomSensorDecoratedRandomSensor

Same as step 2, only with RandomSensor.

#include <gtest/gtest.h>

#include <sensor-decorated-random.h>


TEST(decorator_suite, decorated_sensor_random_added_functionality)
{
    DecoratedRandomSensor r("my-funny-random-sensor", 33.3, 40.5);

    ASSERT_EQ(r.description(), "RandomSensor(33.3, 40.5)");    // <--- appears to be a DecoratedSensor
    ASSERT_EQ(r.name(), "my-funny-random-sensor");             //                -"-

    DecoratedSensor* ds = &r;                                  // <--- is-a DecoratedSensor
    ASSERT_EQ(ds->description(), "RandomSensor(33.3, 40.5)");  // <--- used as-a DecoratedSensor
    ASSERT_EQ(ds->name(), "my-funny-random-sensor");           // <---           -"-
}

Step 4: Verify That DecoratedConstantSensor Can Be Used As Sensor

The point in adding the DecoratedSensor sub-hierarchy to the system is to extend existing functionality without any modification thereof. In that sense, it must be possible to use a decorator object as-a original object.

#include <gtest/gtest.h>

#include <sensor-decorated-const.h>


TEST(decorator_suite, decorated_sensor_const__is_a__sensor)
{
    DecoratedConstantSensor c("my-funny-const-sensor", 38.6);

    Sensor* s = &c;                                            // <--- is-a Sensor
    ASSERT_FLOAT_EQ(s->get_temperature(), 38.6);               // <--- used as-a Sensor
}

Step 5: Verify That DecoratedRandomSensor Can Be Used As Sensor

Same as step 4, only with RandomSensor.

#include <gtest/gtest.h>

#include <sensor-decorated-random.h>


TEST(decorator_suite, decorated_sensor_random__is_a__sensor)
{
    DecoratedRandomSensor r("my-funny-random-sensor", 33.3, 40.5);

    ASSERT_GE(r.get_temperature(), 33.3);                      // <--- appears to be-a Sensor
    ASSERT_LE(r.get_temperature(), 40.5);                      // <--- appears to be-a Sensor
    Sensor* s = &r;                                            // <--- is-a Sensor
    ASSERT_GE(s->get_temperature(), 33.3);                     // <--- used as-a Sensor
    ASSERT_LE(s->get_temperature(), 40.5);                     // <--- used as-a Sensor
}

Step 6: Verify That Current Situation Still Holds True

Proponents of the good old situation might want a proof that the integrity of the system is still guaranteed, even though new functionality has been added. Those proponents will sure want to see the following test pass:

#include <gtest/gtest.h>

#include <sensor-decorated-const.h>
#include <sensor-utils.h>


TEST(decorator_suite, average_with_decorated_sensor)
{
    DecoratedConstantSensor c1("c1", 42);
    DecoratedConstantSensor c2("c2", 666);

    std::vector<Sensor*> sensors{&c1, &c2};

    ASSERT_FLOAT_EQ(average(sensors.begin(), sensors.end()), 354);
}