Функции, отличные от других, не связанные с частями
Herb Sutter сказал, что наиболее объектно-ориентированный способ написания методов в С++ использует не-членные функции, отличные от друга. Должно ли это означать, что я должен принимать частные методы и превращать их в не-членские функции, отличные от друга? Любые переменные-члены, которым могут понадобиться эти методы, могут быть переданы как параметры.
Пример (до):
class Number {
public:
Number( int nNumber ) : m_nNumber( nNumber ) {}
int CalculateDifference( int nNumber ) { return minus( nNumber ); }
private:
int minus( int nNumber ) { return m_nNumber - nNumber; }
int m_nNumber;
};
Пример (после):
int minus( int nLhsNumber, int nRhsNumber ) { return nLhsNumber - nRhsNumber; }
class Number {
public:
Number( int nNumber ) : m_nNumber( nNumber ) {}
int CalculateDifference( int nNumber ) { return minus( m_nNumber, nNumber ); }
private:
int m_nNumber;
};
Я на правильном пути? Следует ли перенести все частные методы в функции, отличные от других? Какими должны быть правила, которые говорили бы вам иначе?
Ответы
Ответ 1
Я верю в свободные функции и соглашаюсь с Саттером, но мое понимание находится в обратном направлении. Дело не в том, что ваши общедоступные методы зависят от бесплатных функций вместо частных методов, а скорее за то, что вы можете построить более богатый интерфейс вне класса со свободными функциями, используя предоставленный публичный интерфейс.
То есть, вы не нажимаете своих рядовых вне класса, а уменьшаете публичный интерфейс до минимума, что позволяет вам построить остальную функциональность с наименьшей возможной связью: только с использованием открытого интерфейса.
В вашем примере то, что я бы переместил за пределы класса, это метод CalculateDifference, если он может быть представлен эффективно с точки зрения других операций.
class Number { // small simple interface: accessor to constant data, constructor
public:
explicit Number( int nNumber ) : m_nNumber( nNumber ) {}
int value() const { return m_nNumber; }
private:
int m_nNumber;
};
Number operator+( Number const & lhs, Number const & rhs ) // Add addition to the interface
{
return Number( lhs.value() + rhs.value() );
}
Number operator-( Number const & lhs, Number const & rhs ) // Add subtraction to the interface
{
return Number( lhs.value() - rhs.value() );
}
Преимущество заключается в том, что если вы решите переопределить внутренние элементы вашего номера (не так много, что вы можете сделать с таким простым классом), если вы поддерживаете постоянный публичный интерфейс, тогда все остальные функции будут работать из коробка. Внутренние детали реализации не заставят вас переопределить все другие методы.
Жесткая часть (не в упрощенном примере выше) определяет, какой минимальный интерфейс вы должны предоставить. Статья (GotW # 84), ссылка на предыдущий вопрос здесь - отличный пример. Если вы прочтете его подробно, вы обнаружите, что можете значительно уменьшить количество методов в std:: basic_string при сохранении одинаковой функциональности и производительности. Счет будет сходить от 103 функций-членов только до 32 членов. Это означает, что изменения в реализации класса будут влиять только на 32 вместо 103 членов, и поскольку интерфейс будет сохранен, 71 бесплатные функции, которые могут реализовать остальную функциональность в терминах 32 членов, не должны быть изменены.
Это важный момент: он более инкапсулирован, поскольку вы ограничиваете влияние изменений реализации на код.
Перемещение исходного вопроса, вот простой пример того, как использование свободных функций улучшает локальность изменений в классе. Предположим, что сложный класс имеет действительно сложную операцию сложения. Вы можете пойти на это и реализовать все переопределения операторов как функции-члены, или вы можете так же легко и эффективно реализовать только некоторые из них внутренне и предоставить остальным в качестве бесплатных функций:
class ReallyComplex
{
public:
ReallyComplex& operator+=( ReallyComplex const & rhs );
};
ReallyComplex operator+( ReallyComplex const & lhs, ReallyComplex const & rhs )
{
ReallyComplex tmp( lhs );
tmp += rhs;
return tmp;
}
Нетрудно видеть, что независимо от того, как оригинальная operator+=
выполняет свою задачу, свободный operator+
выполняет свою работу правильно. Теперь, при любых изменениях в классе, operator+=
нужно будет обновить, но внешний operator+
останется незатронутым на всю оставшуюся жизнь.
Приведенный выше код является общим образцом, в то время как обычно вместо приема операнда lhs
по ссылке константы и создания временного объекта внутри он может быть изменен так, что сам параметр является копией значения, помогая компилятору с некоторыми оптимизации:
ReallyComplex operator+( ReallyComplex lhs, ReallyComplex const & rhs )
{
lhs += rhs;
return lhs;
}
Ответ 2
Не все частные методы должны быть перемещены в не-членную функцию, отличную от друга, но те, которые не нуждаются в доступе для ваших личных данных, должны быть. Вы должны предоставить доступ как можно меньше функций, чтобы инкапсулировать ваши классы в качестве возможного возможного.
Я настоятельно рекомендую прочитать Эффективный С++ от Scott Meyers, который объясняет, почему вы должны это делать и когда это уместно.
Изменить: я хотел бы добавить, что это менее верно для частного метода, а затем для публичных, хотя и действителен. Поскольку инкапсуляция пропорциональна количеству кода, который вы нарушаете, изменяя свой метод, имея частную функцию-член, даже если он не требует доступа к членам данных. Это связано с тем, что изменение этого кода приведет к разрыву небольшого кода и только кода, который вы контролируете.
Ответ 3
Этот вопрос, как представляется, уже был адресован при ответе на другой вопрос.
Такие правила, как правило, являются хорошими слугами и плохими хозяевами, есть сделки. Является ли результат более подходящим, если вы применяете предлагаемое преобразование? Зачем? Я считаю, что предполагаемое преимущество заключается в том, что, уменьшая количество методов, непосредственно связанных с частными данными объекта, вы можете более легко понять его поведение и, следовательно, упростить его поддержку.
Я не считаю, что ваш следующий пример достигает этой цели. [Вы можете заметить, что приведенный выше пример "после" не будет компилироваться в любом случае, но это другая история.] Если мы настроим его для реализации внешних функций только с точки зрения публичных методов, а не внутреннего состояния (добавьте аксессор для значения) то, я думаю, у нас был некоторый выигрыш. Достаточно ли этого, чтобы оправдать работу. Мое мнение: нет. Я думаю, что преимущества предлагаемого перехода к внешней функции становятся намного больше, когда методы обновляют значения данных в объекте. Я хотел бы реализовать небольшое количество мутаторов, поддерживающих инвариантность, и реализовать основные "бизнес-методы" с точки зрения тех, - экстернализация этих методов - это способ обеспечить, чтобы они могли работать только с точки зрения мутаторов.
Ответ 4
Это сильно зависит от вашего дизайна и ситуации, в которой вы находитесь. Когда я пишу свой код, я действительно ставил функции public
и protected
в классы. Остальное - это деталь реализации, которая является частью файла .cpp.
Я часто использую идиом pImpl. На мой взгляд, оба подхода имеют следующие преимущества:
-
Чистый дизайн интерфейса
Пользователи, работающие с вашим интерфейсом, лучше поймут это, не вдаваясь в подробности реализации, если им это не нужно.
-
Отключение от реализации
Если вы изменили что-то в файле .cpp без изменения интерфейса, вам нужно будет только перекомпилировать один .cpp файл (блок компиляции) и повторно связать приложение, что приведет к значительно более быстрому времени сборки.
-
Скрыть зависимости от других классов, которые используют ваш интерфейс
Если все помещено в заголовочный файл, вам иногда приходится включать другие заголовки, которые являются "частью вашего интерфейса", а другие будут включать их независимо от того, не хотят ли они этого. Включение реальных реализаций в единицы компиляции скроет эти зависимости.
Но иногда вы пишете библиотеки или реализации только для заголовков, поэтому вы не можете поместить вещи в единицы компиляции, так как этот подход потребует не только включения вашей библиотеки, но и ссылок на вашу библиотеку.