Search code examples
c++delegatesdomain-driven-designaggregaterootlaw-of-demeter

How to write read-only accessor functions in an aggregate root class?


Overall design: I have an aggregate class C that contains N member variables of type M_i, i = 1 ... N that each have a common write-only update() interface as well as class-specific read-only accessor functions [F]un_i(), [F] = any letter, i = 1 .. N (they do not have such regular names in reality). Each of the member types M_i forms an independent abstraction of its own, and is used elsewhere in my program.

The aggregate class needs to update all the members in a single transaction, so it has an update() function of its own calling the update() member function of all its member variables.

// building blocks M_i, i = 1 ... N

class M_1
{
public:
    // common write-only interface
    void update();

    // M_1 specific read-only interface
    int fun_1() const;
    // ...
    int fun_K() const;

private:
    // impl
};

// ...

class M_N
{
public:
    // common write-only interface
    void update();

    // M_N specific read-only interface
    int gun_1() const;
    // ...
    int gun_K() const;

private:
    // impl
};

// aggregate containing member variables M_i, i = 1 ... N
class C
{
public:
    // update all members in a single transaction
    void update() 
    {
        m1_.update();
        // ...
        mN_.update();
    }

    // read-only interface?? see below

private:
    M_1 m1_;
    // ...
    M_N mN_;
};

Question: the do I access the various member functions of the various member variables in the aggregate class? I can think of three alternatives:

Alternative 1: write N * K delegates to all K member functions of all N member variables

class C
{
    int fun_1() const { return m1_.fun_1(); }
    // ...
    int fun_K() const { return m1_.fun_K(); }

    // ...

    int gun_1() const { return mN_.gun_1(); }
    // ...
    int gun_K() const { return mN_.gun_K(); }

    // as before
};

int res = C.fun_5(); // call 5th member function of 1st member variable 

Alternative 2: write N accessors to all N member variables

class C
{
    M_1 const& m1() const { return m1_; }

    // ...

    M_N const& mN() const { return mN_; }

    // as before
};

int res = C.m1().fun_5(); // call 5th member function of 1st member variable

Alternative 3: write 1 accessor template to all N member variables

class C
{
public:
    enum { m1, /* ... */ mN };

    template<std::size_t I>
    auto get() const -> decltype(std::get<I>(data_)) 
    { 
        return std::get<I>(data_); 
    }

private:
    std::tuple<M_1, /* ... */ M_N> data_;
};

int res = C.get<m1>().fun_5(); // call 5th member function of 1st member variable

Alternative 1 avoids violating the Law of Demeter but it needs an awful lot of tedious boiler plate code (in my application, N = 5 and K = 3, so 15 delegating wrappers). Alternative 2 cuts down on the number of wrappers, but the calling code feels a little uglier to me. But since all that code is read-only, and modfications can only happen through the consistent aggregate update(), my current opinion that Alternative 2 is preferable to Alternative 1 (and at least safe). If that's the case, then a fortiori, Alternative 3 should be the best choice since it uses only a single accessor and has the same safety guarantees as Alternative 2.

Question: what is the preferred interface for this type of code?


Solution

  • Turning my comment into an answer.

    If you decide to go with alternative 1 (N*K delegates), you can use Boost.Preprocessor to do the boilerplate work for you:

    #include <boost/preprocessor.hpp>
    
    // Define identifier names
    
    #define FUNCTIONS (fun)(gun)(hun)
    
    #define MEMBER_NAMES (m1_)(m2_)(m3_)
    
    #define SUFFIXES (_1)(_2)(_3)
    
    
    // Utility "data structure"
    // Used to hand down state from iteration over functions to iteration over suffixes
    
    #define WRAP_DATA(function, member) \
      (2, (function, member))
    
    #define UNWRAP_DATA_FUNTION(data) \
      BOOST_PP_ARRAY_ELEM(0, data)
    
    #define UNWRAP_DATA_MEMBER(data) \
      BOOST_PP_ARRAY_ELEM(1, data)
    
    
    // Accessor-generating functionality
    
      // Convenience macro for generating the correct accessor name
    #define CREATE_FUNCTION_NAME(data, suffix) \
      BOOST_PP_CAT(UNWRAP_DATA_FUNCTION(data), suffix)
    
      // Macro generating one accessor delegation
    #define GENERATE_ACCESSOR(r, data, suffix) \
      int CREATE_FUNCTION_NAME(data, suffix) () const { return UNWRAP_DATA_MEMBER(data).CREATE_FUNCTION_NAME(data, suffix) (); }
    
    
    // Generate accessors
    
    class C
    {
    
      // Execute GENERATE_ACCESSOR once for each element of SUFFIXES
    #define BOOST_PP_LOCAL_MACRO(iter) \
      BOOST_PP_SEQ_FOR_EACH(GENERATE_ACCESSOR, WRAP_DATA(BOOST_PP_SEQ_ELEM(iter, FUNCTIONS), BOOST_PP_SEQ_ELEM(iter, MEMBER_NAMES)), SUFFIXES)
    
    #define BOOST_PP_LOCAL_LIMITS (0, BOOST_PP_SEQ_SIZE(FUNCTIONS) - 1)
    
      // Execute BOOST_PP_LOCAL_MACRO once for each value within BOOST_PP_LOCAL_LIMITS
    #include BOOST_PP_LOCAL_ITERATE()
    
    // rest of class C here
    // ...
    
    };
    

    Translated into pseudo-code to better highlight the working logic:

    FUNCTIONS = {fun, gun, hun};
    MEMBER_NAMES = {m1_, m2_, m3_};
    SUFFIXES = {_1, _2, _3};
    
    struct Data {
      auto function, member;
    };
    
    auto createFunctionName(data, suffix) {
      return data.function + suffix;
    }
    
    auto generateAccessor(data, suffix) {
      return "int " + createFunctionName(data, suffix) + "() const { return " + data.member + "." + createFunctionName(data, suffix) + "(); }";
    }
    
    
    class C
    {
    
    for (i = 0; i < sizeof(FUNCTIONS); ++i) {
      foreach (suffix in SUFFIXES) {
        generateAccessor(Data(FUNCTIONS[i], MEMBER_NAMES[i]), suffix);
      }
    }
    
    };