いつメソッドを短くするの?
今でしょ
ExcessiveMethodLengthとは
メソッドの行数が多すぎる(縦に長い)という警告です。
最初にリファクタしてほしい警告であり、常に気をつけてほしい警告です。
phpmdの既定値は100行ですが、まだまだ長すぎです。
「ThoughtWorksアンソロジー」の第5章で、Jeff Bay氏が提唱した「オブジェクト指向エクササイズ」のルール1は、もっと厳しくて、(メソッドではなく)クラスが50行までです。
例えばね、ケント・ベックが長いメソッドを書いているの、想像できる?
たまには書くかもしれないよ
じゃあね、ケント・ベックが長いメソッドのまま、デザインパターンを使おうとしているの、想像できる?
モンスターメソッドだらけのまま、ちょちょいとデザインパターンを使ってしまうかもね
まじめに答えてよー
メッセージ例
The xx yy() has zz lines of code. Current threshold is set to aa. Avoid really long methods.
抑止方法
/**
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
説明
Violations of this rule usually indicate that the method is doing too much. Try to reduce the method size by creating helper methods and removing any copy/pasted code.
メソッド長すぎ!
メソッドの処理内容が多すぎるね。
ヘルパーメソッドに分割して、行数を小さくしよう。
(空行もカウントします)
モンスターメソッド
この世界には、メソッド人とエンジニア人が暮らしていました。どちらの民族も友好的でした。
メソッド人のなかには巨人化してモンスターメソッドに変化する者がいます。モンスターメソッドは、エンジニア人が対抗することができない恐ろしい怪物なのです。
メソッド人の大きさは「行」で数えます。何行以上をモンスターメソッドとするという明確な定義はありませんが、一般的には、200〜300行級を小型、500行級を中型、1000行級を大型と分類することが多いようです。
200〜300行級の小型はあちこちに大勢います。それどころか、エンジニア人の多くは、200〜300行級をモンスターメソッドと認識していないかもしれません。1000行級の大型も珍しくなく、うわさでは3000行級の超大型もいると聞きます。
メソッド人は数値の計算が得意なおだやかな民族なので、なぜ凶暴なモンスターメソッドに変化してしまうのか、謎につつまれていました。
エンジニア人の調査によると、モンスターメソッドは病気であり、メソッド人は誰でもモンスターメソッドになってしまうかもしれないことがわかりました。
メソッド人の主食はコード芋です。メソッド人の身体には排泄器官がないため、コード芋を食べると食べただけ、内蔵脂肪や皮下脂肪の一種であるコード脂肪がたまり、体が大きくなります。
そのため、メソッド人は健康オタクが多く、テストケースサプリを飲んだり、フィットネスジムでリファクタ運動することがブームとなっているほどです。
ところが、テストケースサプリを飲み忘れると、メソッド人は体を動かすのが面倒になります。リファクタ運動をさぼるようになり、少しづつ巨人化がはじまります。そして、満腹中枢が麻痺し、コード芋をむさぼるように食べ、巨人化へのループが止まらなくなるのです。
いくつかの治療報告では、巨人化の初期に、テストケースサプリを飲んだり、リファクタ運動することで、元のメソッド人に戻すことができたそうです。
別の治療報告では、巨人化がはじまったメソッド人は、性格が凶暴化し、テストケースサプリを飲むこともリファクタ運動をすることも拒否するため、治療は難しいともあります。
病気は進行し、やがて完全なモンスターメソッドとなり、エンジニア人に対して大暴れするのです。
この惨状を見かねたエンジニア人政府は、巨人化がはじまったメソッド人にコード芋を与えてはいけいないという法律(コード芋禁止法)をつくりました。
コード芋禁止法は、いつから巨人化がはじまったとするのか明確ではない、守らなかったときの罰則がないなど、その効果が疑問視されています。
おおかたの予想どおり、エンジニア人はコード芋禁止法を守ることはなく、今このときもモンスターメソッドは増え続けています。
メソッドの成長速度を遅くする
fooメソッドに機能追加するとき、
2行以上を追加する場合は、fooメソッドにその2行を実装するのではなく、
class MyFoo
{
public function foo()
{
...
$x += ($y + $z); // 新規コード
$a += ($b + $c); // 新規コード
...
}
Code language: PHP (php)
新しくbarメソッドを作り、barメソッドに実装します。fooメソッドには、barメソッドを呼ぶ1行だけを追加するようにします。
class MyFoo
{
public function foo()
{
...
list($x, $a) = $this->bar($a, $b, $c, $x, $y, $z);
...
}
public function bar($a, $b, $c, $x, $y, $z)
{
$x += ($y + $z); // 新規コード
$a += ($b + $c); // 新規コード
return [$x, $a];
}
Code language: PHP (php)
MyFooクラスは新しいbarメソッドの分だけ大きくなってしまいました。MyFooクラスを大きくしたくない場合は、新しくMyBarクラスを作って、barメソッドを移動します。
class MyFoo
{
public function foo()
{
...
list($x, $a) = $this->bar($a, $b, $c, $x, $y, $z);
...
}
private function bar($a, $b, $c, $x, $y, $z)
{
$myBar = new MyBar();
return $myBar->bar($a, $b, $c, $x, $y, $z);
}
Code language: PHP (php)
class MyBar
{
public function bar($a, $b, $c, $x, $y, $z)
{
$x += ($y + $z); // 新規コード
$a += ($b + $c); // 新規コード
return [$x, $a];
}
}
Code language: PHP (php)
メソッドに分ける範囲
我々は1行2行でもヘルパーメソッドに分ける、そのドメインの言葉でメソッド名をつけることで、一種のDSL(ドメイン固有言語)を作っているのだ
実践テスト駆動開発
100行に及ぶメソッドを再利用できる機会はない
ThoughtWorksアンソロジー 第5章 Jeff Bay氏
さまざまな文脈で利用できるのは、3行のメソッド
ThoughtWorksアンソロジー 第5章 Jeff Bay氏
ふだんから、どんどんメソッドに分けましょう。
●メソッド前半にガード節と早期リターンが何個もある場合、ガード節群とメインメソッドをそれぞれメソッドに分けます。
●コメントをつけたくなったら、メソッドに分けます。
つけたいコメントを参考にしてメソッド名にします。
●連続行を空白行で区切りたくなったらメソッドに分けます。
そのかたまりには意味があり、他のかたまりとは意味が違うかもしれません。
●メソッド内の配列リテラルは、それを返すメソッドに分けます。
例えば、getFooLiteralというメソッドにします。
●for文まるごとメソッドに分けます。
例えば、最後に Allをつけて、addFooAllというメソッドにします。
●for文内の処理をメソッドに分けます。
例えば、最後に SingleやOneをつけて、addFooOneというメソッドにします。
●if文まるごとメソッドに分けます。
例えば、最後に Ifxxxをつけて、addFooIfAgeIsLessThanEquals にします。
●if文内の処理をメソッドに分けます。
●switch文まるごとメソッドに分けます。
●case文の処理をメソッドに分けます。
●preg_matchやpreg_replaceをメソッドに分けて、ユニットテストを書きます。
正規表現の間違いが見つかるかもれしません。
これまで動いていたのは、たまたまってことね
どんどんメソッドに分けようー!
メソッドを短くしようー!
デモ行進みたい
メソッドを新しいメソッドに分ける
メソッドに分けるときは、IDEのリファクタ機能を使うと便利です。PhpStormでは、分けたい行や範囲を選択して、メニューのRefactor>Refactor this>7. Extract Method...です。
(1)〜(3)は、メインロジックに手を付けないので、テストケースがない場合でも、比較的安心してリファクタできます。
(1)まず、巨大な文字列は、それだけを返すメソッドに分けます。
public function foo()
{
$str = "長い文字列、複数行かもね:" . $x;
Code language: PHP (php)
PhpStormでリファクタする場合は、$str =
の右辺、セミコロン含まない、"長い文字列、複数行かもね:" . $x
の範囲を選択してから、リファクタします。
public function foo()
{
$str = $this->getStr($a);
}
/**
* @param $a
* @return string
*/
public function getStr($a): string
{
return "長い文字列、複数行かもね:" . $a;
}
Code language: PHP (php)
(2)配列リテラルを定義している場合も、それだけを返すメソッドに分けます。
public function foo()
{
$arr = [
['xxx' => 0, 'yyy' => 1],
['xxx' => 1, 'yyy' => 2],
];
Code language: PHP (php)
PhpStormでリファクタする場合は、$arr =
の右辺、セミコロン含まない
の範囲を選択してから、リファクタします。
public function foo()
{
$arr = $this->getArr();
}
/**
* @param $a
* @return array
*/
public function getArr(): array
{
return [
['xxx' => 0, 'yyy' => 1],
['xxx' => 1, 'yyy' => 2],
];
}
Code language: PHP (php)
(3)ガード節と早期リターンが何個もある場合です。
public function foo($x, $y)
{
if ($x === '') {
return;
}
if ($y === '') {
return;
}
// メインロジック
}
Code language: PHP (php)
(3−1)phpstormのリファクタ機能を使って、メインロジックをメソッドに分けます。
public function foo($x, $y)
{
if ($x === '') {
return;
}
if ($y === '') {
return;
}
$this->fooMainLogick($x, $y);
}
private function fooMainLogic($x, $y)
{
// メインロジック
}
Code language: PHP (php)
(3−2)手作業で、ガード節をメソッドに分けます。
public function foo($x, $y)
{
if (!$this->fooIsValidate($x, $y)) {
return;
}
$this->fooMainLogic($x, $y);
}
private function fooIsValidate($x, $y)
{
if ($x === '') {
return false;
}
if ($y === '') {
return false;
}
return true;
}
private function fooMainLogic($x, $y)
{
// メインロジック
}
Code language: PHP (php)
(3−3)fooメソッドごと、別クラスにしてもいいと思います。
public function foo($x, $y)
{
$fooLogic = new FooLogic();
$fooLogic->foo($x, $y);
}
Code language: PHP (php)
class FooLogic
{
public function foo($x, $y)
{
if (!$this->isValidate($x, $y)) {
return;
}
$this->mainLogic($x, $y);
}
private function isValidate($x, $y)
{
if ($x === '') {
return false;
}
if ($y === '') {
return false;
}
return true;
}
private function mainLogic($x, $y)
{
// メインロジック
}
}
Code language: PHP (php)
(4)for文まるごとメソッドに分けます。
public function foo($x, $y)
{
// 何か処理
foreach ($arr as $i => $rcd) {
$rcd['xxx'] = $rcd['yyy'];
$arr[$i] = $rcd;
}
// 何か処理
}
Code language: PHP (php)
foreach内で$arrを変更しているので、return で返しました。
public function foo($x, $y)
{
// 何か処理
$arr = $this->doDomethingAll($arr);
// 何か処理
}
Code language: PHP (php)
public function doDomethingAll($arr)
{
foreach ($arr as $i => $rcd) {
$rcd['xxx'] = $rcd['yyy'];
$arr[$i] = $rcd;
}
}
Code language: PHP (php)
長いメソッドをリファクタするときは、同じように、if文、switch文をメソッドに分けていきます。
for文やif文ではなくて、意味のある範囲を選んで、メソッドに分けたほうがいいと思うんだけど?
それができないから、モンスターメソッドになったんでしょ
短くしているだけの気がするけど、これってリファクタと言えるのかな?
分けたメソッドなら、ユニットテストを書こうという気になるでしょ。一部分だけでもユニットテストができれば、すごい進歩よ!
まじで面倒だよ、モンスターメソッドが放置されてしまうのもわかるよ。最初からユニットテストを書いたり、短いメソッドを書いたほうが楽だよ
1日2日でモンスターメソッドになったわけではないから、リファクタするのにも時間も手間もかかるのよ。ダイエットと同じね。
メソッドを新しいクラスにする
PhpStormのリファクタ機能で、barメソッドから新しいクラスFooBarを作りました。
class Foo
{
private $x;
private $arr;
public function bar($a)
{
$this->arr = [
['xxx' => 0, 'yyy' => 1],
['xxx' => 1, 'yyy' => 2],
];
$this->x = $this->getStr($a);
$this->arr = $this->arrUpdate($this->arr);
}
Code language: PHP (php)
元のクラスには、privateプロパティのsetterが自動生成されました。元のクラスの$thisを、新しいクラスのコンストラクタで渡しています。
class Foo
{
private $x;
private $arr;
/** @var FooBar */
private $fooBar;
public function __construct()
{
$this->fooBar = new FooBar($this);
}
public function bar($a)
{
$this->fooBar->bar($a);
}
/**
* @param mixed $arr
*/
public function setArr($arr): void
{
$this->arr = $arr;
}
/**
* @param mixed $x
*/
public function setX($x): void
{
$this->x = $x;
}
Code language: PHP (php)
class FooBar
{
private $foo;
public function __construct(Foo $foo)
{
$this->foo = $foo;
}
public function bar($a)
{
$this->foo->setArr([
['xxx' => 0, 'yyy' => 1],
['xxx' => 1, 'yyy' => 2],
]);
$this->foo->setX($this->foo->getStr($a));
$this->foo->setArr($this->foo->arrUpdate($this->foo->getArr()));
var_dump($this->foo->getArr());
}
}
Code language: PHP (php)
元のクラスはすっきりしました。
しかし、新しいクラスのbarメソッドは、元のbarメソッドより、読みにくくなったようです。$this->foo経由でset/getしているためですね。
PhpStormが自動生成しているので、リファクタ後も同じように動作する可能性が高く、この安心感は捨てがたいです。
これをベースに読みやすいようにリファクタします。
メソッドの最初で、$this->foo->getXX() をローカル変数やprivateプロパティに代入し、
メソッド内では、ローカル変数やprivateプロパティで処理し、
メソッド最後に、$this->foo->setXX()で、結果を設定します。
筆者が考えていたリファクタは、次のようなコードです。
元のBarクラスのbarメソッドを、新FooBarクラスへコピペ。
barメソッドで使っているFooクラスのプロパティを、新FooBarクラスのコンストラクタで参照渡し。
class Foo
{
private $x;
private $arr;
public function bar($a)
{
$fooBar = new FooBar($x, $arr);
$fooBar->$bar($a);
}
Code language: PHP (php)
class FooBar
{
private $x;
private $arr;
// barメソッドで使っているプロパティを参照渡し
public function __construct(&$x, &$arr)
{
$this->x = &$x;
$this->arr = &$arr;
}
// 元のFooのbarメソッドからコピペ
public function bar($a)
{
$this->arr = [
['xxx' => 0, 'yyy' => 1],
['xxx' => 1, 'yyy' => 2],
];
$this->x = $this->getStr($a);
$this->arr = $this->arrUpdate($this->arr);
}
Code language: PHP (php)
こちらのほうが元のbarメソッドのままで、リファクタしやすいとも思いましたが、こちらの方法は手動です。
barメソッドで使っているprivateフィールドの個数が多いと、コンストラクタを呼ぶ側、コンストラクタ側のコードを正しく書くのが面倒そうです。
ましてやユニットテストなしでは、手動で正しくリファクタできたかどうか不安です。
結局、
(1)元のクラスでリファクタを続ける
(2)PhpStormのリファクタ機能を使って新クラスに分ける
(3)手動で新クラスに分ける
のどれを使うかはケースバイケースですね。