Инъекция зависимостей в С++ 11 без исходных указателей - программирование
Подтвердить что ты не робот

Инъекция зависимостей в С++ 11 без исходных указателей

Я часто использую шаблон "зависимость" в моих проектах. В С++ его проще всего реализовать, перейдя по необработанным указателям, но теперь с С++ 11 все в высокоуровневом коде должно выполняться с помощью интеллектуальных указателей. Но какова наилучшая практика для этого случая? Производительность не имеет решающего значения, теперь чистый и понятный код имеет значение для меня.

Позвольте мне показать упрощенный пример. У нас есть алгоритм, который использует вычисления расстояния внутри. Мы хотим иметь возможность заменить этот расчет на разные метрики расстояния (Евклида, Манхэттен и т.д.). Наша цель - сказать что-то вроде:

SomeAlgorithm algorithmWithEuclidean(new EuclideanDistanceCalculator());
SomeAlgorithm algorithmWithManhattan(new ManhattanDistanceCalculator());

но с умными указателями, чтобы избежать ручных new и delete. Это возможная реализация с исходными указателями:

class DistanceCalculator {
public:
    virtual double distance(Point p1, Point p2) = 0;
};

class EuclideanDistanceCalculator {
public:
    virtual double distance(Point p1, Point p2) {
        return sqrt(...);
    }
};

class ManhattanDistanceCalculator {
public:
    virtual double distance(Point p1, Point p2) {
        return ...;
    }
};

class SomeAlgorithm {
    DistanceCalculator* distanceCalculator;

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

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

    ~SomeAlgorithm(){
        delete distanceCalculator;
    }
};

Предположим, что копирование на самом деле не является проблемой, и если нам не нужен полиморфизм, мы просто передадим DistanceCalculator конструктору SomeAlgorithm по значению (копирование). Но так как мы должны иметь возможность проходить в разных производных экземплярах (без нарезки), параметр должен быть либо необработанным указателем, либо ссылкой, либо умным указателем.

Одно из решений, которое приходит на ум, - передать его ссылкой-на-const и инкапсулировать его в переменную-член std::unique_ptr<DistanceCalculator>. Тогда вызов будет:

SomeAlgorithm algorithmWithEuclidean(EuclideanDistance());

Но этот временный объект, распределенный по стеку (rvalue-reference?), будет разрушен после этой строки. Поэтому нам нужно некоторое копирование, чтобы сделать его более похожим на пропускную стоимость. Но поскольку мы не знаем тип среды выполнения, мы не можем легко создать нашу копию.

Мы могли бы также использовать интеллектуальный указатель как параметр конструктора. Поскольку нет права собственности (DistanceCalculator будет принадлежать SomeAlgorithm), мы должны использовать std::unique_ptr. Должен ли я заменить все такие параметры конструктора на unique_ptr? это уменьшает читаемость. Также пользователь SomeAlgorithm должен построить его неловко:

SomeAlgorithm algorithmWithEuclidean(std::unique_ptr<DistanceCalculator>(new EuclideanDistance()));

Или я должен каким-то образом использовать новую семантику перемещения (& &, std:: move)?

Кажется, это довольно стандартная проблема, должен быть какой-то краткий способ ее реализации.

4b9b3361

Ответ 1

Если бы я хотел сделать это, первое, что я сделал бы, это убить ваш интерфейс и вместо этого использовать это:

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

тип стираемого объекта вызова.

Я мог бы сделать замену с помощью вашего EuclideanDistanceCalculator следующим образом:

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() );

но поскольку калькуляторы расстояний редко требуют состояния, мы можем покончить с объектом.

С поддержкой С++ 1y это сокращается до:

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

который, поскольку он больше не требует локальной переменной, может использоваться inline:

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

но опять же, EuclideanDistance не имеет никакого реального состояния, поэтому вместо этого мы можем просто

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

Если нам действительно не нужно движение, но нам нужно состояние, мы можем написать тип unique_function< R(Args...) >, который не поддерживает назначение без перемещения и вместо этого сохранит один из них.

Ядро этого заключается в том, что интерфейс DistanceCalculator представляет собой шум. Обычно имя переменной достаточно. std::function< double(Point,Point) > m_DistanceCalculator ясно, что он делает. Создатель объекта type-erasure std::function обрабатывает любые проблемы управления жизненным циклом, мы просто сохраняем объект function по значению.

Если ваша фактическая инъекция зависимостей сложнее (скажем, несколько разных связанных обратных вызовов), использование интерфейса неплохо. Если вы хотите избежать требований к копированию, я бы пошел с этим:

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

тогда напишите свой собственный make_unique<T>(Args&&...) (a std, который входит в С++ 1y), и используйте его следующим образом:

Интерфейс:

SomeAlgorithm(std::unique_ptr<InterfaceForDependencyStuff> pDependencyStuff)

Использование:

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

Если вы не хотите virtual ~InterfaceForDependencyStuff() и хотите использовать unique_ptr, вы должны использовать unique_ptr, который хранит свой делеттер (путем передачи в дезактивированном состоянии).

С другой стороны, если std::shared_ptr уже поставляется с make_shared, и, он сохраняет свой отказ по умолчанию по умолчанию. Поэтому, если вы идете с shared_ptr хранилищем вашего интерфейса, вы получаете:

SomeAlgorithm(std::shared_ptr<InterfaceForDependencyStuff> pDependencyStuff)

и

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

и make_shared будут храниться указатель к функции, который удаляет ImplementationForDependencyStuff, который не будет потерян при преобразовании его в std::shared_ptr<InterfaceForDependencyStuff>, поэтому вы можете без проблем удалить деструктор virtual в InterfaceForDependencyStuff. Я лично не стал бы беспокоиться и оставил там virtual ~InterfaceForDependencyStuff.

Ответ 2

В большинстве случаев вы не хотите или не нуждаетесь в передаче прав собственности, это делает код более понятным и менее гибким (перемещенные объекты не могут быть повторно использованы). Типичным случаем будет сохранение права собственности на вызывающего абонента:

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

Необработанные указатели могут содержать несоблюдение. Если вы предпочитаете использовать ссылку в аргументе конструктора, это не имеет никакого значения. Однако, не используйте ссылку как элемент данных, это делает класс излишне нецелесообразным.

Ответ 3

Нижняя сторона использования любого указателя (умного или необработанного) или даже обычной ссылки на С++ заключается в том, что они позволяют вызывать неконстантные методы из контекста const.

Для классов без состояния с единственным методом, который не является проблемой, а std::function является хорошей альтернативой, но для общего случая классов с состоянием или несколькими методами я предлагаю оболочку, похожую, но не идентичную std::reference_wrapper (в котором отсутствует безопасный аксессуар).

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

использование:

   class SomeAlgorithm {
      NonOwningRef<DistanceCalculator> distanceCalculator;

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

      double calculateComplicated() {

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

Замените T* на unique_ptr или shared_ptr, чтобы получить принадлежащие версии. В этом случае также добавьте конструкцию перемещения и конструкцию из любых unique_ptr<T2> или shared_ptr<T2>).