Perl OOP属性操作最佳实践

时间:2013-09-12 14:11:00

标签: perl oop

假设以下代码:

package Thing;
sub new {
    my $this=shift;
    bless {@_},$this;
}
sub name {
    my $this=shift;
    if (@_) {
        $this->{_name}=shift;
    }
    return $this->{_name};
}

现在假设我们已经实例化了一个对象:

my $o=Thing->new();
$o->name('Harold');

足够好。我们还可以使用以下任一方法更快地实例化相同的事情:

my $o=Thing->new(_name=>'Harold');  # poor form
my $o=Thing->new()->name('Harold');

可以肯定的是,我允许在构造函数中传递属性,以允许“友好”类更完整地创建对象。它还可以允许克隆类型运算符使用以下代码:

my $o=Thing->new(%$otherthing);  # will clone attrs if not deeper than 1 level

这一切都很好。我理解需要隐藏方法背后的属性以允许验证等。

$o->name;  # returns 'Harold'
$o->name('Fred'); # sets name to 'Fred' and returns 'Fred'

但是这不允许基于自身轻松操纵属性,例如:

$o->{_name}=~s/old/ry/;  # name is now 'Harry', but this "exposes" the attribute

另一种方法是执行以下操作:

# Cumbersome, not syntactically sweet
my $n=$o->name;
$n=~s/old/ry/;
$o->name($n);

另一个潜力是以下方法:

sub Name :lvalue {  # note the capital 'N', not the same as name
    my $this=shift;
    return $this->{_name};
}

现在我可以执行以下操作:

$o->Name=~s/old/ry/;

所以我的问题是......这是上面的“犹太洁食”吗?或者以这种方式公开属性是不好的形式?我的意思是,这样做会消除“名称”方法中可能发现的任何验证。例如,如果'name'方法强制执行大写第一个字母和小写字母,那么'Name'(大写'N')会绕过该名称,并强制该类用户在使用它时自行警告。

那么,如果'名称'左值方法不完全是“犹太教”,那么有没有确定的方法来做这些事情?

我已经考虑了(但是考虑到头晕)像绑定标量这样的东西。可以肯定的是,这可能是要走的路。

此外,是否有重载可能会有所帮助?

或者我应该创建替代方法(如果它甚至可以工作):

sub replace_name {
    my $this=shift;
    my $repl=shift;
    my $new=shift;
    $this->{_name}=~s/$repl/$new/;
}
...
$o->replace_name(qr/old/,'ry');

提前致谢...请注意,我对Perl的OOP品牌不是很有经验,尽管我对OOP本身非常熟悉。

其他信息: 我想我的界面可以变得非常有创意......这是我修改过的一个想法,但我想这表明它确实没有界限:

sub name {
    my $this=shift;
    if (@_) {
        my $first=shift;
        if (ref($first) eq 'Regexp') {
            my $second=shift;
            $this->{_name}=~s/$first/$second/;
        }
        else {
            $this->{_name}=$first;
        }
    }
    return $this->{_name};
}

现在,我可以使用

设置name属性
$o->name('Fred');

或者我可以用

来操纵它
$o->name(qr/old/,'ry');  # name is now Harry

这仍然不允许像$ o-> name。='Jr。'这样的东西;但这并不难添加。哎呀,我可以让calllback函数传入,不是吗?

4 个答案:

答案 0 :(得分:4)

你的第一个代码示例绝对没问题。这是编写访问器的标准方法。当然,在进行替换时这会变得很难看,最好的解决方案可能是:

$o->name($o->name =~ s/old/ry/r);

/r标志返回替换结果。等价地:

$o->name(do { (my $t = $o->name) =~ s/old/ry/; $t });

嗯,是的,这第二个解决方案无疑是丑陋的。但我假设访问字段比设置字段更常见。

根据您的个人风格偏好,您可以使用两种不同的方法来获取和设置,例如: nameset_name。 (我认为get_前缀不是一个好主意 - 4个不必要的字符。)

如果替换部分名称是您班级的核心方面,那么将其封装在特殊的substitute_name方法中听起来是个好主意。否则,这只是不必要的镇流器,以及避免偶尔出现句法痛苦的不良折衷。

我建议你不要使用左值方法these are experimental

我宁愿不看(并调试)一些返回绑定标量的“聪明”代码。这可行,但对于我对这种解决方案感到满意感觉有点太脆弱了。

运算符重载对编写访问器没有帮助。特别是Perl中的赋值不能超载。


编写访问器很无聊,尤其是当他们不进行验证时。有些模块可以为我们处理自动生成,例如: Class::Accessor。这会将通用访问器getset添加到您的类中,并根据请求添加特定访问器。 E.g。

package Thing;
use Class::Accessor 'antlers';  # use the Moose-ish syntax
has name => (is => 'rw');  # declare a read-write attribute

# new is autogenerated. Achtung: this takes a hashref

然后:

Thing->new({ name => 'Harold'});
# or
Thing->new->name('Harold');
# or any of the other permutations.

如果你想要一个Perl的现代对象系统,那么就有一排兼容的实现。其中功能最丰富的是Moose,并允许您向属性添加验证,类型约束,默认值等。 E.g。

package Thing;
use Moose; # this is now a Moose class

has first_name => (
  is => 'rw',
  isa => 'Str',
  required => 1, # must be given in constructor
  trigger => \&_update_name, # run this sub after attribute is set
);
has last_name => (
  is => 'rw',
  isa => 'Str',
  required => 1, # must be given in constructor
  trigger => \&_update_name,
);
has name => (
  is => 'ro',  # readonly
  writer => '_set_name', # but private setter
);

sub _update_name {
  my $self = shift;
  $self->_set_name(join ' ', $self->first_name, $self->last_name);
}

# accessors are normal Moose methods, which we can modify
before first_name => sub {
  my $self = shift;
  if (@_ and $_[0] !~ /^\pU/) {
    Carp::croak "First name must begin with uppercase letter";
  }
};

答案 1 :(得分:3)

类接口的目的是防止用户直接操作您的数据。你想做的是很酷,但不是一个好主意。

事实上,我设计了我的类,所以即使是类本身也不知道它自己的结构:

package Thingy;

sub new {
    my $class = shift;
    my $name  = shift;

    my $self = {};
    bless, $self, $class;
    $self->name($name);
    return $self;
}

sub name {
    my $self = shift;
    my $name = shift;

    my $attribute = "GLUNKENSPEC";
    if ( defined $name ) {
        $self->{$attribute} = $name;
    }
    return $self->{$attribute};
}

您可以通过我的new构造函数看到我可以为我的 Thingy 传递一个名称。但是,我的构造函数不知道如何存储我的名字。相反,它只使用我的name方法来设置名称。正如您在name方法中看到的那样,它以不寻常的方式存储名称,但我的构造函数不需要知道或关心。

如果你想操纵这个名字,你必须对它起作用(如你所示):

my $name = $thingy->name;
$name =~ s/old/ry/;
$thingy->name( $name );

事实上,许多Perl开发人员仅使用inside out classes来阻止这种直接对象操作。

如果您希望能够通过传入正则表达式直接操作类,该怎么办?你必须写一个方法来做到这一点:

 sub mod_name {
    my $self        = shift;
    my $pattern     = shift;
    my $replacement = shift;

    if ( not defined $replacement ) {
        croak qq(Some basic error checking: Need pattern and replacement string);
    }

    my $name = $self->name;     # Using my name method for my class
    if ( not defined $name ) {
       croak qq(Cannot modify name: Name is not yet set.);
    }

    $name = s/$pattern/$replacement/;
    return $self->name($name);
}

现在,开发人员可以这样做:

my $thingy->new( "Harold" );
$thingy->mod_name( "old", "new" );
say $thingy->name;   # Says "Harry"

通过允许直接对象操作而节省的时间或精力,可以通过维护程序所需的额外工作量来抵消。大多数方法创建时间不会超过几分钟。如果我突然想要以一种全新且令人惊讶的方式操纵我的物体,那么创建一种新的方法就足够了。


1。不。我实际上并没有使用随意的废话来保护我的班级。这纯粹是出于演示目的,表明即使我的构造函数也不必知道方法实际存储数据的方式。

答案 2 :(得分:2)

  

我理解需要在方法后面隐藏属性以允许验证等。

验证不是唯一的原因,尽管它是您引用的唯一原因。我提到这个是因为另一个是像这样的封装使实现开放。例如,如果你有一个类需要有一个可以获取和设置的字符串“name”,你可以只公开一个成员名称。但是,如果您改为使用get()/ set()子例程,那么“name”如何在内部存储和表示无关紧要。

如果您使用该类编写一串代码然后突然意识到虽然用户可能正在将“name”作为字符串访问,但这可能会非常重要,但是以其他方式存储会更好(无论出于何种原因) 。如果用户直接访问字符串,作为成员字段,您现在要么必须通过包含将在真实的任何更改时更改名称的代码来补偿这一点...但是等等,您如何补偿客户端更改名称的代码......

你做不到。你被困住了。您现在必须返回并更改使用该类的所有代码 - 如果可以的话。我确信任何做过足够OOP的人都会以这种或那种形式遇到这种情况。

毫无疑问你以前读过这一切,但是我再次提起它,因为有几点(也许我误解了你),你似乎在概述改变“名字”的策略了解您的实施知识,而不是API的目的。这在perl中非常诱人,因为当然没有访问控制 - 一切都是必不可少的 - 但是由于上述原因,它仍然是一种非常糟糕的做法。

当然,这并不意味着您不能简单地承诺将“name”作为字符串公开。这是一个决定,在所有情况下都不一样。但是,在这种特殊情况下,如果您特别关注的是转换“名称”的简单方法,那么您可以选择使用get / set方法。这样:

# Cumbersome, not syntactically sweet

也许是真的(尽管其他人可能会说它简单明了),但你的关注不应该是语法上的甜蜜,也不应该是执行的速度。它们可能是关注点,但你的主要关注点必须是设计,因为无论你的东西多么甜美和快速,如果它设计得很糟糕,它们都会及时绕过你。

请记住,过早优化是所有邪恶的根源Knuth)。

答案 3 :(得分:2)

  

所以我的问题是......这是上面的“犹太洁食”吗?或者以这种方式公开属性是不好的形式?

归结为:如果内部变化会继续发挥作用吗?如果答案是肯定的,您可以做许多其他事情,包括但不限于验证。)

答案是肯定的。这可以通过让方法返回一个神奇的值来完成。

{
   package Lvalue;
   sub TIESCALAR { my $class = shift; bless({ @_ }, $class) }
   sub FETCH { my $self = shift; my $m = $self->{getter}; $self->{obj}->$m(@_) }
   sub STORE { my $self = shift; my $m = $self->{setter}; $self->{obj}->$m(@_) }
}

sub new { my $class = shift; bless({}, $class) }

sub get_name {
   my ($self) = @_;
   return $self->{_name};
}

sub set_name {
   my ($self, $val) = @_;
   die "Invalid name" if !length($val);
   $self->{_name} = $val;
}

sub name :lvalue {
   my ($self) = @_;
   tie my $rv, 'Lvalue', obj=>$self, getter=>'get_name', setter=>'set_name';
   return $rv;
}

my $o = __PACKAGE__->new();
$o->name = 'abc';
print $o->name, "\n";    # abc
$o->name = '';           # Invalid name