Załóżmy, że istnieje Page
klasa, która reprezentuje zestaw instrukcji dla mechanizmu renderującego strony. Załóżmy, że istnieje Renderer
klasa, 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 $pdf
z każdym $data
, aby działał.
Ale mogę stworzyć taką PdfRenderer
klasę:
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 $renderer
otrzymuje PageSnippet
i wszelkie $data
wymagane 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.
Odpowiedzi:
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.źródło
$renderer
on zdecyduje, jak renderować. Kiedy$page
rozmawia się ze$renderer
wszystkim, mówi to, co należy renderować. Nie jak Nie$page
ma pojęcia jak. To wpędza mnie w kłopoty z SRP?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:
Jeśli jesteś zainteresowany, David West mówi o oryginalnych zasadach OOP w swojej książce Object Thinking .
źródło
Tutaj
page
ponosimy 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 „.
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:
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.
źródło
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
Page
jest 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 stwierdzaPage
Klasa nie obchodzi, czy istniejeRenderer
we wszechświecie. Dba tylko o to, aby być reprezentacją strony. Tak więc klasa lub interfejsRenderer
nigdy nie powinny być wymieniane wewnątrzPage
.ZAKTUALIZOWANA ODPOWIEDŹ
Jeśli moje pytanie jest prawidłowe,
PageSnippet
klasa powinna zajmować się tylko fragmentem strony.PdfRenderer
dotyczy renderowania.Wykorzystanie klienta
Kilka punktów do rozważenia:
$data
jako tablicy asocjacyjnej. Powinien to być przykład klasy.html
właściwościach$data
tablicy, jest szczegółem specyficznym dla twojej domeny iPageSnippet
jest świadomy tych szczegółów.źródło
printOn:aStream
metodę, 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.Page
nie wiedzieć, że $ renderer jest niemożliwy. Dodałem trochę kodu do mojego pytania, patrzPageSnippet
klasa. 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ąPageSnippet
klasę, która zawiera tylko tablicę instrukcji tekstowych do pliku PDF, a inna klasa interpretuje te instrukcje. W ten sposób można uniknąć wstrzykiwanie$pdf
podPageSnippet
kosztem dodatkowej złożonościNajlepiej, 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
Page
zawiera „zestaw instrukcji do mechanizmu renderującego strony”. Wyobrażam sobie coś takiego: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.
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ą:
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 maszScreenRenderer
i dodajPrintRenderer
? Ta sama strona może być renderowana przez oba.źródło
page
oczywiste jest, że dane wejściowe do renderera, a nie dane wyjściowe, do tego pojęcia wyraźnie nie pasuje.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.źródło
Myślę, że większość klas można podzielić na jedną z dwóch kategorii:
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ę,
isAdult
która może być wyprowadzona bezpośrednio z niej,birthDate
ale nie funkcję,hasBirthDay
która wymaga informacji zewnętrznych (bieżąca data).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
Page
był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
Page
być 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
Renderer
jest 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
MobileRenderer
i AStandardRenderer
, obie implementacjeRenderer
klasy, ale z różnymi ustawieniami.Ponieważ
Page
zawiera dane i powinien pozostać głupi, najczystszym rozwiązaniem w tym przypadku byłoby przekazaniePage
doRenderer
:źródło