7

我见过一些 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);

我们 Perl 代码库中的一些通用 DAO 类使用类似的模式。自动生成的访问器调用内部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} // '';
}

我发现这种设计存在多个问题:

  • Setter 调用也通过 getter 代码路径,效率低下,尤其是在您有关系的情况下,因此 getter 必须将存储的 ID 解析为对象:

    $foo->bar($new_bar); # retrieves a Bar instance which is not used
    
  • getter 调用需要携带$new_value参数。当您非常频繁地调用 Perl 方法时(例如,100000 次,报告和其他 cronjobs 中的通常记录数),这已经可能导致可测量的开销。

  • Setter 不能采用未定义的值。(一个 Perl 特定的问题,我可以通过检查参数计数而不是参数定义来解决这个问题,但这会使自动生成的访问器更加复杂,并且可能会破坏一些手动访问器。)

  • 降低grepability:如果我有单独的 getter 和 setter(例如foo()set_foo()对于每个属性),我可以使用简单的grepfoo搜索 setter 调用。

  • 更?

我想知道组合的 setter-and-getter 方法是否有任何实际好处,或者它是否只是各个语言/图书馆社区之间的一种奇怪的传统。

4

3 回答 3

10

概要

  • 拥有单独的 getter/setter 或组合访问器是一种文化偏好。没有什么能阻止你选择少走的路。
  • 你的大部分缺点都不存在。不要将实现细节与风格决策的问题混为一谈。

拥有不同的set_get_方法看起来是自记录的,但是

  • 如果没有发生自动生成,写起来很痛苦,并且
  • 主要是静态访问控制的一种形式:我可以拥有 aget_而无需暴露 a set_

后一点在使用细粒度访问控制和权限系统的情况下尤其重要,例如在 Java、C#、C++ 等具有可见性细微差别的语言中,如私有/受保护/公共等。由于这些语言具有方法重载,因此编写统一的 getter/ setters 并非不可能,而是一种文化偏好

从我个人的角度来看,统一访问器有这些好处

  1. API 更小,可以使文档和维护更容易。
  2. 根据命名约定,每个方法调用的击键次数减少了 3 到 4 次。
  3. 对象感觉更像结构。
  4. 我想不出真正的缺点。

这是我个人foo.bar(foo.bar() + 42)的看法,这比 . 眼睛容易一点foo.setBar(foo.getBar() + 42)。但是,后一个示例清楚地说明了每种方法的作用。统一访问器重载具有不同语义的方法。我认为这感觉很自然,但它显然使理解代码片段变得复杂。

现在让我分析一下你所谓的缺点:

使用组合的 getter/setter 分析所谓的问题

Setter 调用通过 getter 代码路径

不一定是这种情况,而是您正在查看的实现的属性。在 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}
  }
}

代码路径是完全独立的,除了真正常见的东西。请注意,这也将接受undefsetter 模式下的值。

[...] 检索未使用的 Bar 实例

在 Perl 中,您可以自由检查调用方法的上下文。您可以有一个在 void 上下文中执行的代码路径,即当返回值被丢弃时:

if (not defined wantarray) { be_lazy() }
getter 调用需要$new_value围绕 […] 可测量的开销进行论证。

同样,这是一个特定于实现的问题。此外,您在这里忽略了真正的性能问题:您访问者所做的只是调度方法调用。而且方法调用很慢。当然方法解析是缓存的,但这仍然意味着一个哈希查找。这比参数列表上的额外变量要贵得多。

请注意,访问器也可以写成

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

这消除了一半的我不能通过undef问题。

Setter 不能采用未定义的值。

……这是错误的。我已经介绍过了。

可识别性

我将使用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 的定义。

如果我们愿意,我们可以使用更严格的定义:

如果在源文件中搜索方法/变量/...的声明语法,则可以找到声明的位置。这意味着 JS 的“<code>function foo”和 Perl 的“<code>sub foo”。

这只要求在自动生成时,将基本的声明语法放入注释中。例如

# AUTOGENERATE accessors for
# sub set_foo
# sub get_foo
# sub set_bar
# sub get_bar
my @accessors = qw/foo bar/;
...; # as above

grepability 不受使用组合或单独的 getter 和 setter 的影响,只涉及自动生成。

关于非白痴自动生成

我不知道您如何自动生成访问器,但产生的结果不一定很糟糕。要么你使用某种形式的预处理器,这在动态语言中感觉很傻,要么你使用eval,这在现代语言中感觉很危险。

在 Perl 中,我宁愿在编译时破解符号表。包命名空间只是具有类似名称的哈希%Foo::,具有所谓的glob作为条目,其中可以包含 coderefs 我们可以访问类似的 glob *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};
      }
    };
  }
}

这比委派给另一个方法更有效,并且可以处理undef.

如果维护程序员不知道这种模式,那么它的缺点是可维护性降低。

此外,源自该 sub 内部的错误报告为来自__ANON__

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

如果这是一个问题(即访问器包含复杂的代码),这可以通过使用来缓解Sub::Name,正如 Stefan Majewsky 在下面的评论中指出的那样。

于 2013-07-18T22:05:54.663 回答
1

有好处,但缺点超过了它们。

在典型情况下,可读性或易于理解是代码最重要的考虑因素。

我认为这种模式的开发人员正在努力做到简洁,这很重要。API 中的方法越多,就越难理解。

然而,一个设计良好的函数应该只做一件事。这胜过简洁带来的任何好处。

于 2013-07-18T17:19:41.150 回答
1

要考虑的另一个方面是该语言是否支持左值访问器。例如,在 Perl 中,您可以将变异运算符应用于此类访问器,从而达到类似

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

如果没有左值访问器+修改器方法,则需要的整洁度要低得多

$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 );
于 2013-07-21T22:15:40.630 回答