そんな今日この頃の技術ネタ

本家側に書くほどでもない小ネタ用

Perlのレガシーコードと戦う(1) Data::Validatorによる引数の整理

どこかで読んだ「プロダクトファーストおじさんが書き散らしたコードをメンテナンスするために若手が疲弊する」みたいな話に共感する今日この頃。

いっそ新しく作った方が手っ取り早いんじゃないかと思うこともしばしばあるけれど、 他にもタスクがある中で(中身はどうあれ)今動いているシステムを作りなおすような工数を上層部に納得させるのはなかなか難しい。

とはいえ、ただ愚痴っていても状況は改善しない。

生産性低いことをいつまでもやっていたいとは思わないし、後輩にもそんな不毛なことを体験させたくもない。

そんなわけで、レガシーコードを地道に改善していく取り組みその1。


「レガシーコードと戦う」ということ

レガシーコードを相手にすることのしんどさは色々あって、 「ドキュメントが無くて仕様の正解が分からない」とか「テストがないから既存の機能を気軽にいじれない」とか 挙げようと思えばキリがないところだけれど、 その一つに「コードに一貫性が無い」ことが挙げられる。

何かちょっとした変更を加えるにも処理を全部読んで確認しなきゃいけなかったりして、 新たなコードを書く以上に読むことにリソースをもっていかれてしまう。


引数の設計、拡張性≠ガバガバの設計

その中でも今回は特に 関数やメソッドの引数 に着目した話。

設計が固まらないままに勢いとノリで書き進めていると、 大雑把に引数を渡してよしなに処理するみたいな関数やメソッドを作ってしまいがちになる。

作った時に当人が分かって使う分には柔軟性があって使いやすくも感じるのだが、 時間がたち他人が触るようになるに従ってそういったコードは信頼性に欠ける可読性に乏しい存在となっていく。

自由に投げ込める柔軟性ある設計だったはずが、いつの間にか何を入力するのが正解なのか分かりづらい・単に使いづらいものとなり、 結果として処理を読んでみないと怖くて使えない、運用コストを無駄に消費するコードとなってしまう。

特にPerlの場合には後述するような事情により、こういった傾向が顕著に表れる。


色々な引数の渡し方

ことPerlでは関数やメソッドの引数の渡し方にバリエーションがあって、

  1. リストの形で変数を列挙して渡す
  2. ハッシュリファレンスの形で渡す
  3. ハッシュで渡す

といった方法がありうる。

1. リストで渡す

&hoge(1, 2, 3);
sub hoge {
  my ($a, $b, $c) = @_;
  print $a;
}

書いている分にはタイプ数が少なくて気持ち良いのだけど、 引数が増えてくると「あれ?3個目ってなんだっけ?」とか「どっちが前だっけ?」とかいった事態になりうる。

それで間違えてエラーが出てくれる分には良いのだけど、 間違った渡し方でもそれっぽく動いてしまうような場合なんかはバグを発見するのが面倒で厄介だったりする。

例示したように頭で引数を全部受けてくれるならまだマシで、 処理の端々でshiftを使って受け取るような書き方がされていると読むのが非常にストレスだったりする。

2. ハッシュリファレンスの形で渡す

&hoge({a => 1, b => 2, c => 3});
sub hoge {
  my $args = shift;
  print $args->{a};
}

外側でまとめたデータを引数とする場合なんかによく使うのがハッシュリファレンスで渡す形態。

呼び出す側では何を渡しているのかハッキリしてて流用したり変更したりしやすいが、 そのデータの中に余分なキーが入る場合を考慮していなくて意図せぬ動作をしちゃうみたいなバグを誘発したりもする。

3. ハッシュで渡す

&hoge(a => 1, b => 2, c => 3);
sub hoge {
  my %args = @_;
  print $args{a};
}

Perlにおいてハッシュは名前と値が交互に入ってくる配列であることを活かした取り方。

2よりはタイプ数少ないし、個人的には保守性の観点から推したいとこではあるんだけど… うちの会社のコードではあんまり見ないんだよな。


Data::Validator

そんな感じで良く言えば柔軟性ある・悪く言えば一貫性なく書けてしまうのがPerlなのだけど、 そこをなんとかしてくれるのがData::Validatorというモジュール。

cpan Data::Validator

指定の仕方によって複数の渡し方に対応させることができるし、 名前の通りバリデーションチェックもやってくれる。

定義を作成し、そこに関数やメソッドで渡されてきた引数を入れてvalidateメソッドを実行すると、 戻り値として整形された変数が得られる。

use 5.010;
use Data::Validator;

&hoge( a => 1, b => 2, c => 3 );
&hoge( {a => 1, b => 2, c => 3} );

sub hoge {
  state $rule = Data::Validator->new(
    a => {isa => "Int"}, # 指定された型以外であればエラーになる 
    b => {isa => "Int"}, 
    c => {isa => "Int", optional => 1} # 必須ではないパラメータも指定できる
  );
  my $args = $rule->validate(@_); # $args->{a}みたいな感じでアクセスできる
}

特にオプションなしの場合はハッシュまたはハッシュリファレンスでの引き渡しに対応できる。

また、意図せぬ型の引数が渡された場合にエラーを返してくれる。


リストとして渡したい場合

リストによる値を列挙する形での引き渡しは、Sequencedを指定することで対応できる。

# どちらの形でも使用できる
&hoge(a => 1, b => 2, c => 3);
&hoge(1, 2, 3);

sub hoge {
  state $rule = Data::Validator->new(
    a => {isa => "Int"},
    b => {isa => "Int"}, 
    c => {isa => "Int", optional => 1}
  )->with("Sequenced");
  my $args = $rule->validate(@_);
}

メソッドとしての使用

メソッドとして、第一引数が$selfとなるような場合にもMethod指定で対応可能。

sub hoge {
  state $rule = Data::Validator->new(
    a => {isa => "Int"},
    b => {isa => "Int"}, 
    c => {isa => "Int", optional => 1}
  )->with("Sequenced", "Method"); # 複数使用可能
  my ($self, $args) = $rule->validate(@_);
}

余計な引数がつく場合

引数以外にも余分なものがつく場合、AllowExtraが使用できる。

sub hoge {
  state $rule = Data::Validator->new(
    a => {isa => "Int"},
    b => {isa => "Int"}, 
    c => {isa => "Int", optional => 1}
  )->with("Method", "AllowExtra");
  my ($self, $args, %extra) = $rule->validate(@_); # %extra内にa,b,c以外の引数が入ってくる
}

先に述べた、処理結果の変数をそのまま渡すようなパターンで使いやすいかもしれない。



そんなわけで、このData::Validatorを使って既存のコードのメソッドや関数の引数の部分を置き換えていくことにより、 安全かつ確実性ある再利用しやすい形にもっていける。

書き換え作業は自体ちょっと面倒ではあるが、 地道に置き換えていけば後々の生産性の向上につながるんじゃないかと希望をもっているところ。

プログラマが知るべき97のこと

プログラマが知るべき97のこと

  • 作者: 和田卓人,Kevlin Henney,夏目大
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2010/12/18
  • メディア: 単行本(ソフトカバー)
  • 購入: 58人 クリック: 2,107回
  • この商品を含むブログ (344件) を見る

↑直接的な技術を学べる書籍じゃないんで敬遠してたんだけど、読んでみると実際に役に立つ含蓄ある内容が多くて良かった。

レガシーコード関連について語ってる人もいるし、コード規約のつくりかたみたいな話もあって結構学びがある。