Эффективный C ++ Скотт Мейерс в главе 5, пункт 28, говорит, что не следует возвращать «дескрипторы» (указатели, ссылки или итераторы) внутренним объектам, и это, безусловно, хорошо.
То есть не делай этого:
class Family
{
public:
Mother& GetMother() const;
}
потому что это разрушает инкапсуляцию и позволяет изменять элементы частного объекта.
Даже не делай этого:
class Family
{
public:
const Mother& GetMother() const;
}
потому что это может привести к «висящим маркерам», что означает, что вы сохраняете ссылку на член объекта, который уже уничтожен.
Теперь мой вопрос: есть ли хорошие альтернативы? Представь, что мама тяжелая! Если я теперь верну копию Mother вместо ссылки, GetMother становится довольно дорогой операцией.
Как вы справляетесь с такими случаями?
Во-первых, позвольте мне повторить: самая большая проблема заключается не в жизни, а в инкапсуляции.
Инкапсуляция означает не только то, что никто не может изменить внутреннее содержимое без вашего ведома, инкапсуляция означает, что никто не знает, как все реализовано в вашем классе, так что вы можете изменять внутреннее содержимое класса по своему желанию, пока вы сохраняете интерфейс идентичным.
Теперь, является ли ссылка, которую вы возвращаете 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;
};
Видите, как мне удалось полностью переделать внутренности, не меняя интерфейс ни на йоту? Очень просто.
Примечание: очевидно, что это преобразование только для эффекта …
Очевидно, что эта инкапсуляция происходит за счет некоторой производительности, здесь есть напряжение, вы сами решаете, следует ли предпочитать инкапсуляцию или производительность каждый раз, когда вы пишете геттер.
Возможные решения зависят от фактического дизайна ваших классов и от того, что вы считаете «внутренними объектами».
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;
...
};
Если вы ищете только для чтения, и по какой-то причине вам нужно избегать висячих ручек, то вы можете рассмотреть возможность возврата shared_ptr<const Mother>
,
Таким образом, Mother
объект может пережить Family
объект. Который также должен хранить его shared_ptr
, конечно.
Отчасти вопрос заключается в том, собираетесь ли вы создавать ссылочные циклы, используя слишком много shared_ptr
s. Если да, то вы можете рассмотреть weak_ptr
и вы можете также рассмотреть возможность принятия возможности висячих дескрипторов, но написания клиентского кода, чтобы избежать этого. Например, никто не слишком беспокоится о том, что std::vector::at
возвращает ссылку, которая становится устаревшей при уничтожении вектора. Но тогда контейнеры являются крайним примером класса, который преднамеренно выставляет объекты, которыми он «владеет».
Это восходит к фундаментальному принципу ОО:
Tell objects what to do rather than doing it for them.
Тебе нужно Mother
сделать что-нибудь полезное? Спроси Family
возражать сделать это для вас. Дайте ему любые внешние зависимости, завернутые в приятный интерфейс (Class
в с ++) через параметры метода на Family
объект.
потому что это может привести к «висящим ручкам», что означает, что вы держите
ссылка на член объекта, который уже уничтожен.
Ваш пользователь также может отменить ссылку null
или что-то столь же глупое, но они не собираются, и при этом они не собираются делать это, пока жизнь ясна и определена. В этом нет ничего плохого.
Это просто вопрос семантики. В твоем случае, Mother
является не Family
внутренности, а не детали ее реализации. Mother
На экземпляр класса можно ссылаться в Family
Как и во многих других организациях. Более того, Mother
время жизни экземпляра может даже не соотноситься с Family
продолжительность жизни.
Так что лучший дизайн будет хранить в Family
shared_ptr<Mother>
и выставить его в Family
интерфейс без забот.