オブジェクト編その3-アクセサ

※この記事を書いているのは未熟なプログラマです。なのでもし間違いがあったとしても「こんな内容の記事書くんじゃない。未熟者めが!」というご意見があったらありがたく頂戴します。よろしくお願いします。

アクセサとカプセル化

オブジェクト指向では「カプセル化」という概念が存在します。これはオブジェクト内部で行われている処理には直接触らせたくない場合に行います。

なのですが、Perlではその機能は実装されていないので、直接データへアクセスできてしまいます。
何故ならPerlにおけるオブジェクト指向プログラムは変数にメソッドが生えています。変数のデータへのアクセスは制限されません。そもそも変数にアクセスするのは自然な事なので。

Perlでは、カプセル化したいメソッド、データ名には_(アンダースコア)をつける習慣が存在します。これは紳士協定のようなものなのですが、クラスの設計者が「nameというメソッドは使用してもいいけど、_nameというメソッドがあっても使用しないでね」という意思が表示されていると考えて下さい。

C:\work\lib\book.pmを次のように書き換えてみましょう。

package Book;
use strict;
use warnings;

sub new {
    my ( $class, $args ) = @_;
    
    my $name   = $args->{name};
    my $author = $args->{author};
    my $price  = $args->{price};
    
    my $self = bless {
    	_name   => $name,
    	_author => $author,
    	_price  => $price,
    }, $class;
    return $self;
}

sub name   { $_[0]->{_name}   }
sub author { $_[0]->{_author} }
sub price  { $_[0]->{_price}  }

sub speak {
    print 'hi!';
}

1;

次にC:\work\example\oop_003.plを作成します。

#!C:\Perl\bin\perl
use strict;
use warnings;
use lib 'lib';
use Book;

my $args = {
    name   => '初めてのPerl',
    author => [ 'ランダル・L. シュワルツ', 'トム クリスチャンセン', ],
    price  => 3780,
};

my $book = Book->new( $args );

print $book->{_name}, "\n";	# これは設計者が意図した操作ではない。
print $book->name, "\n\n";

実行結果を見てみましょう。


Perlにおけるオブジェクト指向ではほとんどがハッシュリファレンスをblessしており、その際keyは_から始めるHackerもいらっしゃいます。でもつけてなくてもスクリプトは動くし明日はやってきます。

クラスを再利用する前提として、設計者が意図していない操作を行ってしまっては、認識の共有ができません。

そして上記スクリプトのように内部データを取得するメソッドをゲッターと呼びます。
今度はセッターメソッドを記述します。

C:\work\lib\book.pm のnameメソッドを次のように修正して下さい。

sub name   {
    my $self = shift;
    $self->{_name} = shift if @_;
    $self->{_name};
}

次にC:\work\example\oop_003.plを修正します。

#!C:\Perl\bin\perl
use strict;
use warnings;
use lib 'lib';
use Book;

my $args = {
    name   => '初めてのPerl',
    author => [ 'ランダル・L. シュワルツ', 'トム クリスチャンセン', ],
    price  => 3780,
};

my $book = Book->new( $args );

# print $book->{_name}, "\n";	# これは設計者が意図した操作ではない。
print $book->name, "\n\n";

# $book->{_name} = '続はじめてのPerl'; # これは設計者が意図した操作ではない。
$book->{_name} = '続はじめてのPerl';
print $book->name, "\n\n";

実行結果を確認します。



以上がアクセサの一例です。
ここまで書いただけではまだカプセル化の意義が全く見ませんので今度はauthorのアクセサを定義してみましょう。

次にC:\work\example\oop_003.plを次のように変更します。

#!C:\Perl\bin\perl
use strict;
use warnings;
use lib 'lib';
use Book;

my $args = {
    name   => '初めてのPerl',
    author => 'ランダル・L. シュワルツ',
    price  => 3780,
};

my $args2 = {
    name   => '続初めてのPerl',
    author => [ 'ランダル・L. シュワルツ', 'トム クリスチャンセン', ],
    price  => 3360,
};

my $book  = Book->new( $args );
my $book2 = Book->new( $args2 );

print $book->author, "\n\n";
print $book2->author, "\n\n";

実行結果をみてみます。


authorがスカラーと配列リファレンスによって出力結果が異なっています。解決策として次の2つの方法が考えられます。

  • newメソッドによるコンストラクタ(契約)の際にauthorが一人の場合でも配列リファレンスに格納する。
  • 内部関数(Private method)_to_arrayrefを準備して、スカラーの場合でも配列リファレンスとして処理する。

ここでは内部関数_to_arrayrefを準備してみます。

C:\work\lib\book.pm を次のように修正して下さい。

package Book;
use strict;
use warnings;
use Data::Dumper;

sub new {
    my ( $class, $args ) = @_;
    
    my $name   = $args->{name};
    my $author = $args->{author};
    my $price  = $args->{price};
    
    my $self = bless {
    	_name   => $name,
    	_author => _to_arrayref( $author ),
    	_price  => $price,
    }, $class;
    return $self;
}

sub name   {
    my $self = shift;
    $self->{_name} = shift if @_;
    $self->{_name};
}

sub author { 
	my $self = shift;
	my $author = shift;
	$self->{_author} = _to_arrayref( $author ) if $author;
	return @{ $self->{_author} };
}

sub price  { $_[0]->{_price}  }

sub speak {
    print 'hi!';
}

sub _to_arrayref {
    my ($value) = @_;

    return [] if !defined $value;
    return $value if ref $value eq 'ARRAY';
    return [$value];
}

1;

C:\work\example\oop_003.plを実行します。


そういえばauthorを複数形にしていませんでした。いまさらですが..
author を authors へ修正します。

C:\work\lib\book.pm を次のように修正して下さい。

package Book;
use strict;
use warnings;
use Data::Dumper;

sub new {
    my ( $class, $args ) = @_;
    
    my $name        = $args->{name};
    my $authors_ref = $args->{authors};
    my $price       = $args->{price};
    
    my $self = bless {
    	_name    => $name,
    	_authors => _to_arrayref( $authors_ref ),
    	_price   => $price,
    }, $class;
    return $self;
}

sub name   {
    my $self = shift;
    $self->{_name} = shift if @_;
    $self->{_name};
}

sub authors { 
	my $self = shift;
	my $authors_ref = shift;
	$self->{_authors} = _to_arrayref( $authors_ref ) if $authors_ref;
	return @{ $self->{_authors} };
}

sub price  { $_[0]->{_price}  }

sub speak {
    print 'hi!';
}

sub _to_arrayref {
    my ($value) = @_;

    return [] if !defined $value;
    return $value if ref $value eq 'ARRAY';
    return [$value];
}

1;

次にC:\work\example\oop_003.plを次のように変更します。

#!C:\Perl\bin\perl
use strict;
use warnings;
use lib 'lib';
use Book;

my $args = {
    name   => '初めてのPerl',
    author => 'ランダル・L. シュワルツ',
    price  => 3780,
};


my $book  = Book->new( $args );

print $book->authors, "\n";

実行結果を確認して、エラーが発生していない確認してください。

最後にpriceメソッドですが、3桁毎にカンマで区切った形式で出力するformatted_priceメソッドを追加します。

C:\work\lib\book.pm を次のように修正して下さい。

package Book;
use strict;
use warnings;
use Data::Dumper;

sub new {
    my ( $class, $args ) = @_;
    
    my $name        = $args->{name};
    my $authors_ref = $args->{authors};
    my $price       = $args->{price};
    
    my $self = bless {
    	_name    => $name,
    	_authors => _to_arrayref( $authors_ref ),
    	_price   => $price,
    }, $class;
    return $self;
}

sub name   {
    my $self = shift;
    $self->{_name} = shift if @_;
    $self->{_name};
}

sub authors { 
	my $self = shift;
	my $authors_ref = shift;
	$self->{_authors} = _to_arrayref( $authors_ref ) if $authors_ref;
	return @{ $self->{_authors} };
}

sub price  { $_[0]->{_price}  }
sub formatted_price { _addcomma( $_[0]->price ) }

sub speak {
    print 'hi!';
}

sub _addcomma {
    my $number = shift;

    if($number =~ /^[-+]?\d\d\d\d+/g) {
	my ($i, $j);
	for($i = pos($number) - 3,
	    $j = $number =~ /^[-+]/;
	    $i > $j; $i -= 3) {
	    	substr($number, $i, 0) = ',';
		}
    }
    return $number;
}

sub _to_arrayref {
    my ($value) = @_;

    return [] if !defined $value;
    return $value if ref $value eq 'ARRAY';
    return [$value];
}

1;


C:\work\example\oop_003.pm を次のように修正して下さい。

#!C:\Perl\bin\perl
use strict;
use warnings;
use lib 'lib';
use Book;

my $args = {
    name   => '初めてのPerl',
    authors => [ 'ランダル・L. シュワルツ', 'トム クリスチャンセン' ],
    price  => 3780,
};


my $book  = Book->new( $args );

print $book->authors, "\n";
print $book->formatted_price, "\n";

formatted_priceメソッドは直接$self->{_price}にアクセスしないで$self->priceからデータを取得していることに注意して下さい。もちろん結果は同じですが、こちらのほうが良い習慣だと思います。

実行結果を確認します。


お疲れさまでした。
今の段階では、なんとなくアクセサの使い方を見てみる程度で大丈夫だと思います。

次回はClassの利用者が、設計者の意図と反する操作を行わないように例外処理を実装したいと思います。