众所周知Perl 5的面向对象并不是真正意义上的面向对象, 只是添加了一些函数使得它看起来像是面向对象而已。 通常bless出来的对象,它的属性可以被外部程序直接访问。 《Perl Hacks》的#43给出了一个方法,可以封装类属性, 使其不能直接被外部访问。另外 Class::Std 模块可以简单地实现这个方法。

通常的类都是这样声明的:

#
# My::Class::Original
#
# 最基本的声明类的途径。
# 在new函数中通过bless使$self成为类的引用。
#
# 这个途径生成的类的属性以散列值的形式存在于对象中,
# 因此对外界是完全可见的。
# 虽然本例中提供了getter/setter,
# 但使用者完全可以忽略getter/setter并直接访问属性值。

package My::Class::Original;

# 类的构造函数
sub new {

    my ( $class, $data ) = @_;

    #该类包含name和url两个属性
    my $self = {
        name => $data->{name},
        url  => $data->{url}
    };

    bless $self, $class;
    return $self;
}

# name属性的getter
sub get_name {
    my $self = shift;
    return $self->{name};
}

# url属性的getter
sub get_url {
    my $self = shift;
    return $self->{url};
}

# name属性的setter
sub set_name {
    my $self = shift;
    $self->{name} = shift;
}

# url属性的setter
sub set_url {
    my $self = shift;
    $self->{url} = shift;
}

1;

通过 $s = My::Class::Original->new 生成对象之后, 就可以用 $s->{name} 直接访问它的属性。

稍稍用一些小技巧,即可将类属性隐藏在类之中:

#
# My::Class::InsideOut
#
# 通过闭包封装类的属性。
#
# 类属性并不以散列值的形式保存在类中,
# 而是保存在局部变量%names和%urls中,
# 再将局部变量的作用域限制在类之内,
# 就可以达到禁止外部直接访问类属性的目的。
#
# 通过这种途径定义的类,将只能通过getter/setter对类属性进行访问。

# 创建一个新的作用域,限制%names和%urls的有效范围
{

    package My::Class::InsideOut;

    use Scalar::Util 'refaddr';

    # 类属性
    my %names;
    my %urls;

    sub new {
        my ( $class, $data ) = @_;
        my $self = {};

        bless $self, $class;

        # 为类属性赋初始值
        my $id = refaddr($self);
        $names{$id} = $data->{name};
        $urls{$id}  = $data->{url};

        return $self;
    }

    # 以下是类属性的getter/setter

    sub get_name {
        my $self = shift;
        return $names{ refaddr($self) };
    }

    sub get_url {
        my $self = shift;
        return $urls{ refaddr($self) };
    }

    sub set_name {
        my $self = shift;
        $names{ refaddr($self) } = shift;
    }

    sub set_url {
        my $self = shift;
        $urls{ refaddr($self) } = shift;
    }

}

1;

这样在试图直接访问类属性时,由于类中并不存在属性的散列值,因此会返回空字符串。

通过Class::Std模块, 只需书写很少的代码,即可实现上述手工封装的过程。

#
# My::Class::Std
#
# 使用Class::Std包来实现类属性的封装。
# 基本原理与My::Class::InsideOut相同,
# 只是由Class::Std包代替了手工编写代码的麻烦。
#

package My::Class::Std;

use Class::Std;

my %names : ATTR( :get<name> :set<name> :init_arg<name> );
my %urls : ATTR( :get<url> :set<url> :init_arg<url> );

1;