组合getter和setter方法有什么好处?

时间:2013-07-15 09:48:57

标签: javascript jquery perl properties api-design

我见过一些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中的常规记录数),这可能已经导致可测量的开销。

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

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

  • 更多?

我想知道组合制定者和获取者方法是否有任何实际好处,或者它是否是各个语言/图书馆社区中的一种奇怪传统。

3 个答案:

答案 0 :(得分:9)

概要

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

使用不同的set_get_方法看起来是自我记录的,但

    如果没有发生自动生成,
  • 会很难写,
  • 主要是一种静态访问控制形式:我可以get_而不会暴露set_

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

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

  1. API较小,可以使文档和维护更容易。
  2. 根据命名约定,每次方法调用可减少3-4次击键。
  3. 对象感觉更像结构。
  4. 我可以想到没有真正的缺点。
  5. 我个人认为,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}
      }
    }
    

    代码路径是完全独立的,除了真正普通的东西。请注意,这也将在setter模式下接受undef值。

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

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

    if (not defined wantarray) { be_lazy() }
    
    getter调用需要在[...]可测量的开销周围携带$new_value参数。

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

    请注意,访问者也可以像

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

    摆脱了我无法通过undef 问题的一半。

    Setters无法获取未定义的值。

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

    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的定义。

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

      

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

    这只需要在自动生成时,将基本声明语法放入注释中。 E.g。

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

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

    关于非愚蠢的自我生成

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

    在Perl中,我宁愿在编译期间通过符号表破解我的方式。包命名空间只是名称为%Foo::的哈希,其中包含所谓的 globs 作为条目,可以包含coderefs。我们可以访问类似*Foo::foo的全局(请注意* sigil)。所以不要做

    package Foo;
    sub foo { ... }
    

    我也可以

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

    现在让我们考虑两个细节:

    1. 我可以关闭strict 'refs',然后可以动态组合子例程名称。
    2. 那个子......这是一个封闭!
    3. 因此,我可以遍历一个字段名称数组,并为它们分配相同的子程序,区别在于每个字段名称都关闭:

      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

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

      此外,来自该子内部的错误报告来自__ANON__

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

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

答案 1 :(得分:1)

有好处,但它们被缺点所抵消。

在典型情况下,可读性或易于理解是代码最重要的问题。

我认为这种模式的开发者试图简洁,这很重要。 API中的方法越多,理解起来就越困难。

然而,精心设计的功能应该只做一件事。这简化了任何好处。

答案 2 :(得分:1)

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

之类的功能
$obj->name =~ s/foo/bar/;
$obj->value += 10;
没有左值访问器+ 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 );