Are there any alternatives to dependency injection for stateless classes?

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

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;
    }
}

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.

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

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).

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *