W językach obiektowych, kiedy obiekty powinny wykonywać operacje na sobie, a kiedy operacje na obiektach?

11

Załóżmy, że istnieje Pageklasa, która reprezentuje zestaw instrukcji dla mechanizmu renderującego strony. Załóżmy, że istnieje Rendererklasa, która wie, jak renderować stronę na ekranie. Istnieje możliwość strukturyzacji kodu na dwa różne sposoby:

/*
 * 1) Page Uses Renderer internally,
 * or receives it explicitly
 */
$page->renderMe(); 
$page->renderMe($renderer); 

/*
 * 2) Page is passed to Renderer
 */
$renderer->renderPage($page);

Jakie są zalety i wady każdego podejścia? Kiedy będzie lepiej? Kiedy ten drugi będzie lepszy?


TŁO

Aby dodać trochę więcej tła - używam obu podejść w tym samym kodzie. Korzystam z zewnętrznej biblioteki plików PDF o nazwie TCPDF. Gdzieś w kodzie muszę mieć następujące elementy do renderowania PDF:

$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);

Powiedz, że chcę utworzyć reprezentację strony. Mógłbym utworzyć szablon, który zawiera instrukcje renderowania fragmentu strony PDF w następujący sposób:

/*
 * 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) Zauważ, że tutaj $snippet działa samo , jak w moim pierwszym przykładzie kodu. Musi także wiedzieć i być zaznajomiony $pdfz każdym $data, aby działał.

Ale mogę stworzyć taką PdfRendererklasę:

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);
    }
}

a następnie mój kod zwraca się do tego:

$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));

2) Tutaj $rendererotrzymuje PageSnippeti wszelkie $datawymagane do jego działania. Jest to podobne do mojego drugiego przykładu kodu.

Tak więc, mimo że moduł renderujący otrzymuje fragment strony, wewnątrz modułu renderującego nadal działa . To znaczy, że oba podejścia są w grze. Nie jestem pewien, czy możesz ograniczyć użycie OO tylko do jednego lub tylko drugiego. Oba mogą być wymagane, nawet jeśli zamaskujesz jeden po drugim.

Dennis
źródło
2
Niestety, wędrowałeś tutaj w świecie oprogramowania „wojen religijnych”, zgodnie z tym, czy używać spacji, czy zakładek, które mają być w stylu, itp. Nie ma tutaj „lepszego”, tylko silne opinie po obu stronach. Wyszukaj w Internecie zalety i wady zarówno bogatych, jak i anemicznych modeli domen i stwórz własną opinię.
David Arno,
7
@DavidArno Używaj przestrzeni, którą poganie! :)
candied_orange
1
Ha, poważnie czasami nie rozumiem tej strony. Idealnie dobre pytania, na które można uzyskać dobre odpowiedzi, są zamykane w krótkim czasie jako oparte na opiniach. Pojawia się jednak oczywiście takie pytanie oparte na opiniach i nigdzie nie można znaleźć zwykłych podejrzanych. No cóż, jeśli nie możesz ich pokonać i tak dalej ... :)
David Arno
@Erik Eidt, czy możesz cofnąć usunięcie swojej odpowiedzi, ponieważ uważam ją za bardzo dobrą odpowiedź „czwarta opcja”.
David Arno,
1
Oprócz zasad SOLID, możesz przyjrzeć się GRASP , szczególnie w części Expert . Pytanie brzmi, które informacje są potrzebne do wypełnienia obowiązków?
Onesimus

Odpowiedzi:

13

Zależy to całkowicie od tego, co Twoim zdaniem jest OO .

W przypadku OOP = SOLID operacja powinna być częścią klasy, jeśli jest częścią pojedynczej odpowiedzialności klasy.

W przypadku OO = wirtualna wysyłka / polimorfizm operacja powinna być częścią obiektu, jeśli powinna zostać wywołana dynamicznie, tj. Jeśli zostanie wywołana przez interfejs.

W przypadku OO = enkapsulacji operacja powinna być częścią klasy, jeśli używa stanu wewnętrznego, którego nie chcesz ujawniać.

Dla OO = „Lubię płynne interfejsy”, pytanie brzmi, który wariant czyta bardziej naturalnie.

Dla OO = modelowanie bytów realnych, który byt realny wykonuje tę operację?


Wszystkie te punkty widzenia są zwykle błędne w oderwaniu. Ale czasami jedna lub więcej z tych perspektyw pomaga w podjęciu decyzji projektowej.

Np. Używając punktu widzenia polimorfizmu: jeśli masz różne strategie renderowania (takie jak różne formaty wyjściowe lub różne silniki renderowania), to $renderer->render($page)ma wiele sensu. Ale jeśli masz różne typy stron, które powinny być renderowane inaczej, $page->render()może być lepiej. Jeśli wyniki zależą zarówno od typu strony, jak i strategii renderowania, możesz wykonać podwójną wysyłkę za pomocą wzorca odwiedzającego.

Nie zapominaj, że w wielu językach funkcje nie muszą być metodami. Prosta funkcja, jak render($page)często idealne (i cudownie proste) rozwiązanie.

amon
źródło
Eee poczekaj chwilę. Nadal mogę uzyskać renderowanie polimorficzne, jeśli strona zawiera odwołanie do renderera, ale nie ma pojęcia, który renderer posiada. To po prostu oznacza, że ​​polimorfizm jest nieco dalej w dół króliczej nory. Mogę również wybrać i wybrać, co przekazać do renderera. Nie muszę przechodzić całej strony.
candied_orange
@CandiedOrange To dobra uwaga, ale chciałbym zaksięgować twój argument w ramach SRP: decyzja o sposobie renderowania będzie zależała od kapitału strony, być może przy użyciu jakiejś strategii renderowania polimorficznego.
amon
Uznałem, że to $rendereron zdecyduje, jak renderować. Kiedy $pagerozmawia się ze $rendererwszystkim, mówi to, co należy renderować. Nie jak Nie $pagema pojęcia jak. To wpędza mnie w kłopoty z SRP?
candied_orange
Naprawdę nie sądzę, żebyśmy się nie zgadzali. Próbowałem uporządkować twój pierwszy komentarz w ramy koncepcyjne tej odpowiedzi, ale mogłem użyć niezdarnych słów. Jedną rzecz, którą mi przypominasz, o której nie wspomniałem w odpowiedzi: przepływ danych typu „nie pytaj” jest również dobrą heurystyką.
amon
Hmm ok. Masz rację. To, o czym mówię, nastąpiłoby po „nie pytaj”. Teraz popraw mnie, jeśli się mylę. Druga strategia, w której moduł renderujący pobiera odwołanie do strony, oznacza, że ​​moduł renderujący musiałby się odwrócić i poprosić stronę o różne rzeczy, korzystając z modułów pobierających strony.
candied_orange
2

Według Alana Kay przedmioty są samowystarczalnymi, „dorosłymi” i odpowiedzialnymi organizmami. Dorośli robią rzeczy, na których nie działają. Oznacza to, że transakcja finansowa jest odpowiedzialna za samo uratowanie się , strona jest odpowiedzialna za renderowanie siebie itp. Krótko mówiąc, enkapsulacja to wielka rzecz w OOP. W szczególności przejawia się to w słynnej zasadzie Tell not ask (którą @CandiedOrange lubi cały czas wspominać :)) oraz publicznym upominaniu się o osoby pobierające i ustawiające .

W praktyce powoduje to, że obiekty posiadają wszystkie niezbędne zasoby do wykonywania swojej pracy, takie jak bazy danych, funkcje renderowania itp.

Biorąc pod uwagę twój przykład, moja wersja OOP wyglądałaby następująco:

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);
    }
}

Jeśli jesteś zainteresowany, David West mówi o oryginalnych zasadach OOP w swojej książce Object Thinking .

Zapadło
źródło
1
Mówiąc wprost, kogo obchodzi to, co ktoś powiedział o czymś związanym z tworzeniem oprogramowania, 15 lat temu, z wyjątkiem interesu historycznego?
David Arno,
1
Nie obchodzi mnie, co powiedział człowiek, który wynalazł obiektową koncepcję, o tym, czym jest przedmiot. ” Dlaczego? Co więcej, niż zachęcanie cię do stosowania w swoich argumentach błędów „odwoływania się do autorytetu”, jakie ewentualne myśli wynalazcy terminu mogłyby mieć zastosowanie w przypadku terminu 15 lat później?
David Arno
2
@Zapadlo: Nie przedstawiasz argumentu, dlaczego wiadomość jest przekazywana ze strony do renderera, a nie na odwrót. Oboje są przedmiotami, a zatem oboje są dorośli, prawda?
JacquesB
1
Nie można tu zastosować odwołania do błędnego autorytetu ”… „ Tak więc zestaw pojęć, które Twoim zdaniem reprezentują OOP, jest w rzeczywistości błędny [ponieważ jest to zniekształcenie oryginalnej definicji] ”. Rozumiem, że nie wiesz, jaki jest apel do błędu władzy? Wskazówka: użyłeś tutaj jednego. :)
David Arno,
1
@David Arno Czy wszystkie odwołania do władz są złe? Czy wolisz „Odwołaj się od mojej opinii”? Czy za każdym razem, gdy ktoś powołuje się na wujek Bobizm, narzekasz na apelację do władzy? Zapadio zapewniał szanowane źródło. Możesz się nie zgadzać lub cytować źródła sprzeczne, ale powtarzanie, że ktoś podał cytat, nie jest konstruktywne.
user949300
2

$page->renderMe();

Tutaj pageponosimy całkowitą odpowiedzialność za samo renderowanie. Być może został dostarczony z renderowaniem za pośrednictwem konstruktora lub może mieć wbudowaną tę funkcję.

Zignoruję tutaj pierwszy przypadek (dostarczany z renderowaniem przez konstruktor), ponieważ jest on podobny do przekazywania go jako parametru. Zamiast tego przyjrzę się zaletom i wadom wbudowanej funkcjonalności.

Zaletą jest to, że pozwala na bardzo wysoki poziom enkapsulacji. Strona nie musi bezpośrednio zdradzać niczego o swoim stanie wewnętrznym. Odsłania to tylko poprzez renderowanie samego siebie.

Wadą jest to, że łamie zasadę pojedynczej odpowiedzialności (SRP). Mamy klasę, która odpowiada za enkapsulację stanu strony, a także jest mocno zakodowana z regułami, jak się wyrenderować, a tym samym prawdopodobnie cały szereg innych obowiązków, ponieważ obiekty powinny „robić sobie same, a nie robić rzeczy innym „.

$page->renderMe($renderer);

Wciąż wymagamy, aby strona mogła się renderować, ale dostarczamy obiekt pomocnika, który może wykonać rzeczywiste renderowanie. Mogą tu wystąpić dwa scenariusze:

  1. Strona musi po prostu znać reguły renderowania (metody wywoływania w jakiej kolejności), aby utworzyć renderowanie. Hermetyzacja jest zachowana, ale SRP jest nadal zepsuty, ponieważ strona wciąż musi nadzorować proces renderowania, lub
  2. Strona po prostu wywołuje jedną metodę dla obiektu renderera, przekazując jego szczegóły. Zbliżamy się do respektowania SRP, ale teraz osłabiliśmy enkapsulację.

$renderer->renderPage($page);

Tutaj w pełni przestrzegamy SRP. Obiekt strony jest odpowiedzialny za przechowywanie informacji na stronie, a mechanizm renderujący jest odpowiedzialny za renderowanie tej strony. Jednak całkowicie osłabiliśmy enkapsulację obiektu strony, ponieważ musi on upublicznić cały swój stan.

Stworzyliśmy również nowy problem: moduł renderujący jest teraz ściśle powiązany z klasą strony. Co się stanie, gdy chcemy renderować coś innego na stronie?

Który jest najlepszy Żaden z nich. Wszystkie mają swoje wady.

David Arno
źródło
Nie zgadzam się, że V3 szanuje SRP. Renderer ma co najmniej 2 powody do zmiany: jeśli Strona się zmieni lub jeśli zmieni się sposób jej renderowania. I trzeci, który obejmujecie, jeśli moduł renderujący musi renderować obiekty inne niż strony. W przeciwnym razie niezła analiza.
user949300
2

Odpowiedź na to pytanie jest jednoznaczna. To jest $renderer->renderPage($page);właściwa implementacja. Aby zrozumieć, w jaki sposób doszliśmy do tego wniosku, musimy zrozumieć enkapsulację.

Co to jest strona? Jest to przedstawienie wyświetlacza, które ktoś konsumuje. Tym „kimś” może być człowiek lub boty. Pamiętaj, że Pagejest to reprezentacja, a nie sam ekran. Czy reprezentacja istnieje bez reprezentacji? Czy strona jest czymś bez renderera? Odpowiedź brzmi: tak, reprezentacja może istnieć bez reprezentacji. Reprezentowanie jest późniejszym etapem.

Co to jest renderer bez strony? Czy renderer może renderować bez strony? Nie. Interfejs renderujący potrzebuje tej renderPage($page);metody.

Co jest nie tak z $page->renderMe($renderer);?

Jest to fakt, że renderMe($renderer)nadal będzie musiał dzwonić wewnętrznie $renderer->renderPage($page);. To narusza prawo Demeter, które stwierdza

Każda jednostka powinna mieć jedynie ograniczoną wiedzę na temat innych jednostek

PageKlasa nie obchodzi, czy istnieje Rendererwe wszechświecie. Dba tylko o to, aby być reprezentacją strony. Tak więc klasa lub interfejs Renderernigdy nie powinny być wymieniane wewnątrz Page.


ZAKTUALIZOWANA ODPOWIEDŹ

Jeśli moje pytanie jest prawidłowe, PageSnippetklasa powinna zajmować się tylko fragmentem strony.

class PageSnippet
{    
    /** string */
    private $html;

    function __construct($data = ['html' => '']): void
    {
        $this->html = $data['html'];
    }

   public function getHtml()
   {
       return $this->html;
   }
}

PdfRenderer dotyczy renderowania.

class PdfRenderer
{
    /**@var TCPDF */
    protected $pdf;

    function __construct(TCPDF $pdf = new TCPDF())
    {
        $this->pdf = $pdf;
    }

    function runTemplate(string $html): void
    {
        $this->pdf->writeHTML($html);
    }
}

Wykorzystanie klienta

$renderer = new PdfRenderer();
$snippet = new PageSnippet(['html' => '<html />']);
$renderer->runTemplate($snippet->getHtml());

Kilka punktów do rozważenia:

  • Złą praktyką jest przekazywanie jej $datajako tablicy asocjacyjnej. Powinien to być przykład klasy.
  • Fakt, że format strony jest zawarty we htmlwłaściwościach $datatablicy, jest szczegółem specyficznym dla twojej domeny i PageSnippetjest świadomy tych szczegółów.
Juzer Ali
źródło
Ale co jeśli, oprócz stron, masz zdjęcia, artykuły i tryptyki? W twoim schemacie Renderer musiałby wiedzieć o nich wszystkich. To duży wyciek. Tylko jedzenie do namysłu.
user949300
@ user949300: Jeśli Renderer musi być w stanie renderować obrazy itp., to oczywiście musi o nich wiedzieć.
JacquesB
1
Wzorce najlepszych praktyk Smalltalk autorstwa Kent Beck wprowadzają wzorzec metody cofania , w którym oba są obsługiwane. Połączony artykuł pokazuje, że obiekt obsługuje printOn:aStreammetodę, ale jedyne, co robi, to nakazać strumieniowi wydrukowanie obiektu. Analogia z twoją odpowiedzią jest taka, że ​​nie ma powodu, dla którego nie można mieć zarówno strony, która może być renderowana do renderera, jak i renderera, który może renderować stronę, z jedną implementacją i wyborem wygodnych interfejsów.
Graham Lee
2
W każdym razie będziesz musiał złamać / sfałszować SRP, ale jeśli Renderer musi wiedzieć, jak renderować wiele różnych rzeczy, to tak naprawdę jest to „wiele odpowiedzialności” i, jeśli to możliwe, należy tego unikać.
user949300
1
Podoba mi się twoja odpowiedź, ale kusi mnie, aby Pagenie wiedzieć, że $ renderer jest niemożliwy. Dodałem trochę kodu do mojego pytania, patrz PageSnippetklasa. W rzeczywistości jest to strona, ale nie może istnieć bez odniesienia do $pdf, który w tym przypadku jest zewnętrznym mechanizmem renderującym pliki PDF. .. Jednak przypuszczam, że chociaż mógłbym stworzyć taką PageSnippetklasę, która zawiera tylko tablicę instrukcji tekstowych do pliku PDF, a inna klasa interpretuje te instrukcje. W ten sposób można uniknąć wstrzykiwanie $pdfpod PageSnippetkosztem dodatkowej złożoności
Dennis
1

Najlepiej, jeśli chcesz jak najmniej zależności między klasami, ponieważ zmniejsza to złożoność. Klasa powinna mieć zależność od innej klasy, jeśli naprawdę jej potrzebuje.

Stan Pagezawiera „zestaw instrukcji do mechanizmu renderującego strony”. Wyobrażam sobie coś takiego:

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

Tak by było $page->renderMe($renderer), ponieważ strona potrzebuje odniesienia do renderera.

Ale alternatywnie renderowanie instrukcji może być również wyrażone jako struktura danych zamiast bezpośrednich połączeń, np.

[
  Line(x, y, w, h, Color.Black), 
  Text(a, b, Font.Helvetica, Color.Black, "bla bla...")
]

W takim przypadku faktyczny moduł renderujący pobierze tę strukturę danych ze strony i przetworzy ją, wykonując odpowiednie instrukcje renderowania. Przy takim podejściu zależności zostałyby odwrócone - Strona nie musi wiedzieć o Renderze, ale Renderer powinien otrzymać Stronę, którą może następnie renderować. Więc opcja druga:$renderer->renderPage($page);

Więc który jest najlepszy? Pierwsze podejście jest prawdopodobnie najłatwiejsze do wdrożenia, podczas gdy drugie jest znacznie bardziej elastyczne i wydajne, więc myślę, że zależy to od twoich wymagań.

Jeśli nie możesz się zdecydować lub uważasz, że możesz zmienić podejście w przyszłości, możesz ukryć decyzję za warstwą pośrednią, funkcją:

renderPage($page, $renderer)

Jedyne podejście, którego nie zalecam, $page->renderMe()to sugerowanie, że strona może mieć tylko jeden mechanizm renderujący. Ale co, jeśli masz ScreenRendereri dodaj PrintRenderer? Ta sama strona może być renderowana przez oba.

JacquesB
źródło
W kontekście EPUB lub HTML koncepcja strony nie istnieje bez mechanizmu renderującego.
mouviciel
1
@mouviciel: Nie jestem pewien, czy rozumiem, co masz na myśli. Z pewnością możesz mieć stronę HTML bez jej renderowania? Na przykład robot indeksujący Google przetwarza strony bez ich renderowania.
JacquesB
2
Istnieje inne pojęcie strony słowo: wynik procesu stronicowania, gdy strona HTML sformatowana do wydrukowania, może właśnie to miał na myśli @mouviciel. Jednak w tym pytaniu pageoczywiste jest, że dane wejściowe do renderera, a nie dane wyjściowe, do tego pojęcia wyraźnie nie pasuje.
Doc Brown
1

Część D SOLID mówi

„Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji”.

Tak więc między Page a Renderer, co jest bardziej prawdopodobne, że jest to stabilna abstrakcja, rzadziej ulegająca zmianie, prawdopodobnie reprezentująca interfejs? Przeciwnie, jaki jest „szczegół”?

Z mojego doświadczenia wynika, że ​​abstrakcją jest zazwyczaj Renderujący. Na przykład może to być prosty strumień lub XML, bardzo abstrakcyjny i stabilny. Lub jakiś dość standardowy układ. Twoja strona prawdopodobnie będzie niestandardowym obiektem biznesowym, „detalem”. I masz do renderowania inne obiekty biznesowe, takie jak „obrazy”, „raporty”, „wykresy” itp. (Prawdopodobnie nie jest to „tryptich” jak w moim komentarzu)

Ale to oczywiście zależy od twojego projektu. Strona może być abstrakcyjna, na przykład odpowiednik <article>tagu HTML ze standardowymi częściami. I masz wiele różnych „renderujących” niestandardowe raporty biznesowe. W takim przypadku Renderer powinien zależeć od strony.

użytkownik949300
źródło
0

Myślę, że większość klas można podzielić na jedną z dwóch kategorii:

  • Klasy zawierające dane (zmienne lub niezmienne nie mają znaczenia)

Są to klasy, które prawie nie mają zależności od niczego innego. Zazwyczaj są częścią Twojej domeny. Nie powinny zawierać żadnej logiki lub tylko logikę, którą można wyprowadzić bezpośrednio z jej stanu. Klasa pracownika może mieć funkcję, isAdultktóra może być wyprowadzona bezpośrednio z niej, birthDateale nie funkcję, hasBirthDayktóra wymaga informacji zewnętrznych (bieżąca data).

  • Klasy, które świadczą usługi

Te typy klas działają na innych klasach zawierających dane. Zazwyczaj są one konfigurowane raz i niezmienne (więc zawsze pełnią tę samą funkcję). Tego rodzaju klasy mogą jednak nadal zapewniać stanową, krótkotrwałą instancję pomocnika do wykonywania bardziej złożonych operacji, które wymagają utrzymywania pewnego stanu przez krótki czas (np. Klasy Builder).

Twój przykład

W twoim przykładzie Pagebyłaby klasa zawierająca dane. Powinien mieć funkcje do pobierania tych danych i być może modyfikowania ich, jeśli klasa ma być zmienna. Trzymaj to głupie, aby można było z niego korzystać bez wielu zależności.

Dane, w tym przypadku możesz Pagebyć reprezentowany na wiele sposobów. Może być renderowany jako strona internetowa, zapisywany na dysk, przechowywany w bazie danych, konwertowany na JSON, cokolwiek. Nie chcesz dodawać metod do takiej klasy dla każdego z tych przypadków (i tworzyć zależności od wszelkiego rodzaju innych klas, nawet jeśli twoja klasa powinna zawierać tylko dane).

Twoja Rendererjest typową klasą typu usługi. Może operować na określonym zestawie danych i zwrócić wynik. Nie ma własnego własnego stanu, a jaki jest zwykle niezmienny, można go raz skonfigurować, a następnie użyć ponownie.

Na przykład, możesz mieć A MobileRendereri A StandardRenderer, obie implementacje Rendererklasy, ale z różnymi ustawieniami.

Ponieważ Pagezawiera dane i powinien pozostać głupi, najczystszym rozwiązaniem w tym przypadku byłoby przekazanie Pagedo Renderer:

$renderer->renderPage($page)
john16384
źródło
2
Bardzo logika proceduralna.
user949300,