7

I am working on an application whereby I have designed classes to fit into several groups:

  • Immutable: initialised through constructors, uses copy-and-swap idiom (inc. move), can deep copy (i.e. clone), only has "getters" (not "setters") and implements comparison operators (==, !=);
  • Services: stateless classes that have methods that take immutables and/or services to perform desired functions;
  • Builders: factories etc to build immutables.

Unit testing on my immutables has been straightforward. I can use dependency injection through the constructors. This means I can swap in test classes to ensure I am unit testing (as opposed to integration testing). I can use the builders to construct my production objects. This element of my design I am happy with in terms of testing and production.

However, on my services I seem to only be able to keep these classes stateless and unit testable by using dependency injection via the method arguments.

For my services, an example function changes from:

virtual unsigned long foo() const override final;

...to:

virtual unsigned long foo(const ISomeInterface & dependency) const override final;

This means my service classes are testable, but then I have to now instantiate the dependencies outside of the class when using the code in production. For example:

// without dependency injection
Service service;
return service.foo();

// with dependency injection
Service service;
Dependency dependency;
return service.foo(dependency);

This has led to a large number of my service classes now needing at least 1 more argument for each class method. Note - this is the approach I am currently using in my code.

My question is this:

What alternatives do I have to this form of dependency injection that allow me to:

  • unit test stateless classes (without the dependencies)
  • keep these classes stateless
  • reduce / hide code that instantiates the dependencies (particularly if an object has multiple dependencies)

Note - I am also performing integration tests, which tests the real dependencies between objects.

13
  • 1
    Why is explicitly passing in dependencies a problem again?
    – Telastyn
    May 27, 2016 at 13:02
  • @Telastyn - The client calling these stateless classes now needs to know about the dependencies. Secondly, you lose a little readability when you bloat the arguments with dependency types (specifically when you have multiple dependencies). May 27, 2016 at 13:28
  • 5
    Right, I don't see those as problems. The client should know about dependencies, since they're likely to want to be able to change/specify them. And you should lose a little readability if you have too many dependencies - it's a signal that your function is doing too much.
    – Telastyn
    May 27, 2016 at 13:42
  • 2
    Do you really need pure stateless classes, or just client-stateless classes that are configurable?
    – Erik Eidt
    May 27, 2016 at 15:07
  • 1
    Consider changing the stateless class methods into namespace scoped functions. Jul 31, 2017 at 15:49

4 Answers 4

4

If you donot like the additional constructor arguments for the dependencies you need a DI-Container to handle the instance creation for you.

You can use either an existing di-container framework or implement a poor mans version on your own

public PoorMansDiContainer : IPoorMansDiContainer {
    private IService mService = null;
    private IFooService mFooService = null;

    public IService getSingletonService() {
        if (mService == null) {
            mService = new ServiceImpl(getSingletonFooService());
        }
        return mService;
    }
    public IFooService getSingletonFooService() {
        if (mFooService == null) {
            mFooService = new FooServiceImpl();
        }
        return mFooService;
    }
}
1

You could configure your stateless classes to refer directly to their dependencies by using templates. As an example:

// a dependency interface...
class ILogger
{
public:
    virtual void logSomething (std::string message);
};

// an interface for a service
class IMyService
{
public:
    virtual std::unique_ptr<MyClass> getMyObject () = 0;
};

template <LogProvider>
class MyServiceImpl : public IMyService
{
public:
     virtual std::unique_ptr<MyClass> getMyObject ()
     {
          return LogProvider::logger->logSomething ("creating my object");
          return std::make_unique<MyClass> ();
     }
};

// at global level
struct GetLogger 
{
    static std::shared_ptr<ILogger> logger;
}

// initialisation code...
GetLogger::logger = // ... make a concrete logger here
std::unique_ptr<IMyServce> myService = 
   std::make_unique<MyServiceImpl<GetLogger>> ();

You can then create instances of MyServiceImpl using any class you wish that contains a logger. This approach creates global data, but as it only uses it indirectly I don't see an issue with it.

Although, that said, I'd personally just abandon the notion of stateless service classes and just switch to immutable service classes instead, as it seams a much simpler solution.

0

Use a powerful service locator, which is, as Martin Fowler will tell you, nearly functionally equivalent to DI. Then set the service to whatever mock you like for the duration of the test in the context in which the test is running. e.g. (C# code, sorry)

public class Service()
{
  public void Foo()
  {
    ...
    IDependency dependency = ServiceLocator.Get<IDependency>();
    ...
  }
}

For the test

ServiceLocator.SetForThisThread<IDependency>(typeof(MockDependency));
RunTest();

If anyone would like to complain that you are then creating a dependency on ServiceLocator, I'd really like to understand how that could ever be a problem.

2
  • 1
    The problem isn't the dependency on ServiceLocator, the problem is the hidden dependency on "the service locator must have an IDependency available for this test". You can't discover this dependency by just looking at Service's interface. (Also, C# code in a question tagged C++ can be confusing if not called out as such.) May 28, 2016 at 11:23
  • Sure its useful to have the compiler stop you making a mistake because you didn't set up the hidden dependency, however you will find out you need it with a runtime error the first time you run it. That's a pretty small cost as all the enthusiasts of dynamically typed languages will tell you. It may well be worth paying in this case. May 28, 2016 at 13:24
0

I've managed to clean up my stateless classes by using default arguments. For example,

virtual unsigned long foo(
    const ISomeInterface & dependency = ProductionDependency()) const override final;

I appear to have met all the criteria in my original question...


unit test stateless classes (without the dependencies)

I can unit test by specifying the argument (for example, pass in a fake test object).

// unit test snippet
Service service;
TestDependency dependency;
const auto actual = service.foo(dependency);

keep these classes stateless

The stateless classes still only use the default constructors, and have no data members.


reduce / hide code that instantiates the dependencies (particularly if an object has multiple dependencies)

I needed to make the following changes:

  • The service base class headers now include the production dependency headers
  • The method arguments now include default arguments for each dependency
  • In some cases I needed to change the order of arguments to ensure the default arguments are the final arguments

However, I now no longer need to include these dependencies in my client code. For example,

// client code snippet
Service service;
return service.foo();

This is even the same if my Service class has multiple dependencies. As long as I am using default arguments I can use the same snippet as above. The clients no longer need to know about the dependencies (or more specifically, need to instantiate dependency objects because you get it free via default arguments).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.