Injection versus Encapsulation

We (justly) hear a lot about dependency injection, and in many cases straightforward dependency injection, whether managed via a tool like Swing or just manually (as you have to do in C++) is clearly the best thing to do.  In most of these clear cases the dependency is public and shared: one common call will not only be economical in terms of code, but will allow the class into which the dependency is being injected to become less tightly coupled to its environment.

But there are some interesting edge cases.  Excluding those cases where a strategy is purely private to a class -- that is, it is a resource exactly like one which might be injected, but it is properly an implementation detail  of that class and, if visible outside the class, is so purely to allow it to be tested independently[1] -- we have cases where although the resource is not unique to one class its implementation will be different in some way locally.

[1]Testability is one reason that one should think of making such classes public.  The flip side is that a managing class with one or more strategies of this sort may be usefully modelled as a small module, moving the private aspects of the strategies one level up, with the strategies private to the module.

Here are two blocks of code initializing a resource of the same type:

1)

    m_actionScopeStrategy([&]() -> std::unique_ptr<IActionScopeStrategy> {

      if (SourceActionsArePrivate(inEncodedFlags))

        return std::make_unique<PrivateActionScopeStrategy>(inGenerators);

      else

        return std::make_unique<PublicActionScopeStrategy>(

            inGenerators.getBaseClassName(), inGenerators);

    }()),

2)

    m_actionScopeStrategy([&]() -> std::unique_ptr<IActionScopeStrategy> {

      if (SourceUsesActionFactory(inEncodedFlags))

        return std::make_unique<NullActionScopeStrategy>(inGenerators);

      else if (SourceActionsArePrivate(inEncodedFlags))

        return std::make_unique<PrivateActionScopeStrategy>(inGenerators);

      else

        return std::make_unique<PublicActionScopeStrategy>(

            inGenerators.getBaseClassName(), inGenerators);

    }()),

The inputs and types are common, but in one case two out of three concrete instances are used, and in the other three concrete instances are used.  The differences are derived naturally from the nature of the two classes using the resource.

Now, you could still model these two classes in terms of dependency injection.  If all they know is the interface to IActionScopeStrategy, it loosens the coupling of the classes.  But there is now a downside: logic which is strictly knowledge appropriate to the class internals is now being exported to allow for the appropriate construction of the resource.  We gain physical decoupling by weakening logical encapsulation.  Things that might need attention have an extra place to lurk when changes are made.

You can, in principle, design your way around it; the more elaborate case could have a wrapper holding the dependency executing the special logic which would otherwise the executed by choosing the NullActionScopeStrategy.  This can always be made to work, but it leads to more complex and less easily-understood designs in many if not most cases.

You can limit the scope of the exporting by wrapping the construction of the action scope strategy in a factory, and have different arguments passed in at these call sites:

  m_actionScopeStrategy(inFactory.generate(inEncodedFlags, inGenerators, false)),

2)

  m_actionScopeStrategy(inFactory.generate(inEncodedFlags, inGenerators, true)),

where true is a parameter meaning "enable null strategy":

class ActionScopeStrategyFactory {

public:

   std::unique_ptr<IActionScopeStrategy> generate(const int inEncodedFlags, const IActionGeneratorList &inGenerators, const bool inEnableNullStrategy)

    {

        if (inEnableNullStrategy)

{

            if (SourceUsesActionFactory(inEncodedFlags))

              return std::make_unique<NullActionScopeStrategy>(inGenerators);

            else if (SourceActionsArePrivate(inEncodedFlags))

              return std::make_unique<PrivateActionScopeStrategy>(inGenerators);

}

else if (SourceActionsArePrivate(inEncodedFlags))

              return std::make_unique<PrivateActionScopeStrategy>(inGenerators);

        return std::make_unique<PublicActionScopeStrategy>(inGenerators.getBaseClassName(), inGenerators);

    }

};

This is reasonable decoupling: the factory provides for all cases, the concrete classes provide the appropriate inputs, and we still have insulation from the concrete types in the main classes in question.  This is the classical "add another level of indirection" of all computer science problems.  You still may want to determine whether a factory is worth it at this scale: factories pay off for effort increasingly as the complexity of the construction logic increases.  These are fairly simple construction models.  In addition, these are the only two classes making use of the IActionScopeStrategy type.

(We can even clean up the call interface, since the inputs that are common can be hidden:

class ActionScopeStrategyFactory {

public:

      ActionScopeStrategyFactory(const int inEncodedFlags, const IActionGeneratorList &inGenerators):

        m_encodedFlags(inEncodedFlags), m_generators(inGenerators)

    { }

    std::unique_ptr<IActionScopeStrategy> generate(const bool inEnableNullStrategy) const

    {

        if (inEnableNullStrategy)

{

            if (SourceUsesActionFactory(m_encodedFlags))

              return std::make_unique<NullActionScopeStrategy>(m_generators);

            else if (SourceActionsArePrivate(m_encodedFlags))

              return std::make_unique<PrivateActionScopeStrategy>(m_generators);

}

else if (SourceActionsArePrivate(m_encodedFlags))

              return std::make_unique<PrivateActionScopeStrategy>(m_generators);

        return std::make_unique<PublicActionScopeStrategy>(m_generators.getBaseClassName(), m_generators);

    }

private:

    int m_encodedFlags;

    const IActionGeneratorList &m_generators;

};

and if we really want physical insulation from the mechanics of production:

class IActionScopeStrategyFactory {

public:

    virtual ~ActionScopeStrategyFactory();

    virtual std::unique_ptr<IActionScopeStrategy> generate(const bool inEnableNullStrategy) const = 0;

};

class ActionScopeStrategyFactory: public IActionScopeStrategyFactory {

...

and pass in the interface.

Or we could leave it as is.  If the containing classes have full test coverage, we can do effective testing even without the ability to have a mocked or traced object which injection gives us. Resources of time, energy, and attention are limited: there may be other loci where attention would better be bestowed.  And we just might decide in principle that at this scale for some particular project, it gives us more benefits to keep the logic fully encapsulated than to lose that encapsulation to get the marginally looser coupling.

Comments

Popular posts from this blog

Boundaries

Overview

Considerations on an Optimization