オブジェクト指向言語では、オブジェクトはいつ自分自身に対して操作を行う必要があり、いつオブジェクトに対して操作を行う必要がありますか?


11

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つだけに制限できるのかわかりません。一方を他方でマスクする場合でも、両方が必要になる場合があります。


2
残念ながら、スペースまたはタブを使用するかどうか、使用するスタイルを補強するなど、ソフトウェアの「宗教戦争」の世界に迷い込んでいます。ここには「より良い」ものはなく、双方に強い意見があります。リッチドメインモデルと貧血ドメインモデルの両方の利点と欠点をインターネットで検索し、独自の意見を作成します。
デビッドアルノ

7
@DavidArno 異教徒のスペースを使用してください!:)
candied_orange

1
ハ、私は時々真剣にこのサイトを理解していません。良い答えを得る完全に良い質問は、意見に基づいているのですぐに閉じられます。しかし、このような明らかに意見に基づく質問が付随し、それらの通常の容疑者はどこにも見つかりません。まあ、もしあなたがそれらと他のすべてを倒せないなら... :)
デビッドアルノ

@Erik Eidt、答えを元に戻してください
デビッドアルノ

1
SOLIDの原則とは別に、GRASP、特にエキスパートの部分を見ることができます。問題は、あなたが責任を果たすための情報を持っているのはどれですか?
OnesimusUnbound

回答:


13

これは、OOが何であると思うかに完全に依存します

OOP = SOLIDの場合、操作がクラスの単一責任の一部である場合、操作はクラスの一部である必要があります。

OO =仮想ディスパッチ/ポリモーフィズムの場合、動的にディスパッチする必要がある場合、つまりインターフェイスを介して呼び出される場合、操作はオブジェクトの一部である必要があります。

OO =カプセル化の場合、公開したくない内部状態を使用する場合、操作はクラスの一部である必要があります。

OO =「流れるようなインターフェイスが好き」の場合、問題はどのバリアントがより自然に読み取れるかです。

OO =現実世界のエンティティをモデリングする場合、どの現実世界のエンティティがこの操作を実行しますか?


これらの視点はすべて、単独では通常間違っています。ただし、これらの観点の1つまたは複数が、設計上の決定に到達するのに役立つ場合があります。

たとえば、ポリモーフィズムの視点を使用する場合:さまざまなレンダリング戦略(さまざまな出力形式やさまざまなレンダリングエンジンなど)がある場合$renderer->render($page)、非常に理にかなっています。ただし、異なる方法でレンダリングする必要のあるさまざまなページタイプがある場合は、$page->render()改善される可能性があります。出力がページタイプとレンダリング戦略の両方に依存している場合、訪問者パターンを介して二重ディスパッチを実行できます。

多くの言語では、関数はメソッドである必要がないことを忘れないでください。render($page)多くの場合、完全に素晴らしい(そして素晴らしく単純な)ソリューションのような単純な関数。


ちょっと待って ページがレンダラーへの参照を保持しているが、どのレンダラーが保持しているかわからない場合でも、ポリモーフィックレンダリングを取得できます。多型がウサギの穴の少し下にあることを意味します。また、レンダラーに渡すものを選択することもできます。ページ全体を渡す必要はありません。
candied_orange

@CandiedOrangeそれは良い点ですが、私はあなたの議論をSRPの下で予約します。それは、おそらく何らかのポリモーフィックレンダリング戦略を使用して、レンダリング方法を決定するのはページの大文字R責任です。
アモン

$rendererはレンダリング方法を決定しようとしていたと考えました。すべてに$page話すとき、$rendererそれは何をレンダリングするかということです。どうでもいい。$pageどのようにないアイデアを持っていません。それでSRPのトラブルに巻き込まれますか?
candied_orange

私たちは意見が違うとは本当に思わない。私はあなたの最初のコメントをこの答えの概念的な枠組みに分類しようとしていましたが、不器用な言葉を使ったかもしれません。あなたが私が答えで言及しなかったことを私に思い出させている1つのこと:データフローを聞かないでくださいも良い発見的です。
アモン

うーん あなたが正しい。私が話してきたことは、言わないでください。私が間違っている場合、今私を修正します。レンダラーがページ参照を取得するもう1つの方法は、ページゲッターを使用して、レンダラーが向きを変えてページの内容を尋ねる必要があることを意味します。
candied_orange

2

アランケイによると、オブジェクトは自給自足の「大人」で責任ある生物です。大人は物事を行いますが、彼らは手術を受けません。つまり、金融取引はそれ自体保存する責任があり、ページはそれ自体レンダリングする責任などがあります。より簡潔に言えば、カプセル化はOOPで大きなことです。特に、有名なTell do n't ask原則(@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は著書Object ThinkingでオリジナルのOOP原則について語っています。


1
率直に言って、15年前に、歴史的な関心を除いて、誰かがソフトウェア開発に関係することについて言ったことを気にしているのは誰ですか?
デビッドアルノ

1
オブジェクト指向の概念を発明した人が、オブジェクトとは何かについて言ったことに関心があります。」あなたの議論で「権威へのアピール」の誤usingを使用するようにあなたを説得する以外に、用語の発明者の考えがその用語の適用に15年後にどのような影響を与える可能性がありますか?
デビッドアルノ

2
@Zapadlo:ページからレンダラーへのメッセージであり、その逆ではない理由については説明しません。彼らは両方とも対象であり、したがって両方とも大人ですよね?
ジャックB

1
権威へのアピールはここでは適用できません」...「だからあなたの意見ではOOPを表す概念のセットは実際には間違っています(元の定義のゆがみのため)」当局の誤acyに対する訴えが何であるかわからないので、私はそれを取る?手がかり:ここで使用しました。:)
デビッドアルノ

1
@David Arnoそれでは、権威への訴えはすべて間違っていますか?「私の意見にアピールしますか?」?毎回誰かがおじさんBobismを引き合いに出して、あなたが当局にアピール文句を言うだろうZapadioは尊敬ソースを提供あなたは同意しない、または矛盾する情報源を引用するが、repeatefly誰かが引用は建設的ではありません提供していたことを不満をすることができます。。
user949300

2

$page->renderMe();

ここではpage、レンダリング自体を完全に担当しています。コンストラクターを介してレンダーが提供されている場合や、その機能が組み込まれている場合があります。

ここでは、最初のケース(コンストラクターを介してレンダリングで提供される)を無視します。これは、パラメーターとして渡すのと非常に似ているためです。代わりに、組み込みの機能の長所と短所を見ていきます。

長所は、非常に高いレベルのカプセル化を可能にすることです。ページは、その内部状態について直接何も明らかにする必要はありません。それ自体のレンダリングを介してのみ公開します。

欠点は、単一責任原則(SRP)に違反することです。ページの状態のカプセル化を担当するクラスがあり、それ自体をレンダリングする方法に関するルールでハードコーディングされているため、オブジェクトが他の人によって処理されるのではなく、自分自身に対して処理を実行する必要があるため「。

$page->renderMe($renderer);

ここでは、ページが自分自身をレンダリングできるようにする必要がありますが、実際のレンダリングを実行できるヘルパーオブジェクトを提供しています。ここで2つのシナリオが発生する可能性があります。

  1. ページは、そのレンダリングを作成するために、レンダリングルール(どのメソッドをどの順序で呼び出すか)を知っている必要があります。カプセル化は保持されますが、ページがレンダリングプロセスを監視する必要があるため、SRPは依然として破損しています。または
  2. ページはレンダラーオブジェクトの1つのメソッドを呼び出して、その詳細を渡します。SRPを尊重する方法に近づいていますが、カプセル化が弱くなっています。

$renderer->renderPage($page);

ここでは、SRPを完全に尊重しています。ページオブジェクトはページ上の情報を保持し、レンダラーはそのページのレンダリングを担当します。ただし、ページオブジェクトの状態全体を公開する必要があるため、ページオブジェクトのカプセル化を完全に弱めました。

また、新しい問題を作成しました。レンダラーはページクラスに密接に結合されています。ページと異なるものをレンダリングしたい場合はどうなりますか?

どちらがベストですか?そのなかで何も。彼らはすべて彼らの欠陥を持っています。


V3がSRPを尊重することに同意しません。レンダラーには少なくとも2つの変更理由があります。ページが変更された場合、またはレンダリング方法が変更された場合です。また、レンダラーがページ以外のオブジェクトをレンダリングする必要がある場合は、3つ目をカバーします。そうでなければ、素晴らしい分析。
user949300

2

この質問への答えは明確です。これが$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連想配列として渡すのは悪い習慣です。クラスのインスタンスでなければなりません。
  • ページ形式が配列のhtmlプロパティ内に含まれているという事実$dataは、ドメインに固有の詳細でありPageSnippet、この詳細を認識しています。

しかし、Pagesに加えて、写真、記事、Triptichがある場合はどうでしょうか?あなたのスキームでは、レンダラーはそれらすべてについて知る必要があります。それは多くの漏れです。思考の糧。
user949300

@ user949300:レンダラーが写真などをレンダリングできるようにする必要がある場合は、明らかにそれらについて知る必要があります。
ジャックB

1
Kent BeckによるSmalltalk Best Practice Patternsでは、Reversing Methodパターンが導入されており、これらの両方がサポートされています。リンクされた記事は、オブジェクトがprintOn:aStreamメソッドをサポートしていることを示していますが、オブジェクトが印刷するようストリームに指示するだけです。あなたの答えとの類推は、レンダラーにレンダリングできるページと、ページをレンダリングできるレンダラーの両方を、1つの実装と便利なインターフェースの選択で両方持つことができない理由がないということです。
グラハムリー

2
いずれにせよ、SRPを破る必要がありますが、レンダラーが多くの異なるものをレンダリングする方法を知る必要がある場合、それは実際には「多くの責任」であり、可能であれば回避する必要があります。
user949300

1
私はあなたの答えが好きですがPage、$ rendererに気付かないことは不可能だと思いたいです。PageSnippetクラスにいくつかのコードを質問に追加しました。これは事実上ページですが、を何らかの形で参照しないと存在できません。$pdf実際には、この場合はサードパーティのPDFレンダラーです。..ただし、PageSnippetPDFへのテキスト命令の配列のみを保持するようなクラスを作成し、他のクラスにそれらの命令を解釈させることができると思います。そう$pdfすればPageSnippet、余分な複雑さを犠牲にして、への注入を避けることができます
デニス

1

理想的には、複雑さを軽減するため、クラス間の依存関係をできるだけ少なくしたいと考えています。クラスは、本当に必要な場合にのみ、別のクラスへの依存関係を持つ必要があります。

あなたにPageは「ページレンダラーへの指示のセット」が含まれています。私はこのようなものを想像します:

renderer.renderLine(x, y, w, h, Color.Black)
renderer.renderText(a, b, Font.Helvetica, Color.Black, "bla bla...")
etc...

だからでしょう$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()は、ページが単一のレンダラーのみを持つことができることを示唆しているためです。しかし、aがありa ScreenRendererを追加しPrintRendererたらどうなるでしょうか?同じページが両方でレンダリングされる場合があります。


EPUBまたはHTMLのコンテキストでは、ページの概念はレンダラーなしでは存在しません。
mouviciel

1
@mouviciel:あなたが何を言っているのか理解できません。レンダリングせずにHTMLページを作成できますか?たとえば、Googleクローラーはページをレンダリングせずに処理します。
ジャックB

2
ワードページには別の概念があります。HTMLページが印刷用にフォーマットされたときのページネーションプロセスの結果、おそらく@mouvicielが念頭に置いていたものです。ただし、この質問でpageは、a は明らかにレンダラーへの入力であり、出力ではなく、その概念は明らかに適合しません。
Doc Brown

1

SOLIDD部分には、

「抽象化は詳細に依存すべきではありません。詳細は抽象化に依存すべきです。」

では、PageとRendererの間では、安定した抽象化である可能性が高く、変更される可能性は低く、インターフェイスを表している可能性があります。反対に、「詳細」はどれですか?

私の経験では、抽象化は通常レンダラーです。たとえば、非常に抽象的で安定した単純なストリームまたはXMLの場合があります。または、かなり標準的なレイアウト。ページは、カスタムビジネスオブジェクトである「詳細」である可能性が高くなります。そして、「写真」、「レポート」、「チャート」など、レンダリングされる他のビジネスオブジェクトがあります。

しかし、それは明らかにあなたのデザインに依存します。ページは抽象的である可能性があります。たとえば、<article>標準のサブパートを持つHTML タグに相当します。また、さまざまなカスタムビジネスレポート「レンダラー」があります。その場合、レンダラーはページに依存する必要があります。


0

ほとんどのクラスは、次の2つのカテゴリのいずれかに分割できると思います。

  • データを含むクラス(可変または不変は関係ありません)

これらは、他にほとんど依存しないクラスです。通常、これらはドメインの一部です。ロジックを含まないか、その状態から直接派生できるロジックのみを含める必要があります。Employeeクラスは、外部情報(現在の日付)を必要とする関数isAdultからではbirthDateなく、直接派生できる関数を持つことができますhasBirthDay

  • サービスを提供するクラス

これらのタイプのクラスは、データを含む他のクラスで動作します。通常、これらは一度構成され、不変です(したがって、常に同じ種類の機能を実行します)。ただし、これらの種類のクラスは、ステートフルな短命のヘルパーインスタンスを提供して、短期間に何らかの状態を維持する必要があるより複雑な操作を実行できます(Builderクラスなど)。

あなたの例

あなたの例でPageは、データを含むクラスになります。このデータを取得し、クラスが可変であると想定される場合はおそらく変更するための関数が必要です。バカにしておくので、多くの依存関係なしで使用できます。

データ、またはこの場合Pageは、さまざまな方法で表すことができます。Webページとしてレンダリングしたり、ディスクに書き込んだり、データベースに保存したり、JSONに変換したりできます。これらの各ケースについて、そのようなクラスにメソッドを追加する必要はありません(クラスにデータが含まれているだけであっても、他のすべてのクラスに依存関係を作成します)。

あなたRendererは典型的なサービスタイプのクラスです。特定のデータセットを操作し、結果を返すことができます。独自の状態はあまりありません。通常、どの状態にあるかは不変であり、一度構成してから再利用できます。

たとえば、a MobileRendererとa を使用できますがStandardRenderer、両方ともRendererクラスの実装ですが、設定は異なります。

だから、などPageのデータが含まれており、ダム保つ必要があり、この場合はきれいな解決策を渡すことであろうPageRenderer

$renderer->renderPage($page)

2
非常に手続き的なロジック。
user949300
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.