Dependency injection in C++11 without raw pointers

可紊 提交于 2019-12-03 05:57:28
Yakk - Adam Nevraumont

If I wanted to do this, the first thing I'd do is kill your interface, and instead use this:

SomeAlgorithm(std::function<double(Point,Point)> distanceCalculator_)

type erased invocation object.

I could do a drop-in replacement using your EuclideanDistanceCalculator like this:

std::function<double(Point,Point)> UseEuclidean() {
  auto obj = std::make_shared<EuclideanDistance>();
  return [obj](Point a, Point b)->double {
    return obj->distance( a, b );
  };
}
SomeAlgorithm foo( UseEuclidean() );

but as distance calculators rarely require state, we could do away with the object.

With C++1y support, this shortens to:

std::function<double(Point,Point>> UseEuclidean() {
  return [obj = std::make_shared<EuclideanDistance>()](Point a, Point b)->double {
    return obj->distance( a, b );
  };
}

which as it no longer requires a local variable, can be used inline:

SomeAlgorithm foo( [obj = std::make_shared<EuclideanDistance>()](Point a, Point b)->double {
    return obj->distance( a, b );
  } );

but again, the EuclideanDistance doesn't have any real state, so instead we can just

std::function<double(Point,Point>> EuclideanDistance() {
  return [](Point a, Point b)->double {
    return sqrt( (b.x-a.x)*(b.x-a.x) + (b.y-a.y)*(b.y*a.y) );
  };
}

If we really don't need movement but we do need state, we can write a unique_function< R(Args...) > type that does not support non-move based assignment, and store one of those instead.

The core of this is that the interface DistanceCalculator is noise. The name of the variable is usually enough. std::function< double(Point,Point) > m_DistanceCalculator is clear in what it does. The creator of the type-erasure object std::function handles any lifetime management issues, we just store the function object by value.

If your actual dependency injection is more complicated (say multiple different related callbacks), using an interface isn't bad. If you want to avoid copy requirements, I'd go with this:

struct InterfaceForDependencyStuff {
  virtual void method1() = 0;
  virtual void method2() = 0;
  virtual int method3( double, char ) = 0;
  virtual ~InterfaceForDependencyStuff() {}; // optional if you want to do more work later, but probably worth it
};

then, write up your own make_unique<T>(Args&&...) (a std one is coming in C++1y), and use it like this:

Interface:

SomeAlgorithm(std::unique_ptr<InterfaceForDependencyStuff> pDependencyStuff)

Use:

SomeAlgorithm foo(std::make_unique<ImplementationForDependencyStuff>( blah blah blah ));

If you don't want virtual ~InterfaceForDependencyStuff() and want to use unique_ptr, you have to use a unique_ptr that stores its deleter (by passing in a stateful deleter).

On the other hand, if std::shared_ptr already comes with a make_shared, and it stores its deleter statefully by default. So if you go with shared_ptr storage of your interface, you get:

SomeAlgorithm(std::shared_ptr<InterfaceForDependencyStuff> pDependencyStuff)

and

SomeAlgorithm foo(std::make_shared<ImplementationForDependencyStuff>( blah blah blah ));

and make_shared will store a pointer-to-function that deletes ImplementationForDependencyStuff that will not be lost when you convert it to a std::shared_ptr<InterfaceForDependencyStuff>, so you can safely lack a virtual destructor in InterfaceForDependencyStuff. I personally would not bother, and leave virtual ~InterfaceForDependencyStuff there.

In most cases you don't want or need ownership transfer, it makes code harder to understand and less flexible (moved-from objects can't be reused). The typical case would be to keep ownership with the caller:

class SomeAlgorithm {
    DistanceCalculator* distanceCalculator;

public:
    explicit SomeAlgorithm(DistanceCalculator* distanceCalculator_)
        : distanceCalculator(distanceCalculator_) {
        if (distanceCalculator == nullptr) { abort(); }
    }

    double calculateComplicated() {
        ...
        double dist = distanceCalculator->distance(p1, p2);
        ...
    }

    // Default special members are fine.
};

int main() {
    EuclideanDistanceCalculator distanceCalculator;
    SomeAlgorithm algorithm(&distanceCalculator);
    algorithm.calculateComplicated();
}

Raw pointers are fine to express non-ownership. If you prefer you can use a reference in the constructor argument, it makes no real difference. However, don't use a reference as data member, it makes the class unnecessarily unassignable.

The down side of just using any pointer (smart or raw), or even an ordinary C++ reference, is that they allow calling non-const methods from a const context.

For stateless classes with a single method that is a non-issue, and std::function is a good alternative, but for the general case of classes with state or multiple methods I propose a wrapper similar but not identical to std::reference_wrapper (which lacks the const safe accessor).

  template<typename T>
  struct NonOwningRef{
    NonOwningRef() = delete;
    NonOwningRef(T& other) noexcept : ptr(std::addressof(other)) { };
    NonOwningRef(const NonOwningRef& other) noexcept = default;

    const T& value() const noexcept{ return *ptr; };
    T& value() noexcept{ return *ptr; };

  private:
    T* ptr;
  };

usage:

   class SomeAlgorithm {
      NonOwningRef<DistanceCalculator> distanceCalculator;

   public:
      SomeAlgorithm(DistanceCalculator& distanceCalculator_)
        : distanceCalculator(distanceCalculator_) {}

      double calculateComplicated() {

         double dist = distanceCalculator.value().distance(p1, p2);
         return dist;
    }
};

Replace T* with unique_ptr or shared_ptr to get owning versions. In this case, also add move construction, and construction from any unique_ptr<T2> or shared_ptr<T2> ).

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!