*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

вторник, 9 ноября 2010 г.

Как называть getter'ы и setter'ы

Для именования функций записи и чтения членов класса (getter/setter) в стандартном C++ есть три часто используемых приема.

1. Чисто плюсовый вариант, основанный на семантике ссылок.

class Foo {
  Value field_;
public:
  Value& field() { return field_; }
  const Value& field() const { return field_; }
};

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

Foo foo;
foo.field() = field_instance;
field_instance = foo.field();

Плюсы: краткость текста, близость к нотации "свойств", и возможность использования присвоения в каскаде (foo1.field() = foo2.field() = 2;).

Минусы: использование синтаксиса вызова функции слева от знака присваивания выглядит непривычно.

2. Способ в стиле Java.

class Foo {
  Value field_;
public:
  void setField(const Value& value) { field_ = value; }
  const Value& getField() const { return field_; }
};

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

Foo foo;
foo.setField(field_instance);
field_instance = foo.getField();

Плюсы: ясность и очевидность синтаксиса.

Минусы: многословность из приставок "get" и "set".

3. Cтиль Objective-C

class Foo {
  Value field_;
public:
  void setField(const Value& value) { field_ = value; }
  const Value& field() const { return field_; }
};

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

Foo foo;
foo.setField(field_instance);
field_instance = foo.field();

Плюсы: краткость текста при чтении (нет почти бессмысленной приставки "get") и очевидность при записи.

Минусы: я пока не нашел таковых.

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

Лично я почти всегда предпочитал способ #1, но после своего последнего поста я перешел на третий.

33 комментария:

  1. Использую #3 (он же используется в Qt)

    ОтветитьУдалить
  2. Вообще думаю, что надо стараться избегать пропертей.

    Если какое-то поле меняется и анализируется извне, то зачем оно вообще в этом классе?

    Или может быть имеет смысл анализ и изменение поля разместить в соответствующих методах этого класса и отказаться вообще от проперти?

    То есть предоставить к переменной только косвенный доступ, через соответствующую логику. ИМХО было бы логично.

    PS: Я сам не всегда этому следую... :)

    Есть еще один вариант:
    class Foo {
    Value field_;
    public:
    Value field() const { return field_; }
    void field(const Value &f) { field_ = f; }
    };

    То есть те же геттеры/сеттеры только без get/set :) В нашем проекте часто используется. :(

    ОтветитьУдалить
  3. "ясность и очевидность синтаксиса" - это самое важное в стилистике кода, ИМХО. Поэтому №2

    ОтветитьУдалить
  4. @Андрей Валяев: А передача по значению, а не по ссылке в getter - это специально? Может дорого обойтить.

    ОтветитьУдалить
  5. 2Александр: По ссылке чревато ошибками... Это ж getter... Он должен вернуть значение, а не ссылку на мембера. Которую, кроме того, необходимо константить, дабы избежать злоупотреблений.

    Кроме того существует RVO...

    Писать в данном контексте ссылку - это однозначная преждевременная оптимизация. Если профайлер покажет узкое место - можно и поставить, не велика проблема. Но по значению как-то вернее ИМХО.

    Из
    Value getter() const;
    const Value &getter() const;

    Я выбираю первое - оно короче и понятнее. :)

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

    ОтветитьУдалить
  6. Сам пользуюсь №2, но по прочтении, думаю, что №3 лучше.

    ОтветитьУдалить
  7. Вообще конечно не все так однозначно... В этом и прелесть программирования.

    Может случиться так, что по значению передавать нельзя.

    Но ИМХО лучше вообще избегать геттеров и сеттеров. Если класс не контролирует и не отвечает за содержимое переменной, то какой смысл ее защищать? public: Value field; И дело с концом.

    Или всетаки возложить на класс какую-то ответственность, которая автоматически избавит нас от геттеров/сеттеров.

    ОтветитьУдалить
  8. А если Value имеет методы, то их не получится вызвать, что возможно в способе №1

    ОтветитьУдалить
  9. Если поле доступно на запись извне, то нет смысла во всех этих геттерах/сеттерах.

    Перенести его в public и выкинуть все эти недофункции нафиг.

    Код будет понятнее, короче и проще любого из вышеприведённых вариантов.

    ОтветитьУдалить
  10. @Oehgt:

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

    ОтветитьУдалить
  11. О_о я прозрел :) Никогда не думал, что стиль #3 называется стилем Objectiv-C. Для себя его определял как QT-подобный стиль.

    Я придерживаюсь стилей #2 и #3 - смотря где как и зачем. Вообще, считаю, что проект должен обладать своим Code Convention, который составляется рабочей группой программистов. Всегда можно найти компромис в стилистике :)

    ОтветитьУдалить
  12. @Stas:

    В этот прекрасный день произойдёт "рефакторинг", поле перейдёт в закрытую часть класса, а все обращения к ней - заменены на любимый setter. При этом, все изменения по всем проектам можно сделать не задействуя головной мозг вообще.

    И не раньше.

    А то, блин, много любителей написать код посложнее, да потом время на отладку потратить (и часто не своё время!), а этот "один прекрасный день, когда я захочу ХХХ сделать" за время жизни проекта так и не наступил.

    ОтветитьУдалить
  13. @Qehgt:

    Вы это всерьёз? :) Нет, я конечно понимаю, что легче в один прекрасный день пройтись по нескольким тысячам строк кода, выискивая все места, где вы присваивали/считывали значение члена и расставить get/set (и хорошо, если это в рамках только одного проекта, ибо если это во внешней библиотеке, то можно сразу вешаться), чем сразу написать по-человечески, но всё же )

    По теме: предпочитаю стиль №3, ибо считаю его наиболее красивым и лаконичным. Но в некоторых случаях приходится использовать стиль №1, хотя стараюсь всячески этого избегать.

    ОтветитьУдалить
  14. @Oehgt:

    То есть вместо того, чтобы сначала написать один маленький setter(который я вообще макросом для начала запузыриваю), мне надо будет по всему проекту менять обращения к переменной на вызов set метода и это сэкономит мне время? Или вы предлагаете перегрузить оператор присваивания для объекта? А если POD тип? А если мне именно на этом уровне иерархии нужно добавить отладку?

    ОтветитьУдалить
  15. @alexanius:
    В чём проблема-то? Что такое насколько тысяч строк для обычного проекта? Такие вещи при рефакторинге происходят постоянно - интерфейс там изменился/добавился/разделился, сопутствующие классы поэтому требуется изменить и т.п. Изменение исходников при этом - тривиальнейшая операция, если у вас почему-то это сложности вызывает, значит на каком-то уровне вы что-то не так делаете.

    @Stas:
    Да, так как удаление или добавление setter'ов - быстрая дешёвая операция.

    ps: уважаемые комментаторы как-то забыли причину появления этого топика, а именно предыдущее сообщение в этом блоге, где автор показывает, к чему приводит увлечение getter'ами и setter'ами на пустом месте.

    ОтветитьУдалить
  16. @Qehgt:

    Затруднения проявляются в самой необходимости выполнения лишней работы тогда, когда этого можно было избежать в самом начале. И всё из-за странного желания пойти вразрез с одной из основных концепций ООП без каких-либо на то оснований. Код при написании get/set не усложняется вообще, и, как минимум, не усложняет отладку.

    А предыдущее сообщение показывает, к чему может привести невнимательность и использование не очевидного синтаксиса при написании кода. Геттеры и сеттеры как концепция там вообще ни при чём.

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

    О, какая верная мысль. Внимание, вопрос - чем полный доступ на чтение/запись к закрытому полю отличается от public доступа к этому полю? Тем, что мы замусорили описание класса дополнительными методами и прокачали скилл отладки? Как это соотносится со стремлением избегать лишней работы?

    >И всё из-за странного желания пойти вразрез с одной из основных концепций ООП без каких-либо на то оснований.
    О, да. Сначала закроем поле, чтобы не забывать про инкапсуляцию, а затем дадим полный доступ к этому полю, так как иначе работать невозможно. Главное, что всё концептуально :)

    ОтветитьУдалить
  18. @Qehgt:

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

    В большинстве случаев полный доступ к полю давать не нужно и даже вредно. Профит от концептуальности выше чем профит от времени, как-бы сэкономленном на написании 2 функций.

    ОтветитьУдалить
  19. >Отличается тем, что доступ не полный
    Я не про "что такое getters and setters вообще, и что ими можно делать", а про данный конкретный вариант использования, который здесь автор рассматривает.

    >Профит от концептуальности выше
    Ох, грустно-то как всё. Вы серъёзно считаете, что getter+setter (в том виде, как предлагает автор) это и есть инкапсуляция, а не прямое её нарушение?

    ОтветитьУдалить
  20. Хм, извиняюсь, я не совсем правильно понял Ваш первый комментарий. Да, в данном конкретном примере оно, конечно, ничем не отличается с точки зрения прав доступа, но на мой взгляд всё равно предпочтительней чем просто публичный доступ хотя бы по тем причинам, которые указал @Stas. Но тут уже, видимо, дело личных предпочтений.

    ОтветитьУдалить
  21. Собственно бывают транспортные структуры (без логики), и наверное никому не придет в голову в насаждать в таких структурах проперти методы. :)

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

    ИМХО - лучше написать меньше буков, меньше придется отлаживать. Отладка совершенно не аргумент в пользу геттеров/сеттеров.

    Еще есть принцип единой ответственности. :)
    Кто нибудь может словами объяснить какой смысл существования метода? :)
    void setField(const Value& value)
    {
    field_ = value;
    }

    ОтветитьУдалить
  22. >Еще есть принцип единой ответственности. :)
    >Кто нибудь может словами объяснить какой смысл >существования метода? :)
    >void setField(const Value& value)
    >{
    >field_ = value;
    >}

    В этом методе смысла не вижу. Зато, если добавить проверку, например, на диапазон разрешенных значений для field_ и возвращать какой-то результат, то смысл может появиться. :)

    ОтветитьУдалить
  23. Собственно в этом и заключается контраргумент против глупых пропертей.

    А если добавить проверку - то и название по идее надо менять, потому что он станет уже не просто setField а checkAnsSetField. :)

    Но лучше конечно что нибудь более глубокомысленное. А то получается дублирование между кодом и именем. :)

    ОтветитьУдалить
  24. Getter+setter, даже в том примере, который приводит автор не является нарушением инкапсуляции, потому как принцип инкапсуляции(базовый принцип ООП) заключается в сокрытии реализации объекта от пользователя и работа с ним через интерфейс(спецификацию).Защита данных означает не то, что их нельзя модифицировать, а то, что с ними можно работать лишь используя методы содержащего их объекта. Что дает возможность варьировать его поведение(не изменяя сам интерфейс). Запись же напрямую, это и есть прямое нарушение принципа инкапсуляции, т.к есть объект, кто-то там снаружи ковыряется в его данных, а он ни сном не духом.
    Лучше уж тогда вообще вынести эти данные.Класс ведь это данные + методы для работы с ними, не так ли?
    А уж как удобно будет потом этот код отлаживать, я вообще молчу -
    ни проверку, ни трассировку не добавить и вообще
    эти обращения отслеживать будет гораздо труднее.

    ОтветитьУдалить
  25. Примеры нарушением не являются, но сами по себе явно избыточны. Пока не известно какую задачу должен решать этот класс. :)

    Вместо Foo foo вообще можно написать Value foo и будет счастье. :)

    ОтветитьУдалить
  26. А по поводу возможностей трассировки я уже высказался.

    Трассировка не является смыслом существования программы и поэтому не является достаточным основанием для создания лишних программных сущностей.

    Value foo не нуждается во внутренней трассировке. :)

    ОтветитьУдалить
  27. Смотря в какой программе, я бы не говорил столь категорично. К тому же кроме трассировки есть журналирование, к примеру переходов между состояниями у системы. Вот вы говорите Value foo не нуждается в трассировке. Может быть, сегодня.
    А вот когда мы напишем программу, в коде которой из разных потоков, из сотни мест происходит к ней обращение, такая необходимость может появиться. Может понабиться делать какое-нибудь промежуточное преобразование, проверку.
    И что значит, менять теперь код по всей программе, модифицировать эти обращения?
    Потому, что вначале это типа было избыточно..
    Чем добавление примитивного трехстрочного метода
    замусоривает код? У меня кстати проперти определяются макросом в одну строку.
    Отлично читается и уж багов никаких точно не привносит.

    ОтветитьУдалить
  28. void SetField(const Value& value) {
    m_Field = value;
    }
    Value GetField() const {
    return m_Field;
    }
    Использую когда нужно и читать, и задавать данные. По причине понятности. В геттере передаю не ссылку, а копию.

    Value Field() const {
    return m_Field;
    }
    Использую, когда нужно только читать, и запись в это поле в обозримом будущем не предвидится.

    Вообще, геттеры и сеттеры рассматриваю как полноценные методы классов, а не как некие "свойства". Кроме того, считаю, что сеттеров в коде должно быть как можно меньше.

    Публичный доступ к мемберам классов открывать неправильно.

    ОтветитьУдалить
  29. 2Stas: Если у тебя по всей программе идет обращение к одному полю одного класса - у тебя проблема, слишком жесткая связанность.

    Есть мнение что даже в классе нужно использовать доступ к переменным через методы... Когда у нас нету ни одного прямого доступа к переменной - у нас конечно есть возможность оттрассировать ее через метод доступа.

    Но обычно в методах класса никто такой ерундой не заморачивается. Прямой доступ понятнее. Так что трассировка всеравно не полная.

    Собственно ты трассируешь не использование конкретной переменной - а использование класса. Тогда тебе нет смысла параноидально строить сеттеры возле каждой переменной, следуй за логикой приложения.

    ОтветитьУдалить
  30. Сильно удивился, не найдя способа аксессоров в «стиле Smalltalk». Хорошо, что уже в третьем комментарии Андрей Валяев про них сообщил. Странно также, что в Objective-C, один из корней которого уходит в Smalltalk, используется не этот стиль.

    Если кому интересно, в Smalltalk сигнатуры геттера и сеттера — «field» и «field:» соответственно (двоеточие обозначает место для передачи аргумента, т. е. у геттера нет аргументов, а у сеттера есть один).

    ОтветитьУдалить
  31. В нотации Java-аксессоров не только getField, то такжe isField и hasField, что тоже способствует читабельности синтаксиса.

    ОтветитьУдалить