ページレンダラーへの一連の命令を表すPage
クラスがあるとします。そして、画面にページをレンダリングする方法を知っているRenderer
クラスがあるとします。 2つの異なる方法でコードを構造化することが可能です。
/*
* 1) Page Uses Renderer internally,
* or receives it explicitly
*/
$page->renderMe();
$page->renderMe($renderer);
/*
* 2) Page is passed to Renderer
*/
$renderer->renderPage($page);
各アプローチの長所と短所は何ですか?いつが良いですか?どちらがより良いですか?
[〜#〜]背景[〜#〜]
もう少し背景を追加します-同じコードで両方のアプローチを使用していることに気づきました。私はサードパーティを使用していますPDFと呼ばれるライブラリTCPDF
。コードのどこかに私が持っていますにPDFレンダリングが機能するためには、以下が必要です。
$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);
ページの表現を作成したいとします。 PDFページスニペットを次のようにレンダリングするための指示を保持するテンプレートを作成できます。
/*
* A representation of the PDF page snippet:
* a template directing how to render a specific PDF page snippet
*/
class PageSnippet
{
function runTemplate(TCPDF $pdf, array $data = null): void
{
$pdf->writeHTML($data['html']);
}
}
/* To be used like so */
$pdf = new TCPDF();
$data['html'] = "some text";
$snippet = new PageSnippet();
$snippet->runTemplate($pdf, $data);
1)最初のコード例のように、$snippet
が自身を実行することに注意してください。また、$pdf
、および$data
を理解し、機能するために必要です。
しかし、次のようにPdfRenderer
クラスを作成できます。
class PdfRenderer
{
/**@var TCPDF */
protected $pdf;
function __construct(TCPDF $pdf)
{
$this->pdf = $pdf;
}
function runTemplate(PageSnippet $template, array $data = null): void
{
$template->runTemplate($this->pdf, $data);
}
}
そして私のコードはこれに変わります:
$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));
2)ここで、$renderer
は、PageSnippet
と、機能するために必要な$data
を受け取ります。これは、2番目のコード例に似ています。
そのため、レンダラーがページスニペットを受信しても、レンダラー内でスニペットは引き続き自身を実行します。つまり、両方のアプローチが機能しているということです。 OOの使用を1つだけに制限できるか、もう1つだけに制限できるかわかりません。一方を他方でマスクしたとしても、両方が必要になる場合があります。
これは あなたがどう思うかOO is に完全に依存します。
OOP = SOLIDの場合、操作がクラスの単一責任の一部である場合、操作はクラスの一部である必要があります。
OO = virtual dispatch/polymorphismの場合、動的にディスパッチする必要がある場合、つまりインターフェースを介して呼び出される場合、操作はオブジェクトの一部である必要があります。
OO =カプセル化の場合、公開したくない内部状態を使用する場合、操作はクラスの一部である必要があります。
OO =「流暢なインターフェースが好き」の場合、問題はどのバリアントがより自然に読み取るかです。
OO =実世界のエンティティをモデリングする場合、どの実世界のエンティティがこの操作を実行しますか?
これらの視点のすべては通常、単独では間違っています。しかし、これらの視点の1つ以上が設計上の決定に到達するのに役立つ場合があります。
例えば。ポリモーフィズムの視点を使用する:異なるレンダリング戦略(異なる出力フォーマットや異なるレンダリングエンジンなど)がある場合、$renderer->render($page)
は非常に理にかなっています。しかし、異なるレンダリングが必要な異なるページタイプがある場合は、$page->render()
の方が適している場合があります。出力がページタイプとレンダリング戦略の両方に依存している場合は、訪問者パターンを通じて二重ディスパッチを実行できます。
多くの言語では、関数はメソッドである必要がないことを忘れないでください。 render($page)
のような単純な関数は、多くの場合、完全に細かい(そして素晴らしく単純な)ソリューションです。
この質問への答えは明確です。正しい実装であるのは$renderer->renderPage($page);
です。この結論に至った方法を理解するには、カプセル化を理解する必要があります。
ページとは何ですか?それは誰かが消費するディスプレイの表現です。その「誰か」は人間またはボットである可能性があります。 Page
は表現であり、ディスプレイ自体ではないことに注意してください。表現されていない表現が存在しますか?ページはレンダラーのないものですか?答えは「はい」です。表現されなくても表現は存在できます。表現することは後の段階です。
ページのないレンダラーとは何ですか?レンダラーはページなしでレンダリングできますか?いいえ。そのため、RendererインターフェースにはrenderPage($page);
メソッドが必要です。
$page->renderMe($renderer);
の何が問題になっていますか?
これは、renderMe($renderer)
が内部的に$renderer->renderPage($page);
を呼び出す必要があるという事実です。これは デメテルの法則 に違反しています
各ユニットは他のユニットについて限られた知識しか持っていないはずです
Page
クラスは、ユニバースにRenderer
が存在するかどうかを考慮しません。それはページの表現であることにのみ関心があります。そのため、クラスまたはインターフェイスRenderer
をPage
内で記述しないでください。
質問が正しければ、PageSnippet
クラスはページスニペットであることにのみ関係するはずです。
class PageSnippet
{
/** string */
private $html;
function __construct($data = ['html' => '']): void
{
$this->html = $data['html'];
}
public function getHtml()
{
return $this->html;
}
}
PdfRenderer
はレンダリングに関係しています。
class PdfRenderer
{
/**@var TCPDF */
protected $pdf;
function __construct(TCPDF $pdf = new TCPDF())
{
$this->pdf = $pdf;
}
function runTemplate(string $html): void
{
$this->pdf->writeHTML($html);
}
}
$renderer = new PdfRenderer();
$snippet = new PageSnippet(['html' => '<html />']);
$renderer->runTemplate($snippet->getHtml());
考慮すべきいくつかの点:
$data
を連想配列として渡すのは悪い習慣です。クラスのインスタンスである必要があります。$data
配列のhtml
プロパティ内に含まれているという事実は、ドメイン固有の詳細であり、PageSnippet
はこの詳細を認識しています。Alan Kayによると 、オブジェクトは自給自足の「大人の」責任ある生物です。大人は物事を行い、彼らは手術を受けていません。つまり、金融トランザクションは それ自体を保存する を担当し、ページは それ自体をレンダリングする などを担当します。より簡潔に言えば、カプセル化はOOPの大きなものです。特に、それは有名なテル・ドア・アスクの原則(@CandiedOrangeは常に言及することを好む:))と ゲッターとセッター の公の模倣によって明らかになります。
実際には、データベース機能、レンダリング機能などのように、ジョブを実行するために必要なすべてのリソースを持つオブジェクトになります。
したがって、例を考えると、私のOOPバージョンは次のようになります。
class Page
{
private $data;
private $renderer;
public function __construct(ICanRender $renderer, $data)
{
$this->renderer = $renderer;
$this->data = $data;
}
public function render()
{
$this->renderer->render($this->data);
}
}
興味がある場合は、David Westが本の最初のOOP原則、本の Object Thinking について語っています。
$page->renderMe();
ここでは、page
が自身のレンダリングを完全に担当しています。コンストラクターを介してレンダリングが提供されているか、その機能が組み込まれている場合があります。
パラメータとして渡すのとよく似ているので、ここでは最初のケース(コンストラクタを介してレンダリングで提供される)を無視します。代わりに、組み込まれている機能の長所と短所を確認します。
長所は、非常に高レベルのカプセル化を可能にすることです。ページはその内部状態について直接何も明らかにする必要はありません。それ自体のレンダリングを介してそれを公開するだけです。
欠点は、単一責任の原則(SRP)に違反することです。ページの状態のカプセル化を担当するクラスがあり、それ自体をレンダリングする方法に関するルールもハードコードされているため、オブジェクトは他の人に「自分自身に物事を行わせるのではなく、自分自身に物事を行わせるべきではない」という他のあらゆる責任を担っています。 」.
$page->renderMe($renderer);
ここでは、ページ自体をレンダリングできるようにする必要がありますが、実際のレンダリングを実行できるヘルパーオブジェクトをページに提供しています。ここでは2つのシナリオが発生する可能性があります。
$renderer->renderPage($page);
ここでは、SRPを完全に尊重しています。ページオブジェクトはページの情報を保持し、レンダラーはそのページをレンダリングします。ただし、状態全体を公開する必要があるため、ページオブジェクトのカプセル化を完全に弱めました。
また、新しい問題が発生しました。レンダラーがページクラスに密結合されています。ページとは異なるものをレンダリングしたい場合はどうなりますか?
どれが一番いいですか?そのなかで何も。彼らはすべて彼らの欠点を持っています。
理想的には、複雑さを軽減するため、クラス間の依存関係をできるだけ少なくする必要があります。クラスは、本当に必要な場合にのみ、別のクラスへの依存関係を持つ必要があります。
あなたは、Page
に「ページレンダラーへの一連の指示」が含まれていると述べています。私はこのようなものを想像します:
_renderer.renderLine(x, y, w, h, Color.Black)
renderer.renderText(a, b, Font.Helvetica, Color.Black, "bla bla...")
etc...
_
したがって、ページneedsはレンダラーへの参照であるため、$page->renderMe($renderer)
になります。
しかし、代わりに、レンダリング命令は、直接呼び出しではなく、データ構造として表現することもできます。
_[
Line(x, y, w, h, Color.Black),
Text(a, b, Font.Helvetica, Color.Black, "bla bla...")
]
_
この場合、実際のレンダラーはページからこのデータ構造を取得し、対応するレンダリング命令を実行してそれを処理します。このようなアプローチでは、依存関係が逆になります。ページはレンダラーについて知る必要はありませんが、レンダラーにはレンダリング可能なページを提供する必要があります。したがって、オプション2:$renderer->renderPage($page);
それでどちらが一番ですか?最初のアプローチはおそらく実装が最も簡単ですが、2番目のアプローチははるかに柔軟で強力であるため、要件によって異なります。
決定できない場合、または将来的にアプローチを変更する可能性があると思われる場合は、間接決定の層、関数の背後に決定を隠すことができます。
_renderPage($page, $renderer)
_
私がお勧めしない唯一のアプローチは$page->renderMe()
です。これは、ページには単一のレンダラーしか持てないことを示唆しているためです。しかし、ScreenRenderer
があり、PrintRenderer
を追加するとどうなりますか?同じページが両方でレンダリングされる可能性があります。
SOLIDのD部分 は言う
「抽象化は詳細に依存するべきではありません。詳細は抽象化に依存するべきです。」
では、ページとレンダラーの間では、安定した抽象化である可能性が高く、変更される可能性が低く、インターフェイスを表す可能性がありますか?逆に、「詳細」はどれですか?
私の経験では、抽象化は通常レンダラーです。たとえば、非常に抽象的で安定した単純なストリームまたはXMLの場合があります。または、かなり標準的なレイアウト。ページは、カスタムビジネスオブジェクト、つまり「詳細」である可能性が高くなります。そして、「写真」、「レポート」、「チャート」など、レンダリングする他のビジネスオブジェクトがあります(おそらく私のコメントの「tryptich」ではありません)。
しかし、それは明らかにあなたのデザインに依存します。ページは、たとえばHTML <article>
タグと標準のサブパート。また、さまざまなカスタムビジネスレポート「レンダラー」がたくさんあります。その場合、レンダラーはページに依存する必要があります。
ほとんどのクラスは、次の2つのカテゴリのいずれかに分類できると思います。
これらは、他のものにほとんど依存しないクラスです。通常はドメインの一部です。ロジックを含めないか、その状態から直接派生できるロジックのみを含める必要があります。 Employeeクラスは、isAdult
から直接派生できる関数birthDate
を持つことができますが、外部情報(現在の日付)を必要とする関数hasBirthDay
を持つことはできません。
これらのタイプのクラスは、データを含む他のクラスを操作します。通常、これらは一度構成され、不変です(したがって、常に同じ種類の機能を実行します)。ただし、これらの種類のクラスは、ステートフルな短期間ヘルパーインスタンスを提供して、(Builderクラスのように)一定の状態を短時間維持する必要があるより複雑な操作を実行できます。
あなたの例
あなたの例では、Page
はデータを含むクラスです。このデータを取得し、クラスが変更可能であると思われる場合は変更するための関数が必要です。多くの依存関係なしで使用できるように、それはばかげたままにしてください。
データ、またはこの場合、Page
はさまざまな方法で表すことができます。 Webページとしてレンダリングしたり、ディスクに書き込んだり、データベースに保存したり、JSONに変換したりできます。これらのケースのそれぞれについて、そのようなクラスにメソッドを追加したくない(そして、クラスにデータのみが含まれているはずの場合でも、他のすべての種類のクラスに依存関係を作成する)必要はありません。
Renderer
は、典型的なサービスタイプクラスです。特定のデータセットを操作して結果を返すことができます。独自の状態はあまりありません。通常、状態は不変であり、一度設定すれば再利用できます。
たとえば、MobileRenderer
とStandardRenderer
を使用できます。どちらもRenderer
クラスの実装ですが、設定が異なります。
したがって、Page
にはデータが含まれており、簡潔に保つ必要があるため、この場合の最もクリーンな解決策は、Page
をRenderer
に渡すことです。
$renderer->renderPage($page)