Refactoring ch1 A First Example -- PHPで書き直してみた (2/3)
Splitting the Phases of Calculation and Formatting
<?php function statement($invoice, $plays) { $playFor = function ($perf) use ($plays) { return $plays[$perf['playID']]; }; $amountFor = function ($aPerformance) use ($playFor) { $result = 0; switch ($playFor($aPerformance)['type']) { case 'tragedy': $result = 40000; if ($aPerformance['audience'] > 30) { $result += 1000 * ($aPerformance['audience'] - 30); } break; case 'comedy': $result = 30000; if ($aPerformance['audience'] > 20) { $result += 10000 + 500 * ($aPerformance['audience'] - 20); } $result += 300 * $aPerformance['audience']; break; default: throw new Error('unknown type: ' . $playFor($aPerformance)['type']); } return $result; }; $volumeCreditsFor = function ($aPerformance) use ($playFor) { $result = 0; $result += max($aPerformance['audience'] - 30, 0); if ('comedy' === $playFor($aPerformance)['type']) $result += floor($aPerformance['audience'] / 5); return $result; }; $usd = function ($aNumber) { $format = '$%.2f'; return sprintf($format, $aNumber / 100); }; $totalVolumeCredits = function () use ( $invoice, $volumeCreditsFor ) { $volumeCredits = 0; foreach ($invoice['performances'] as $perf) { $volumeCredits += $volumeCreditsFor($perf); } return $volumeCredits; }; $totalAmount = function () use ($invoice, $amountFor) { $result = 0; foreach ($invoice['performances'] as $perf) { $result += $amountFor($perf); } return $result; }; // ---------------------------------------- $result = "Statement for ${invoice['customer']}"; foreach ($invoice['performances'] as $perf) { // print line for this order $result .= ' ' . $playFor($perf)['name'] . ': ' . $usd($amountFor($perf)) . "(${perf['audience']} seats)" . PHP_EOL; } $result .= 'Amount owed is ' . $usd($totalAmount()) . PHP_EOL; $result .= 'You earned ' . $totalVolumeCredits() . ' credits' . PHP_EOL; return $result; }
- HTML版レンダリング関数も作りたい
- 現状の
statement
関数は2種類の処理が混ざっている- 料金計算処理
- PlainText文字列の構築処理
- 現状の
statement
関数をコピペしてHTML版を作ると、料金計算処理が重複してしまう - そこで、処理を分割する
- まず料金計算処理のstubを作り、少しずつ処理を移していく
- 新たに適用したリファクタリングパターン
Separated Into Two Files
<?php use App\CreateStatementData; function statement($invoice, $plays) { $createStatementData = new CreateStatementData(); return renderPlainText($createStatementData($invoice, $plays)); } function renderPlainText($data) { $result = "Statement for ${data['customer']}"; foreach ($data['performances'] as $aPerformance) { // print line for this order $result .= ' ' . $aPerformance['play']['name'] . ': ' . usd($aPerformance['amount']) . "(${aPerformance['audience']} seats)" . PHP_EOL; } $result .= 'Amount owed is ' . usd($data['totalAmount']) . PHP_EOL; $result .= 'You earned ' . $data['totalVolumeCredits'] . ' credits' . PHP_EOL; return $result; } // stub function renderHtml($data) { return ''; } function usd($aNumber) { $format = '$%.2f'; return sprintf($format, $aNumber / 100); }
- CreateStatementData.php
<?php namespace App; class CreateStatementData { public function __invoke($invoice, $plays) { $amountFor = function ($aPerformance) { $result = 0; switch ($aPerformance['play']['type']) { case 'tragedy': $result = 40000; if ($aPerformance['audience'] > 30) { $result += 1000 * ($aPerformance['audience'] - 30); } break; case 'comedy': $result = 30000; if ($aPerformance['audience'] > 20) { $result += 10000 + 500 * ($aPerformance['audience'] - 20); } $result += 300 * $aPerformance['audience']; break; default: throw new Error('unknown type: ' . $aPerformance['play']['type']); } return $result; }; $playFor = function ($perf) use ($plays) { return $plays[$perf['playID']]; }; $volumeCreditsFor = function ($aPerformance) { $result = 0; $result += max($aPerformance['audience'] - 30, 0); if ('comedy' === $aPerformance['play']['type']) $result += floor($aPerformance['audience'] / 5); return $result; }; $totalVolumeCredits = function ($data) { return array_reduce( $data['performances'], function ($accumulator, $aPerformance) { return $accumulator + $aPerformance['volumeCredits']; }, 0 ); }; $totalAmount = function ($data) { return array_reduce( $data['performances'], function ($accumulator, $aPerformance) { return $accumulator + $aPerformance['amount']; }, 0 ); }; $enrichPerformance = function ($aPerformance) use ( $playFor, $amountFor, $volumeCreditsFor ) { // PHPの配列は値渡し $aPerformance['play'] = $playFor($aPerformance); $aPerformance['amount'] = $amountFor($aPerformance); $aPerformance['volumeCredits'] = $volumeCreditsFor($aPerformance); return $aPerformance; }; $statementData = []; $statementData['customer'] = $invoice['customer']; $statementData['performances'] = array_map( $enrichPerformance, $invoice['performances'] ); $statementData['totalVolumeCredits'] = $totalVolumeCredits($statementData); $statementData['totalAmount'] = $totalAmount($statementData); return $statementData; } }
- コード量は増えてしまった
- 他の点がすべて同じだとしたら、コード量が多いことは悪いこと
- だが、「他の点がすべて同じ」になることはまずない
Brevity is the soul of wit, but clarity is the soul of evolvable software.
英語
- in the digital age, frailty's name is software.
- デジタル時代において、弱者の名はソフトウェアである
- すぐ壊れる(不具合が入る)ということ
- "Frailty, thy name is woman" (Hamlet)のもじり
- デジタル時代において、弱者の名はソフトウェアである
- Brevity is the soul of wit.
- 言は簡潔を尊ぶ(Hamlet)