コードの読みやすさ(readability)はソフトウェアの保守性を高め、より価値のあるものにします。コードの読みやすさを向上させるために抽象度を整えることは、プログラマーの日常的な活動です。今回はコードの抽象度を整えるための指針について考えてみます。
全体と詳細
他のメソッドを呼び出すコードでメソッドを構成する場合、メソッドは、それぞれがほぼ同じ抽象度になるようにしよう。
— Kent Beck 実装パターン p.95 8章 メソッド 複合メソッド
上記はKent Beck氏によるメソッドの実装パターンの一つ複合メソッドの冒頭にある文です。以前の私がどのような指針を持ってメソッドを分割していたのか、今となっては正確に思い出すことはできませんが、おそらく1メソッド40行程度を目安に分割可能なコード片を抽出したものにそれらしい名前を付けていたと思います。本パターンにより、そのようなものから脱却し、全体と詳細を分離するという指針をもってメソッドの分割を行うことができるようになります。
以下にStagehand_TestRunnerにおける具体例を示します。
変更前:
<?php
...
class PHPUnitRunner extends Runner
{
...
public function run($suite)
{
$testResult = new \PHPUnit_Framework_TestResult();
if ($this->printsDetailedProgressReport()) {
$printer = new DetailedProgressPrinter(null, false, $this->terminal->colors());
} else {
$printer = new ProgressPrinter(null, false, $this->terminal->colors());
}
$printer->setRunner($this);
$arguments = array();
$arguments['printer'] = $printer;
Stream::register();
$arguments['listeners'] =
array(
new TestDoxPrinter(
fopen('testdox://' . spl_object_hash($testResult), 'w'),
$this->terminal->colors(),
$this->prettifier()
)
);
if ($this->logsResultsInJUnitXML) {
$arguments['listeners'][] = $this->junitXMLPrinterFactory->create(
$this->createStreamWriter($this->junitXMLFile)
);
}
if ($this->stopsOnFailure) {
$arguments['stopOnFailure'] = true;
$arguments['stopOnError'] = true;
}
if ($this->phpunitXMLConfiguration->isEnabled()) {
$arguments['configuration'] = $this->phpunitXMLConfiguration->getFileName();
}
$testRunner = new TestRunner();
$testRunner->setTestResult($testResult);
$testRunner->doRun($suite, $arguments);
$this->notification = $printer->getNotification();
}
...
変更後:
<?php
...
class PHPUnitRunner extends Runner
{
...
public function run($suite)
{
$printer = $this->createPrinter();
$testResult = new \PHPUnit_Framework_TestResult();
$testRunner = new TestRunner();
$testRunner->setTestResult($testResult);
$testRunner->doRun($suite, $this->createArguments($printer, $testResult));
$this->notification = $printer->getNotification();
}
...
protected function createPrinter()
{
if ($this->printsDetailedProgressReport()) {
$printer = new DetailedProgressPrinter(null, false, $this->terminal->colors());
} else {
$printer = new ProgressPrinter(null, false, $this->terminal->colors());
}
$printer->setRunner($this);
return $printer;
}
...
protected function createArguments(ResultPrinter $printer, \PHPUnit_Framework_TestResult $testResult)
{
$arguments = array();
$arguments['printer'] = $printer;
Stream::register();
$arguments['listeners'] =
array(
new TestDoxPrinter(
fopen('testdox://' . spl_object_hash($testResult), 'w'),
$this->terminal->colors(),
$this->prettifier()
)
);
if ($this->logsResultsInJUnitXML) {
$arguments['listeners'][] = $this->junitXMLPrinterFactory->create(
$this->createStreamWriter($this->junitXMLFile)
);
}
if ($this->stopsOnFailure) {
$arguments['stopOnFailure'] = true;
$arguments['stopOnError'] = true;
}
if ($this->phpunitXMLConfiguration->isEnabled()) {
$arguments['configuration'] = $this->phpunitXMLConfiguration->getFileName();
}
return $arguments;
}
...
変更前のrun()メソッドは、テストランナーが引数として受け取ったテストスイートを実行するという全体に、Printerオブジェクトの構成に関するコード、TestRunner::doRun()メソッドの第2引数に渡す配列の構成に関するコードといった詳細が混じっているため、コードの抽象度が揃っておらず読みにくいものになっています。
対して変更後のrun()メソッドは、それら詳細部分を別メソッドとして分離したことにより抽象度が揃い、変更前のものと比べると確実に読みやすくなっています。
問題と解決
概念的に凝集されたメカニズムを切り分けて別個の軽量なフレームワークにすること。特に、形式主義や十分にドキュメント化された種類のアルゴリズムに気を配ること。意図の明白なインタフェースを用いてフレームワークの機能を他から使えるようにすること。これで、ドメインの他の要素は問題を表現すること(「何が」)に集中でき、複雑な解決(「どのように」)はフレームワークに委譲できる。
— Eric Evans エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践) p.429 第4部 戦略的設計 第15章 蒸溜 凝集されたメカニズム(COHESIVE MECHANISMS)
上記はEric Evans氏によるドメインモデルの設計パターンの一つ凝集されたメカニズムにある文です(強調は筆者によるもの)。本パターンはドメインモデルを表現するクラスを対象に書かれたものですが、問題(「何が」)と解決(「どのように」)を分離するという観点は一般的な指針としても活用することができます。
問題空間の言語と解決空間の言語
問題部分と解決部分に分離されたコードは同一のプログラミング言語で書かれてはいるものの本質的に別の言語空間に属すると考えることができます。すなわち、問題空間の言語と解決空間の言語です。特にソフトウェアのコアドメインについては、このような観点の分離が重要となってくるのではないでしょうか。
コードを超えた表現へ
一口にコードの抽象度を整えるといっても掴みどころがなくなかなか難しいものですが、前述のような指針を活用することでより効果的にコードの読みやすさを高めることができます。特に問題と解決、問題空間の言語と解決空間の言語といった観点は、コードの読みやすさの向上だけでなく、ドメイン固有の意図(intention)の発見や意図性(intentionality)の向上にも繋がる可能性があります。そうして抽出された意図はテキストDSLやグラフィカルDSLとして表現され、ソースコードの形態ですらなくなるかもしれません。