Подтвердить что ты не робот

Использование ссылки в качестве классов для зависимостей

Я возвращаюсь к C++ после того, как провожу некоторое время на языках, управляемых памятью, и внезапно теряюсь в том, что является лучшим способом реализации внедрения зависимостей. (Я полностью продан DI, потому что я нашел, что это самый простой способ сделать дизайн, управляемый тестами, очень простым).

Теперь просмотр SO и Google принес мне довольно много мнений по этому вопросу, и я немного запутался.

В ответ на этот вопрос, Внедрение зависимостей в C++, кто-то предложил вам не передавать необработанные указатели даже для внедрения зависимостей. Я понимаю, что это связано с владением объектами.

Теперь право собственности на объекты также рассматривается (хотя и не достаточно подробно в моем состоянии;)) в печально известном руководстве по стилю Google: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Smart_Pointers

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

class Addict {
   // Something I depend on (hence, the Addict name. sorry.)
   Dependency * dependency_;
public:
   Addict(Dependency * dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_->do_something();
   }
   // ... whatever ... 
};    

Если Dependency - это чисто виртуальный класс (так называемый Интерфейс бедняка), тогда этот код облегчает внедрение фиктивной версии Dependency (используя что-то вроде google mock).

Проблема в том, что я на самом деле не вижу проблем, с которыми я могу столкнуться с таким кодом, и почему я должен хотеть использовать что-то еще, кроме сырых указателей! Разве не ясно, откуда взялась зависимость?

Кроме того, я прочитал довольно много постов, намекающих на то, что в этой ситуации действительно следует использовать ссылки, так что этот код лучше?

class Addict {
   // Something I depend on (hence, the Addict name. sorry.)
   const Dependency & dependency_;
  public:
   Addict(const Dependency & dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_.do_something();
   }
   // ... whatever ... 
};

Но потом я получаю и другие, не менее авторитетные советы против использования ссылок в качестве участника: http://billharlan.com/pub/papers/Managing_Cpp_Objects.html

Как вы видите, я не совсем уверен в относительных плюсах и минусах различных подходов, поэтому я немного запутался. Я сожалею, если это обсуждалось до смерти, или если это только вопрос личного выбора и последовательности внутри данного проекта... но любая идея приветствуется.


Резюме ответов

(Я не знаю, является ли это хорошим SO-тикетом для этого, но я добавлю пример кода для того, что я собрал из ответов...)

Из различных ответов вот что я, вероятно, в конечном итоге буду делать в моем случае:

  • передать зависимости как ссылку (по крайней мере, чтобы убедиться, что NULL невозможен)
  • в общем случае, когда копирование невозможно, явно запретите его и сохраните зависимости как ссылку
  • в более редком случае, когда копирование возможно, сохраняйте зависимости как RAW-указатели
  • пусть создатель зависимостей (какой-то фабрики) выбирает между выделением стека или динамическим размещением (и в последнем случае управление через интеллектуальный указатель)
  • установить соглашение об отделении зависимостей от собственных ресурсов

Таким образом, я хотел бы получить что-то вроде:

class NonCopyableAddict {
    Dependency & dep_dependency_;

    // Prevent copying
    NonCopyableAddict & operator = (const NonCopyableAddict & other) {}
    NonCopyableAddict(const NonCopyableAddict & other) {}

public:
    NonCopyableAddict(Dependency & dependency) : dep_dependency_(dep_dependency) {
    }
    ~NonCopyableAddict() {
      // No risk to try and delete the reference to dep_dependency_ ;)
    }
    //...
    void do_some_stuff() {
      dep_dependency_.some_function();
    }
};

И для копируемого класса:

class CopyableAddict {
    Dependency * dep_dependency_;

public: 
    // Prevent copying
    CopyableAddict & operator = (const CopyableAddict & other) {
       // Do whatever makes sense ... or let the default operator work ? 
    }
    CopyableAddict(const CopyableAddict & other) {
       // Do whatever makes sense ...
    }


    CopyableAddict(Dependency & dependency) : dep_dependency_(&dep_dependency) {
    }
    ~CopyableAddict() {
      // You might be tempted to delete the pointer, but its name starts with dep_, 
      // so by convention you know it is not your job
    }
    //...
    void do_some_stuff() {
      dep_dependency_->some_function();
    }
};

Из того, что я понял, нет никакого способа выразить намерение "у меня есть указатель на некоторые вещи, но я им не владею", которое может применять компилятор. Поэтому мне придется прибегнуть к соглашению об именах здесь...


Хранится для справки

Как указал Мартин, следующий пример не решает проблему.

Или, если у меня есть конструктор копирования, что-то вроде:

class Addict {
   Dependency dependency_;
  public:
   Addict(const Dependency & dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_.do_something();
   }
   // ... whatever ... 
};
4b9b3361

Ответ 1

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

Для людей, которые требуют примеров: потоки всегда передаются и сохраняются как ссылки (поскольку они не могут быть скопированы).

Некоторые комментарии к примерам кода:

Пример один и два

Ваш первый пример с указателями. Является в основном тем же, что и второй пример с использованием ссылок. Разница заключается в том, что ссылка не может быть NULL. Когда вы передаете ссылку, объект уже жив и, следовательно, должен иметь продолжительность жизни больше, чем объект, который вы уже тестируете (если он был создан в стеке), поэтому должно быть безопасно хранить ссылку. Если вы динамически создаете указатели в качестве зависимостей, я бы подумал об использовании boost:: shared_pointer или std:: auto_ptr в зависимости от того, является ли принадлежность к зависимости общей или нет.

Пример три:

Я не вижу большого смысла для вашего третьего примера. Это связано с тем, что вы не можете использовать полиморфные типы (если вы передаете объект, полученный из Dependency, он будет нарезан во время операции копирования). Таким образом, код может также быть внутри Addict, а не отдельным классом.

Билл Харлен: (http://billharlan.com/pub/papers/Managing%5FCpp%5FObjects.html)

Не брать ничего от Билла Но:

  • Я никогда не слышал о нем.
    • Он - геофизик, а не программист.
    • Он рекомендует программировать на Java, чтобы улучшить ваш С++
    • Языки теперь настолько различны в использовании, что это абсолютно ложно).
    • Если вы хотите использовать ссылки What To-do/not-do.
      Затем я бы выбрал одно из больших имен в поле С++:
      Страуструп/Sutter/Alexandrescu/Мейерс

Резюме:

  • Не используйте RAW-указатели (если требуется владение)
  • Использовать интеллектуальные указатели.
  • Не копируйте объекты в свой объект (он будет срезать).
  • Вы можете использовать ссылки (но знаете ограничения).

Мой пример инъекции зависимостей с использованием ссылок:

class Lexer
{
    public: Lexer(std::istream& input,std::ostream& errors);
    ... STUFF
    private:
       std::istream&  m_input;
       std::ostream&  m_errors;
};
class Parser
{
    public: Parser(Lexer& lexer);
    ..... STUFF
    private:
        Lexer&        m_lexer;
};

int main()
{
     CLexer  lexer(std::cin,std::cout);  // CLexer derived from Lexer
     CParser parser(lexer);              // CParser derived from Parser

     parser.parse();
}

// In test.cpp
int main()
{
     std::stringstream  testData("XXXXXX");
     std::stringstream  output;
     XLexer  lexer(testData,output);
     XParser parser(lexer);

     parser.parse();
}

Ответ 2

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

В глубине:

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

Во-вторых, тип указателя/ссылки, передаваемый в функции и конструкторы, должен указывать, кто несет ответственность за освобождение объекта и как он должен быть освобожден:

  • std::auto_ptr - вызываемая функция отвечает за освобождение и будет делать это автоматически, когда это будет сделано. Если вам нужна семантика копирования, интерфейс должен предоставить метод clone, который должен возвращать auto_ptr.

  • std::shared_ptr - вызываемая функция отвечает за освобождение и будет делать это автоматически, когда это будет сделано и когда все другие ссылки на объект исчезнут. Если вам нужна семантика поверхностного копирования, сгенерированные компилятором функции будут в порядке, если вам нужно глубокое копирование, интерфейс должен предоставить метод clone, который должен возвращать shared_ptr.

  • Справка - звонящий несет ответственность. Вам все равно - объект может быть выделен для всего, что вы знаете. В этом случае вы должны передать по ссылке, но сохранить по указателю. Если вам нужна семантика поверхностного копирования, функции, сгенерированные компилятором, будут в порядке, если вам нужно глубокое копирование, у вас проблемы.

  • Необработанный указатель. Кто знает? Может быть выделен в любом месте. Может быть нулевым Вы можете нести ответственность за его освобождение, а можете и нет.

  • Любой другой умный указатель - он должен управлять временем жизни за вас, но вам нужно будет просмотреть документацию, чтобы узнать, каковы требования к копированию.

Обратите внимание, что методы, которые дают вам ответственность за освобождение объекта, не нарушают DI - освобождение объекта - это просто часть контракта, который у вас есть с интерфейсом (поскольку вам не нужно ничего знать о конкретном типе, чтобы освободить его).

Ответ 3

[обновление 1]
Если вы всегда можете гарантировать, что зависимость переживает наркомана, вы можете использовать необработанный указатель/ссылку, конечно. между этими двумя, я бы принял очень простое решение: указатель, если NULL разрешен, ссылайтесь иначе.

(Пункт моего первоначального сообщения состоял в том, что ни указатель, ни ссылка не решают проблему времени жизни)


Я бы следил за позорным руководством по стилю google и использовал интеллектуальные указатели.

И указатель, и ссылка имеют одну и ту же проблему: вам нужно убедиться, что зависимость переживает наркомана. Это налагает на клиента довольно неприятную ответственность.

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

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

Типичные проблемы интеллектуальных указателей - накладные и круговые ссылки - редко вступают в игру здесь. Обычно экземпляры зависимостей не являются крошечными и многочисленными, и зависимость, которая имеет сильную ссылку на своих наркоманов, является по меньшей мере запахом кода. (все же вам нужно помнить об этих вещах. Добро пожаловать на С++)

Предупреждение: я не "полностью продан" DI, но я полностью продаюсь по умным указателям;)

[обновление 2]
Обратите внимание, что вы всегда можете создать shared_ptr в стек/глобальный экземпляр, используя нулевой дебетер.
Это требует, чтобы обе стороны поддерживали это, хотя: наркоман должен делать гарантии, что он не передаст ссылку на зависимость от кого-то еще, кто может прожить дольше, а звонящий вернулся с ответственностью, гарантирующей всю жизнь. Я не доволен этим решением, но иногда использовал это.

Ответ 4

Я бы избегал ссылок как членов, поскольку они, как правило, не вызывают головных болей, если вы в конечном итоге вставляете один из ваших объектов в контейнер STL. Я хотел бы изучить комбинацию boost::shared_ptr для владения и boost::weak_ptr для иждивенцев.

Ответ 5

Это было задано раньше, но мои навыки поиска SO не доходят до его поиска. Чтобы подвести итог моей позиции, вы должны очень редко, если вообще когда-либо, использовать ссылки в качестве членов класса. Это приводит к разным проблемам инициализации, назначения и копирования. Вместо этого используйте указатель или значение.

Изменить: Нашел один - это вопрос с множеством мнений в качестве ответов: Должен ли я предпочитать указатели или ссылки в данных членов?

Ответ 6

Но затем я получаю другие, одинаково авторитетные советы против использования ссылок как члена: http://billharlan.com/pub/papers/Managing%5FCpp%5FObjects.html

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

Ответ 7

Я могу здесь спуститься вниз, но я скажу, что в классе не должно быть никаких ссылочных членов ДЛЯ ЛЮБОЙ РЕАСО, КОГДА-ЛИБО. За исключением случаев, когда они являются простым постоянным значением. Причин для этого много, во-вторых, вы начинаете это, вы открываете все плохие вещи на С++. Посмотрите мой блог, если вам действительно все равно.