Case Study/Livehacking: Heating Control (Reading Sensors)

Step 1: Monolithic

  • Hack SensorReader

  • Depending on concrete classes (logging, value store)

  • All in one main() file

../../../../../_images/heating-step-1.png
#include <sensor-const.h>
#include <sensor-random.h>

#include <chrono>
#include <thread>
#include <vector>
#include <map>
#include <iostream>

using namespace std::chrono_literals;


class DemoLogger
{
public:
    void log(const std::string& msg)
    {
        std::cerr << "DEMO-LOGGER: " << msg << std::endl;
    }
};

class DemoValueStore
{
public:
    void set(const std::string& name, double value)
    {
        std::cerr << "DEMO-STORE: setting " << name << " = " << value << std::endl;
    }
    
private:
    std::map<std::string/*name*/, double/*temperature*/> _store;
};


class SensorReader
{
public:
    using NamedSensor = std::pair<std::string, Sensor*>;
    using Sensors = std::vector<NamedSensor>;

public:
    SensorReader(
        const Sensors& sensors,
        DemoLogger& logger,
        DemoValueStore& store)
    : _sensors(sensors),
      _logger(logger),
      _value_store(store) {}

    void doit()
    {
        for (auto [name, sensor]: _sensors){
            _logger.log(name);
            double temperature = sensor->get_temperature();
            _value_store.set(name, temperature);
        }
    }
    
private:
    std::vector<NamedSensor> _sensors;    
    DemoLogger& _logger;
    DemoValueStore& _value_store;
};


int main()
{
    DemoLogger logger;
    DemoValueStore store;
    SensorReader::Sensors sensors{
        {"sensorA", new RandomSensor(34.2, 41.3)},
        {"sensorB", new ConstantSensor(4)},
        {"sensorC", new RandomSensor(100, 200000)},
    };
    
    SensorReader rdr(
        sensors,
        logger,
        store
    );

    for (auto round: {1,2,3,4,5}) {
        std::cout << "*** Round " << round << " ..." << std::endl;
        rdr.doit();
        std::this_thread::sleep_for(0.5s);
    }

    return 0;
}
$ ./heating-demo-v1
*** Round 1 ...
DEMO-LOGGER: sensorA
DEMO-STORE: setting sensorA = 40.1392
DEMO-LOGGER: sensorB
DEMO-STORE: setting sensorB = 4
DEMO-LOGGER: sensorC
DEMO-STORE: setting sensorC = 12597.1
...

Step 2: And D-Bus? ⟶ Interfaces

../../../../../_images/heating-interfaces.png
#pragma once

#include <sensor.h>

#include <vector>
#include <string>


// * reads a configured list of sensors
// * writes name/sensor-value pairs into a value store of some kind
// * logs messages as it goes
class SensorReader
{
public:
    using NamedSensor = std::pair<std::string, Sensor*>;
    using Sensors = std::vector<NamedSensor>;

    // Aspects of a logger that I need: get rid of messages
    class Logger
    {
    public:
        virtual ~Logger() {}
        virtual void log(const std::string& msg) = 0;
    };

    // Aspects of a value-store that I need: store double value under
    // a name
    class ValueStore
    {
    public:
        virtual ~ValueStore() {}
        virtual void set(const std::string& name, double temperature) = 0;
    };

public:
    SensorReader(
        const Sensors& sensors,
        Logger& logger,
        ValueStore& store)
    : _sensors(sensors),
      _logger(logger),
      _value_store(store) {}

    void doit()
    {
        for (auto [name, sensor]: _sensors){
            _logger.log(name);
            double temperature = sensor->get_temperature();
            _value_store.set(name, temperature);
        }
    }
    
private:
    std::vector<NamedSensor> _sensors;    
    Logger& _logger;
    ValueStore& _value_store;
};
#include "sensor-reader.h"

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

#include <chrono>
#include <thread>
#include <map>
#include <iostream>

using namespace std::chrono_literals;


class DemoLogger : public SensorReader::Logger
{
public:
    void log(const std::string& msg) override
    {
        std::cerr << "DEMO-LOGGER: " << msg << std::endl;
    }
};

class DemoValueStore : public SensorReader::ValueStore
{
public:
    void set(const std::string& name, double value) override
    {
        std::cerr << "DEMO-STORE: setting " << name << " = " << value << std::endl;
    }
    
private:
    std::map<std::string/*name*/, double/*temperature*/> _store;
};


int main()
{
    DemoLogger logger;
    DemoValueStore store;
    SensorReader::Sensors sensors{
        {"sensorA", new RandomSensor(34.2, 41.3)},
        {"sensorB", new ConstantSensor(4)},
        {"sensorC", new RandomSensor(100, 200000)},
    };
    
    SensorReader rdr(
        sensors,
        logger,
        store
    );

    for (auto round: {1,2,3,4,5}) {
        std::cout << "*** Round " << round << " ..." << std::endl;
        rdr.doit();
        std::this_thread::sleep_for(0.5s);
    }

    return 0;
}
$ ./heating-demo-v2
*** Round 1 ...
DEMO-LOGGER: sensorA
DEMO-STORE: setting sensorA = 36.2895
DEMO-LOGGER: sensorB
DEMO-STORE: setting sensorB = 4
DEMO-LOGGER: sensorC
DEMO-STORE: setting sensorC = 158243
...

Step 3: Start D-Bus Implementation

../../../../../_images/heating-dbus.png

Pull Demo Logger/Store Out Into Separate Files

Adapter: DBusLogger

#pragma once

#include "sensor-reader.h"

#include <string>
#include <cassert>


class DBusLogger : public SensorReader::Logger
{
public:
    void log(const std::string& msg) override
    {
        assert(!"Boss, we need a DBus consultant!!");
    }
};

Adapter: DBusValueStore

#pragma once

#include "sensor-reader.h"

#include <string>
#include <map>


class DBusValueStore : public SensorReader::ValueStore
{
public:
    void set(const std::string& name, double value) override
    {
        assert(!"Boss, we need a DBus consultant!!");
    }
};

Demo Program To Instantiate Either Demo Or DBus

#include "sensor-reader.h"

#include "logger-demo.h"            // <--- pulled out to header
#include "valuestore-demo.h"        // <--- pulled out to header
#include "logger-dbus.h"            // <--- new header
#include "valuestore-dbus.h"        // <--- new header

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

#include <chrono>
#include <thread>
#include <memory>
#include <iostream>

using namespace std::chrono_literals;


int main(int argc, char** argv)
{
    std::unique_ptr<SensorReader::Logger> logger;
    std::unique_ptr<SensorReader::ValueStore> store;

    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " 'DEMO|DBUS'" << std::endl;
        return 1;
    }
    std::string environ = argv[1];
    if (environ == "DEMO") {
        logger.reset(new DemoLogger);
        store.reset(new DemoValueStore);
    }
    else if (environ == "DBUS") {
        logger.reset(new DBusLogger);
        store.reset(new DBusValueStore);
    }
    else {
        std::cerr << "Usage: " << argv[0] << " 'DEMO|DBUS'" << std::endl;
        return 1;
    }


    SensorReader::Sensors sensors{
        {"sensorA", new RandomSensor(34.2, 41.3)},
        {"sensorB", new ConstantSensor(4)},
        {"sensorC", new RandomSensor(100, 200000)},
    };
    
    SensorReader rdr(
        sensors,
        *logger,
        *store
    );

    for (auto round: {1,2,3,4,5}) {
        std::cout << "*** Round " << round << " ..." << std::endl;
        rdr.doit();
        std::this_thread::sleep_for(0.5s);
    }

    return 0;
}

Stop Here, Need Help

$ ./heating-demo-v3 DBUS
*** Round 1 ...
heating-demo-v3: /home/jfasch/work/jfasch-home/trainings/material/soup/cxx-design-patterns/exercises/../code/heating/logger-dbus.h:14: virtual void DBusLogger::log(const std::string&): Assertion `!"Boss, we need a DBus consultant!!"' failed.
Aborted (core dumped)
  • Call for consultant to do the dirty work

  • In the meantime, focus on stabilizing core logic (there’s a leak somewhere)

Note

We did not modify SensorReader in a while!!

Tests

../../../../../_images/heating-tests.png
#include <gtest/gtest.h>

#include "sensor-reader.h"
#include <sensor-const.h>
#include <map>

namespace {

struct MockLogger : public SensorReader::Logger
{
    void log(const std::string& msg) override
    {
        lines_logged++;
    }

    int lines_logged = 0;
};

struct MockValueStore : public SensorReader::ValueStore
{
    void set(const std::string& name, double value)
    {
        values[name] = value;
    }
    std::map<const std::string, double> values;
};

}


TEST(sensorreader_suite, basics)
{
    MockLogger logger;
    MockValueStore store;
    SensorReader::Sensors sensors {
        {"sensor1", new ConstantSensor(1) },
        {"sensor2", new ConstantSensor(2) },
    };

    SensorReader rdr(sensors, logger, store);
    rdr.doit();

    ASSERT_FLOAT_EQ(store.values["sensor1"], 1);
    ASSERT_FLOAT_EQ(store.values["sensor2"], 2);
}
$ ./heating-tests
Running main() from /home/jfasch/work/jfasch-home/googletest/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from sensorreader_suite
[ RUN      ] sensorreader_suite.basics
[       OK ] sensorreader_suite.basics (0 ms)
[----------] 1 test from sensorreader_suite (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.
$ valgrind ./heating-tests
==141320== Memcheck, a memory error detector
==141320== HEAP SUMMARY:
==141320==     in use at exit: 32 bytes in 2 blocks
==141320==   total heap usage: 204 allocs, 202 frees, 113,874 bytes allocated