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

"Избегайте возврата дескрипторов к внутренним объектам", так что альтернатива?

Эффективный С++ от Scott Meyers рассказывает в главе 5, Item 28, чтобы избежать возврата "дескрипторов" (указателей, ссылок или итераторов) к объектным внутренним элементам и это определенно делает хороший момент.

т.е. не делайте этого:

class Family
{
public:
    Mother& GetMother() const;
}

поскольку он уничтожает инкапсуляцию и позволяет изменять частные члены объекта.

Даже не делайте этого:

class Family
{
public:
    const Mother& GetMother() const;
}

потому что это может привести к "оборванным ручкам", что означает, что вы сохраняете ссылку на элемент объекта, который уже уничтожен.

Теперь, мой вопрос: есть ли хорошие альтернативы? Представьте, что Мать тяжелая! Если теперь я верну копию копии вместо ссылки, GetMother станет довольно дорогостоящей операцией.

Как вы обрабатываете такие случаи?

4b9b3361

Ответ 1

Сначала позвольте мне повторить итерацию: самая большая проблема не в жизни, а в инкапсуляции.

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

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

Простейшим решением является возврат по значению:

class Family {
public:

    Mother mother() { return _mother; }
    void mother(Mother m) { _mother = m; }

private:
    Mother _mother;
};

Поскольку на следующей итерации я могу удалить _mother без нарушения интерфейса:

class Family {
public:

     Mother mother() { return Mother(_motherName, _motherBirthDate); }

     void mother(Mother m) {
         _motherName = m.name();
         _motherBirthDate = m.birthDate();
     }

private:
     Name _motherName;
     BirthDate _motherBirthDate;
};

Посмотрите, как мне удалось полностью перестроить внутренности, не меняя интерфейс на iota? Легкий Peasy.

Примечание: очевидно, что это преобразование только для эффекта...

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

Ответ 2

Возможные решения зависят от фактического дизайна ваших классов и того, что вы считаете "внутренними объектами".

  • Mother представляет собой только деталь реализации Family и может быть полностью скрыта от пользователя Family
  • Family рассматривается как состав других общественных объектов.

В первом случае вы должны полностью инкапсулировать подобъект и предоставить ему доступ только через Family членов функции (возможно, дублируя открытый интерфейс Mother):

class Family
{
  std::string GetMotherName() const { return mommy.GetName(); }
  unsigned GetMotherAge() const { return mommy.GetAge(); }
  ...
private:
   Mother mommy;
   ...
};

Ну, может быть скучно, если интерфейс Mother довольно велик, но, возможно, это проблема дизайна (хорошие интерфейсы должны иметь 3-5-7 членов), и это заставит вас снова и снова переработать его.

Во втором случае вам все равно нужно вернуть весь объект. Есть две проблемы:

  • Пробой инкапсуляции (код конечного пользователя будет зависеть от определения Mother)
  • Проблема с владением (оборванные указатели/ссылки)

Чтобы обратиться к проблеме 1 используйте интерфейс вместо определенного класса, для решения проблемы 2 используйте совместное или слабое владение:

class IMother
{
   virtual std::string GetName() const = 0;
   ...
};

class Mother: public IMother
{
   // Implementation of IMother and other stuff
   ...
};

class Family
{
   std::shared_ptr<IMother> GetMother() const { return mommy; }
   std::weak_ptr<IMother> GetMotherWeakPtr() const { return mommy; }

   ...
private:
   std::shared_ptr<Mother> mommy;
   ...
};

Ответ 3

Если просмотр только для чтения - это то, что вам нужно, и по какой-то причине вам нужно избегать оборванных ручек, тогда вы можете рассмотреть возможность возврата shared_ptr<const Mother>.

Таким образом, объект Mother может выжить из объекта Family. Который также должен хранить его shared_ptr, конечно.

Часть рассмотрения состоит в том, собираетесь ли вы создавать циклы ссылок, используя слишком много shared_ptr s. Если да, то вы можете рассмотреть weak_ptr, и вы также можете просто принять возможность оборванных ручек, но написать код клиента, чтобы избежать этого. Например, никто не слишком беспокоится о том, что std::vector::at возвращает ссылку, которая становится устаревшей при уничтожении вектора. Но тогда контейнеры являются крайним примером класса, который преднамеренно предоставляет объекты, которым он "владеет".

Ответ 4

Это относится к основному принципу OO:

Tell objects what to do rather than doing it for them.

Вам нужно Mother сделать что-нибудь полезное? Попросите объект Family сделать это за вас. Передайте ему любые внешние зависимости, завернутые в приятный интерфейс (Class в С++) через параметры метода в объекте Family.

Ответ 5

потому что это может привести к "оборванным ручкам", что означает, что вы держите ссылку на элемент объекта, который уже уничтожен.

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

Ответ 6

Это просто вопрос семантики. В вашем случае Mother не является Family внутренними, а не деталями его реализации. Mother экземпляр класса можно ссылаться в Family, а также во многих других объектах. Более того, время жизни экземпляра Mother может даже не коррелировать с временем жизни Family.

Таким образом, лучший дизайн будет хранить в Family a shared_ptr<Mother> и без проблем выставлять его в интерфейсе Family.