トップページ > Perlについて
●Perlについて●
2023-02-06 20:48:50良いコードについて:2023-02-3

良いコードについて:2023-02-3.txt
さて、プログラミングでの良いコードについての記事を見つけました。
それはPythonで書かれたコードなので、Perlに書き直して、記事と合わせて載せたいと思います。
良いコードについて:2023-02-2の続きになります。
関連した処理をクラス側で定義する
もう少し突っ込んで考えてみると、税込みの計算$shina{"food_$t"} * ( 1 + $shina{"tax_food_1"});は、純粋にItemクラスのデータしか利用していません。
ということは、Itemクラスが担当しても良さそうですね。あるデータに関連した処理は、そのデータの近くに配置する方が管理しやすいでしょう。
これを踏まえて、再度実装すると、こうなります。
# total_price3.pm
package total_price3;
use strict;
sub new {
my $class = shift;
my $self = {
my $price => my $total_price
};
return bless $self,$class;
}
sub item {
my @item_list = @_;
my %items;
my $i = 0;
my $monoflg = 0;
foreach(@item_list){
if($_ =~ /food/){ # food
$monoflg = 1;
}elsif($monoflg == 1){
$items{"food_$i"} = $_; # 100yen
$items{"tax_food_1"} = 0.08; # 税率8%
$monoflg = 0;
$i++;
}elsif($_ =~ /item/){ #item
$monoflg = 2;
}elsif($monoflg == 2){
$items{"item_$i"} = $_; # 200yen
$items{"tax_item_1"} = 0.1; # 税率10%
$monoflg = 0;
$i++;
}
}
return \%items;
sub price_with_tax {
my $s_ref = shift;
my %shina = %{$s_ref};
my $t = 0;
my $price = 0;
my $total_price = 0;
foreach(sort keys %shina){
if($_ =~ /food_$t/ ){
$price = $shina{"food_$t"} * ( 1 + $shina{"tax_food_1"}); # foodは8%消費税
}elsif($_ =~ /item_$t/ ){
$price = $shina{"item_$t"} * ( 1 + $shina{"tax_item_1"}); # itemは10%消費税
}
$total_price = $total_price + $price;
$price = 0;
if($shina{"food_$t"} ne ''){ $t++; }
}
return $total_price;
}
}
sub total_price {
my $hashref = shift;
my %shina = %{$hashref};
return &price_with_tax(\%shina);
}
my @syohin_list = ('アンパン','food',100,'アンパン','food',100,'ペン','item',200);
my $item_ref = &item(@syohin_list);
my %syohin_list2 = %{$item_ref};
my $price = &total_price(\%syohin_list2);
print "Total:$price yen\n";
1;
※カンマ(,)を全角で表示しています。
Itemクラスにprice_with_taxメソッドを定義し、total_priceサブルーチンでそれを呼び出しています。
こうするとtotal_priceサブルーチンはすべてのitemにprice_with_taxメソッドを呼び出して、合計するだけのサブルーチンになったので、内包表記を利用して書くようにしました。
クラスを整理する
total_priceサブルーチンとしては、これで十分に責務が整理されていそうです。
ただ、まだもう少し責務の分離を進めてみましょう。
責務的な観点でいうと、今度はItemクラスが少し気になります。
というのも、商品の品種によってtax_rateを決定していますが、商品の品種の種類が増えれば、このコンストラクタを修正しなくてはいけません。
Itemクラスを変更するということは、基本的にはItemクラスを利用するすべてのコードがその変更の影響を受けるということです。
例えば、商品の品種に新しくbookという種別ができたとして(税率は…、0.01くらいにしましょうか。
本は人類にとってとても重要な商品ですもんね)、Itemクラスのコンストラクタに分岐を1つ加えるだけです。
ですが、そのせいでバグを混入させてしまうかもしれません。
bookの種別が出てきていない既存コードに影響を与えてしまうかもしれません。
このような仕様追加に対して、既存コードがなるべく影響を受けないようにするにはどうすればいいでしょうか?
そんなうまい方法があるでしょうか?
実は、これは継承を利用するとうまく扱うことができます。
つまり、商品の品種ごとにItemクラスを継承した子クラスを定義していく方法です。
これを実装するとこうなります。
# total_price4.pm
package total_price4;
use strict;
sub new {
my $class = shift;
my $self = {
'total_price' => my $total_price
};
return bless $self, $class;
}
sub item {
my @item_list = @_;
my %items;
my $i = 0;
my $monoflg = 0;
foreach(@item_list){
if($_ =~ /food/){ # food
$monoflg = 1;
}elsif($monoflg == 1){
$items{"food_$i"} = $_; # 100yen
$monoflg = 0;
$i++;
}elsif($_ =~ /item/){ #item
$monoflg = 2;
}elsif($monoflg == 2){
$items{"item_$i"} = $_; # 200yen
$monoflg = 0;
$i++;
}
}
return \%items;
sub tax_rate_defult {
return 0.1;
}
sub price_with_tax {
my $s_ref = shift;
my %shina = %{$s_ref};
my $t = 0;
my $price = 0;
my $total_price = 0;
foreach(sort keys %shina){
if($_ =~ /food_$t/ ){
$price = $shina{"food_$t"} * ( 1 + &food()); # foodは8%消費税
}elsif($_ =~ /item_$t/ ){
$price = $shina{"item_$t"} * ( 1 + &tax_rate_defult()); # itemは10%消費税
}
$total_price = $total_price + $price;
$price = 0;
if($shina{"food_$t"} ne ''){ $t++; }
}
return $total_price;
}
}
sub food {
return 0.08;
}
sub book {
return 0.01;
}
sub total_price {
my $hashref = shift;
my %shina = %{$hashref};
return &price_with_tax(\%shina);
}
my @syohin_list = ('アンパン','food',100,'アンパン','food',100,'ペン','item',200);
my $item_ref = &item(@syohin_list);
my %syohin_list2 = %{$item_ref};
my $price = &total_price(\%syohin_list2);
print "Total:$price yen\n";
1;
※カンマ(,)を全角で表示しています。
また、tax_rateフィールドをやめて、その代わりtax_rateメソッドを追加しています。
ただし、税率はクラスで固定になっています。
これで、新しい種別が来たとしても、新しい子クラスを定義するだけです。
例えばbookに対しては、クラスを追加するだけでよく、既存のFoodクラスやItemクラスを変更していません。
ですので、既存のコードに影響を与えることなく新しい仕様を追加することができています。
ここまで、責務を分離させることを指針としてコードを変更してきました。一番最初のコードからすると随分と異なるコードになりましたね。
整理すると、サブルーチンから「ロジック」と「具体的な値でロジックに適用する」という処理を分離するために、引数を利用しました。
次に、データをまとめるという責務を分離させるために、抽象データ型としてのクラスを導入しました。
このように、責務をうまく分離・整理するためにプログラミング言語の機能を使っていくことが重要です。
ちなみに、責務がまったく分離されていない最初のコードも、今回限りの利用であれば、それで十分です。
ですが、今後使う必要があるのであれば、何らかの責務の分離が必要になります。ただ、どこまで分離・整理するのが良いのかは状況次第ではあります。
例えば、もう絶対に種別は増えないということがわかっていれば、継承を利用してFoodクラスやBookクラスを作ることは過剰です。
実際には、現在の求められる要件や想定などから、その都度、どの程度まで責務の分離・整理が必要かを判断していってください。
サンプルコードを使いながら、責務の分離を進めていきましたが、そもそもなぜ責務の分離が重要なのでしょうか?
これは先ほどの責務の分離を行う際にも色々と触れているのですが、変更を容易に行うために責務の分離が重要なのです。
まず、責務が分離・整理されていないコードは変更が難しいという問題があります。
責務が整理されていないと、どこを変更すればよいのかを判断するのが難しくなります。
例えば、先ほどの例のように、total_priceサブルーチンが引数に対応していない場合、似たようなサブルーチンを大量に書く必要があるかもしれません。
すると、至る所に「税込みの合計」の責務を持つコードが散らばることになり、例えば税率に変更が入った場合など、どこを変更すれば正しく変更できたことになるのか、判断するのがとても難しくなります。
このように、責務が散らばっていないということは重要で、そのためにクラスやサブルーチンなどを利用して責務を整理する必要性が生じます。
さらに、total_priceサブルーチンの例で言うと、最初、total_priceサブルーチン自身が、商品の種別ごとに税率を管理し、税込み金額の合計を算出していました。
その後、最終的には、total_priceサブルーチンの責務は「税込み金額の合計を算出する」にまでまとめられました。
つまり、途中のtotal_priceサブルーチンには責務が複数あったというわけです。
1つのコンポーネント(関数やクラスなど)に複数の責務があるのは基本的には好ましい状況ではありません。
責務に変更が出てしまうと、それごとに修正する必要が出てきます。
つまり、責務が多ければ多いほど、そのコンポーネントは変更される可能性が高くなってしまいます。
変更というのは常に正しく実装できるものではありません。
当然、バグが混入する危険があります。
また、複数の責務が混在していると、無関係な責務のコードをうっかり変更してしまうかもしれません。
少なくともそのような可能性は排除できません。
これに対して、1つのコンポーネントに1つの責務しかないのであれば、関係のない責務によってコードを変更する必要はなくなり、変更による影響の範囲を限定できます。
また、責務が分離されているとテストがしやすいという点も重要です。
テストはコードが正しく動作することを保証する行為ですが、責務が分離されていないとテストが複雑になりやすいのです。
テストを行うとしてもどの責務のテストを行っているのかがわかりにくくなりますし、無関係な責務のためにテストデータなどを準備することが必要になったりします。
ですが、責務が分離され、その責務のロジックだけが抽出されていると、そのロジックだけを狙い撃ちにして確認することができます。
記事はここまで。
無理やり、Perlに書き換えた感が拭えませんが、いかがでしたでしょうか。
学ぶ一助になれば、幸いです。
出典「日経クロステック 変更が難しいコードはNG、プログラミングでは「責務の分離」を意識すべし 3page」
出典「日経クロステック 変更が難しいコードはNG、プログラミングでは「責務の分離」を意識すべし 4page」
いいね:35 |