Каковы преимущества комбинированных методов геттера и сеттера?


Я видел некоторые API, особенно в скриптовых языках (мы используем Perl и JS в нашей команде), которые используют комбинированные методы getter и setter. Например, в jQuery:

//append something to element text
var element = $('div#foo');
element.text(element.text() + 'abc');

Например, в Perl CGI.pm модуль:

# read URL parameter from request
my $old_value = $cgi->param('foo');
# change value of parameter in request
$cgi->param('foo', $new_value);

Некоторый универсальный класс DAO в нашей кодовой базе Perl использует аналогичный шаблон. Автогенерированные методы доступа вызывают внутренний метод getset(), подобный этому:

sub foo { # read/write accessor for the foo() property (autogenerated)
    my ($self, $new_value) = @_;
    return $self->getset('foo', $new_value);
}

sub getset {
    my ($self, $field, $new_value) = @_;
    ## snip (omitted some magic here) ##

    # setter mode
    if (defined $new_value) {
        ## snip (omitted some magic here) ##
        $self->{data}{$field} = $new_value;
        ## snip (omitted more magic here) ##
    }

    # getter mode
    return $self->{data}{$field} // '';
}

Я вижу несколько проблем с этим дизайном:

  • Звонки сеттера проходят путь кода геттера также неэффективен, особенно если у вас есть отношения, поэтому геттер должен разрешить сохраненный идентификатор в объект:

    $foo->bar($new_bar); # retrieves a Bar instance which is not used
    
  • Вызов геттера должен нести Аргумент $new_value. Когда вы вызываете метод Perl очень часто (скажем, 100000 раз, обычное количество записей в отчетах и других cronjob), это уже может вызвать измеримые накладные расходы.

  • Сеттеры не могут принимать неопределенное значение. (Конкретная проблема Perl, с которой я мог бы работать вокруг, проверяя количество аргументов вместо определенности аргументов, но это сделало бы автогенерированные методы доступа еще более сложными и, вероятно, сломало бы некоторые ручные методы доступа.)

  • Уменьшенная grepability : Если бы у меня были отдельные геттеры и сеттеры (например, foo() и set_foo() для каждого свойства foo), я мог бы искать вызовы setter, используя простой grep.

  • Даже больше?

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

3 7

3 ответа:

Синопсис

    Наличие отдельных геттеров / сеттеров или комбинированных аксессоров является культурным предпочтением. Нет ничего, что помешало бы вам выбрать менее избранный путь. Большинство ваших недостатков не существует. Не путайте детали реализации с проблемами стилистического решения.

, имеющих различные set_ и get_ методы выглядит самодокументируемыми, но

  • больно писать, если не происходит автогенерации, и
  • являются в первую очередь формой статического контроля доступа: я могу иметь get_, не подвергая set_.
Последний пункт особенно важен там, где используются мелкозернистые системы контроля доступа и разрешения, например, в таких языках, как Java, C#, C++ с нюансами видимости, как private/protected/public и т. д. Поскольку эти языки имеют перегрузку методов, написание унифицированных геттеров / сеттеров не является невозможным, вместо этого этокультурное предпочтение .

От моего личная перспектива, унифицированные аксессоры имеют эти преимущества

  1. API меньше, что может облегчить документирование и обслуживание.
  2. В зависимости от соглашения об именовании на вызов метода приходится на 3-4 нажатия клавиш меньше.
  3. объекты кажутся немного более похожими на структуры.
  4. я не могу придумать никаких реальных недостатков.

Мое личное мнение, что foo.bar(foo.bar() + 42) немного легче на глаза, чем foo.setBar(foo.getBar() + 42). Однако последний пример делает это ясным. что делает каждый метод. Унифицированные методы доступа перегружают метод с различной семантикой. Я считаю это естественным, но это, очевидно, усложняет понимание фрагмента кода.

Теперь позвольте мне проанализировать ваши предполагаемые недостатки:

Анализ предполагаемых проблем с комбинированными геттерами / сеттерами

вызовы сеттера проходят через путь кода геттера

Это не обязательно так, а скорее свойство реализации , на которую вы смотрите. В Perl, вы может достаточно написать

sub accessor {
  my $self = shift;
  if (@_) {
    # setter mode
    return $self->{foo} = shift;
    # If you like chaining methods, you could also
    # return $self;
  } else {
    # getter mode
    return $self->{foo}
  }
}

Пути кода полностью разделены, за исключением материала, который являетсядействительно общим. Обратите внимание, что это также будет принимать значение undef в режиме setter.

[...] извлекает экземпляр бара, который не используется

В Perl вы можете проверить контекст, в котором был вызван ваш метод. У вас может быть путь кода, который выполняется в контексте void, т. е. когда возвращаемое значение отбрасывается:

if (not defined wantarray) { be_lazy() }
вызов геттера должен нести аргумент $new_value вокруг [ ... ] измеримых накладных расходов. Опять же, это специфическая проблема реализации. Кроме того, вы упустили из виду реальную проблему производительности здесь: все, что вы делаете, это отправляете вызов метода . И вызовы методов выполняются медленно. Конечно, разрешение метода кэшируется, но это все равно подразумевает один поиск хэша. Что намного дороже, чем дополнительная переменная в списке аргументов.

Обратите внимание, что аксессор также мог быть написан как

sub foo {
   my $self = shift;
   return $self->getset('foo', @_);
}

Который избавляется от половины я не могу пройти undef проблема.

сеттеры не могут принимать неопределенное значение.

... что неверно. Я уже об этом говорил.

Grepability

Я буду использовать это определение grepability :

Если в исходном файле искать имя метода / переменной/..., то можно найти сайт объявления.

Это запрещает дебильных автогенерации как

my @accessors = qw/foo bar/;
for my $field (@accessors) {
  make_getter("get_$field");
  make_setter("set_$field");
}

Здесь set_foo не приведет нас к точке объявления (приведенный выше фрагмент). Однако автогенерация, как

my @accessors = qw/foo bar/;

for my $field (@accessors) {
  make_accessor($field);
}

Действительно выполняет приведенное выше определение grepability.

Мы можем использовать более строгое определение, если захотим:

Если исходный файл ищется для синтаксиса объявления метода / переменной/..., то сайт объявления может быть найден. Это будет означать "function foo "для JS и" sub foo" для Perl.

Это просто требует, чтобы в момент автогенерации основной синтаксис объявления был помещен в комментарий. Например,

# AUTOGENERATE accessors for
# sub set_foo
# sub get_foo
# sub set_bar
# sub get_bar
my @accessors = qw/foo bar/;
...; # as above
На смазываемость не влияет использование комбинированных или отдельных геттеров и сеттеров, а только затрагивает автогенерацию.

О неморальной автогенерации

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

В Perl я бы предпочел взломать таблицу символов во время компиляции. Пространства имен пакетов-это просто хэши с именами типа %Foo::, которые имеют так называемые Глобы в качестве записей, которые могут содержать кодовые символы. Мы можем получить доступ к глобусу, такому как *Foo::foo (обратите внимание на сигил *). Поэтому вместо того, чтобы делать

package Foo;
sub foo { ... }

Я мог бы также

BEGIN {
  *{Foo::foo} = sub { ... }
}

Теперь рассмотрим две детали:

  1. я могу выключить strict 'refs' и тогда динамически составить имя подпрограммы.
  2. эта подлодка ... это конец!
Таким образом, я могу выполнить цикл над массивом имен полей и назначить им одну и ту же подпрограмму, с той разницей, что каждая из них закрывается над другим именем Поля:
BEGIN {
  my @accessors = qw/foo bar/;
  # sub foo
  # sub bar

  for my $field (@accessors) {
    no strict 'refs';

    *{ __PACKAGE__ . '::' . $field } = sub {
      # this here is essentially the old `getset`

      my $self = shift;

      ## snip (omitted some magic here) ##

      if (@_) {
        # setter mode
        my $new_value = shift;
        ## snip (omitted some magic here) ##
        $self->{data}{$field} = $new_value;
        ## snip (omitted more magic here) ##
        return $new_value; # or something.
      }
      else {
        # getter mode
        return $self->{data}{$field};
      }
    };
  }
}

Это greppable, более эффективно, чем просто делегирование другому методу и может обрабатывать undef.

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

Кроме того, ошибки, возникающие внутри этой субподрядной системы, сообщаются как исходящие из __ANON__:

Some error at script.pl line 12
    Foo::__ANON__(1, 2, 3) called at Foo.pm line 123

Если это проблема (то есть средство доступа содержит сложный код), это можно смягчить с помощью Sub::Name, как указывает Стефан Маевский в комментарии ниже.

Есть преимущества, но они перевешиваются недостатками.

В типичных случаях читабельность или легкость понимания является наиболее важной проблемой для кода.

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

Еще один аспект, который следует рассмотреть, заключается в том, поддерживает ли язык методы доступа lvalue. Например, в Perl вы можете применить операторы мутации к таким методам доступа, приходя к таким вещам, как

$obj->name =~ s/foo/bar/;
$obj->value += 10;

, который без метода lvalue accessor+mutator потребовал бы гораздо менее аккуратного

$obj->set_name( $obj->get_name =~ s/foo/bar/r );  # perl 5.14+
$obj->set_name( do { $_ = $obj->get_name; s/foo/bar/; $_ } );  # previously

$obj->set_value( $obj->get_value + 10 );