Каковы преимущества комбинированных методов геттера и сеттера?
Я видел некоторые 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 ответа:
Синопсис
Наличие отдельных геттеров / сеттеров или комбинированных аксессоров является культурным предпочтением. Нет ничего, что помешало бы вам выбрать менее избранный путь. Большинство ваших недостатков не существует. Не путайте детали реализации с проблемами стилистического решения.
, имеющих различные
set_
иget_
методы выглядит самодокументируемыми, ноПоследний пункт особенно важен там, где используются мелкозернистые системы контроля доступа и разрешения, например, в таких языках, как Java, C#, C++ с нюансами видимости, как private/protected/public и т. д. Поскольку эти языки имеют перегрузку методов, написание унифицированных геттеров / сеттеров не является невозможным, вместо этого этокультурное предпочтение .
- больно писать, если не происходит автогенерации, и
- являются в первую очередь формой статического контроля доступа: я могу иметь
get_
, не подвергаяset_
.От моего личная перспектива, унифицированные аксессоры имеют эти преимущества
- API меньше, что может облегчить документирование и обслуживание.
- В зависимости от соглашения об именовании на вызов метода приходится на 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 { ... } }
Теперь рассмотрим две детали:
Таким образом, я могу выполнить цикл над массивом имен полей и назначить им одну и ту же подпрограмму, с той разницей, что каждая из них закрывается над другим именем Поля:
- я могу выключить
strict 'refs'
и тогда динамически составить имя подпрограммы.- эта подлодка ... это конец!
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 );