Refactoring ch1 A First Example -- PHPで書き直してみた (2/3)

PHPリファクタリング勉強メモ

出典: 


Splitting the Phases of Calculation and Formatting

  • 1/3 でコード構造化した結果からスタート
  • repos
<?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を作り、少しずつ処理を移していく

  • 新たに適用したリファクタリングパターン

    • Move Function

      • nested function(や、メソッド)を別の場所に移す
      • 例1/2
      • 例2/2
      • RenderPlainText関数内の$playFor関数をstatement関数内に移動
    • Replace Loop With Pipeline

      • ループの代わりに高階関数使う
      • 例A
      • 例B

Separated Into Two Files

  • 処理を2つに分けた

    • 中間データ生成
    • レンダリング
  • 別ファイルに再配置
  • repos
  • index.php
<?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)