Supongamos que hay una Page
clase, que representa un conjunto de instrucciones para un procesador de páginas. Y supongamos que hay una Renderer
clase que sabe cómo representar una página en la pantalla. Es posible estructurar el código de dos maneras diferentes:
/*
* 1) Page Uses Renderer internally,
* or receives it explicitly
*/
$page->renderMe();
$page->renderMe($renderer);
/*
* 2) Page is passed to Renderer
*/
$renderer->renderPage($page);
¿Cuáles son los pros y los contras de cada enfoque? ¿Cuándo será uno mejor? ¿Cuándo será mejor el otro?
ANTECEDENTES
Para agregar un poco más de antecedentes, me encuentro usando ambos enfoques en el mismo código. Estoy usando una biblioteca PDF de terceros llamada TCPDF
. En algún lugar de mi código tengo que tener lo siguiente para que funcione la representación de PDF:
$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);
Digamos que deseo crear una representación de la página. Podría crear una plantilla que contenga instrucciones para representar un fragmento de página PDF de esta manera:
/*
* 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) Observe aquí que se $snippet
ejecuta solo , como en mi primer ejemplo de código. También necesita saber y estar familiarizado con el $pdf
, y con cualquiera $data
para que funcione.
Pero, puedo crear una PdfRenderer
clase así:
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);
}
}
y luego mi código se convierte en esto:
$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));
2) Aquí $renderer
recibe el PageSnippet
y todo lo $data
necesario para que funcione. Esto es similar a mi segundo ejemplo de código.
Por lo tanto, aunque el procesador recibe el fragmento de página, dentro del procesador, el fragmento aún se ejecuta solo . Es decir que ambos enfoques están en juego. No estoy seguro de si puede restringir su uso de OO a solo uno o solo al otro. Ambos pueden ser necesarios, incluso si se enmascaran uno por el otro.
Respuestas:
Esto depende completamente de lo que creas que es OO .
Para OOP = SOLID, la operación debe ser parte de la clase si es parte de la responsabilidad única de la clase.
Para OO = despacho virtual / polimorfismo, la operación debe ser parte del objeto si se despacha dinámicamente, es decir, si se llama a través de una interfaz.
Para OO = encapsulación, la operación debería ser parte de la clase si usa un estado interno que no desea exponer.
Para OO = "Me gustan las interfaces fluidas", la pregunta es qué variante se lee más naturalmente.
Para OO = modelar entidades del mundo real, ¿qué entidad del mundo real realiza esta operación?
Todos esos puntos de vista suelen estar equivocados de forma aislada. Pero a veces una o más de estas perspectivas son útiles para llegar a una decisión de diseño.
Por ejemplo, utilizando el punto de vista de polimorfismo: Si tiene diferentes estrategias de representación (como diferentes formatos de salida, o diferentes motores de renderizado), entonces
$renderer->render($page)
tiene mucho sentido. Pero si tiene diferentes tipos de página que deberían mostrarse de manera diferente,$page->render()
podría ser mejor. Si el resultado depende tanto del tipo de página como de la estrategia de representación, puede realizar un envío doble a través del patrón de visitante.No olvide que en muchos idiomas, las funciones no tienen que ser métodos. Una función simple como
render($page)
si a menudo una solución perfectamente fina (y maravillosamente simple).fuente
$renderer
iba a decidir cómo renderizar. Cuando las$page
conversaciones con$renderer
todo lo que dice es qué hacer. No como. El$page
no tiene idea de cómo. Eso me mete en problemas SRP?Según Alan Kay , los objetos son organismos autosuficientes, "adultos" y responsables. Los adultos hacen cosas, no son operados. Es decir, la transacción financiera es responsable de salvarse a sí misma , la página es responsable de representarse a sí misma , etc., etc. Más concisamente, la encapsulación es lo más importante en OOP. En particular, se manifiesta a través del famoso principio Tell not ask (que a @CandiedOrange le gusta mencionar todo el tiempo :)) y la reprobación pública de captadores y setters .
En la práctica, da como resultado objetos que poseen todos los recursos necesarios para hacer su trabajo, como instalaciones de bases de datos, instalaciones de representación, etc.
Entonces, considerando su ejemplo, mi versión OOP se vería así:
En caso de que esté interesado, David West habla sobre los principios originales de OOP en su libro, Object Thinking .
fuente
Aquí tenemos que
page
ser completamente responsables de renderizarnos. Puede haber sido suministrado con un renderizado a través de un constructor, o puede tener esa funcionalidad incorporada.Ignoraré el primer caso (suministrado con un renderizado a través de un constructor) aquí, ya que es bastante similar a pasarlo como parámetro. En cambio, veré los pros y los contras de la funcionalidad incorporada.
La ventaja es que permite un nivel muy alto de encapsulación. La página no necesita revelar nada sobre su estado interno directamente. Solo lo expone a través de una representación de sí mismo.
La desventaja es que rompe el principio de responsabilidad única (SRP). Tenemos una clase que es responsable de encapsular el estado de una página y también está codificada con reglas sobre cómo representarse a sí mismo y, por lo tanto, es probable que una gran variedad de otras responsabilidades como objetos deberían "hacerse cosas a sí mismos, no hacer que otros les hagan cosas a ellos". ".
Aquí, todavía estamos requiriendo una página para poder representarse a sí misma, pero la estamos suministrando con un objeto auxiliar que puede hacer la representación real. Aquí pueden surgir dos escenarios:
Aquí, hemos respetado completamente el SRP. El objeto de la página es responsable de mantener la información en una página y el renderizador es responsable de representar esa página. Sin embargo, ahora hemos debilitado por completo la encapsulación del objeto de la página, ya que necesita hacer público todo su estado.
Además, hemos creado un nuevo problema: el renderizador ahora está estrechamente acoplado a la clase de página. ¿Qué sucede cuando queremos representar algo diferente en una página?
¿Cuál es el mejor? Ninguno de ellos. Todos tienen sus defectos.
fuente
La respuesta a esta pregunta es inequívoca. Es
$renderer->renderPage($page);
cuál es la implementación correcta. Para comprender cómo llegamos a esta conclusión, necesitamos comprender la encapsulación.¿Qué es una página? Es una representación de una pantalla que alguien consumirá. Ese "alguien" podría ser humano o bots. Tenga en cuenta que
Page
es una representación, y no la pantalla en sí. ¿Existe una representación sin ser representado? ¿Es una página algo sin renderizador? La respuesta es Sí, una representación puede existir sin ser representada. Representar es una etapa posterior.¿Qué es un renderizador sin una página? ¿Puede un renderizador renderizar sin una página? No. Entonces, una interfaz Renderer necesita el
renderPage($page);
método.¿Qué tiene de malo
$page->renderMe($renderer);
?Es el hecho que
renderMe($renderer)
todavía tendrá que llamar internamente$renderer->renderPage($page);
. Esto viola la Ley de Demeter que estableceA la
Page
clase no le importa si existe un aRenderer
en el universo. Solo le importa ser una representación de una página. Por lo tanto, la clase o la interfazRenderer
nunca deben mencionarse dentro de aPage
.RESPUESTA ACTUALIZADA
Si recibí su pregunta correcta, la
PageSnippet
clase solo debería preocuparse por ser un fragmento de página.PdfRenderer
se preocupa por el renderizado.Uso del cliente
Par de puntos a considerar:
$data
como una matriz asociativa. Debería ser una instancia de una clase.html
propiedad de la$data
matriz es un detalle específico de su dominio, yPageSnippet
está al tanto de estos detalles.fuente
printOn:aStream
método, pero todo lo que hace es decirle a la secuencia que imprima el objeto. La analogía con su respuesta es que no hay ninguna razón por la que no pueda tener una página que se pueda representar en un procesador y un procesador que pueda representar una página, con una implementación y una selección de interfaces convenientes.Page
no ser consciente de $ renderer es imposible. Agregué un código a mi pregunta, vea laPageSnippet
clase. Es efectivamente una página, pero no puede existir sin hacer algún tipo de referencia a la$pdf
, que de hecho es un procesador de PDF de terceros en este caso. Sin embargo, supongo que podría crear unaPageSnippet
clase que solo contenga una serie de instrucciones de texto en el PDF, y que otra clase interprete esas instrucciones. De esa manera puedo evitar inyectar$pdf
enPageSnippet
, a expensas de una complejidad adicionalIdealmente, desea la menor cantidad posible de dependencias entre clases, ya que reduce la complejidad. Una clase solo debería tener una dependencia de otra clase si realmente la necesita.
Su estado
Page
contiene "un conjunto de instrucciones para un procesador de página". Me imagino algo como esto:Así sería
$page->renderMe($renderer)
, ya que la página necesita una referencia al renderizador.Pero, alternativamente, las instrucciones de representación también podrían expresarse como una estructura de datos en lugar de llamadas directas, por ejemplo.
En este caso, el Renderizador real obtendría esta estructura de datos de la Página y la procesaría ejecutando las instrucciones de renderización correspondientes. Con este enfoque, las dependencias se revertirían: la Página no necesita saber sobre el Renderer, pero al Renderer se le debe proporcionar una Página que luego pueda representar. Entonces la opción dos:
$renderer->renderPage($page);
Entonces, ¿cuál es el mejor? El primer enfoque es probablemente el más sencillo de implementar, mientras que el segundo es mucho más flexible y potente, así que supongo que depende de sus requisitos.
Si no puede decidir, o cree que podría cambiar el enfoque en el futuro, puede ocultar la decisión detrás de una capa de indirección, una función:
El único enfoque que no recomendaré es
$page->renderMe()
que sugiere que una página solo puede tener un único procesador. Pero, ¿qué pasa si tienes unScreenRenderer
y agregas unPrintRenderer
? La misma página puede ser representada por ambos.fuente
page
es claramente una entrada para el renderizador, no una salida, a esa noción claramente no cabe.La parte D de SOLID dice
"Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones".
Entonces, entre Page y Renderer, ¿cuál es más probable que sea una abstracción estable, menos probable que cambie, posiblemente representando una interfaz? Por el contrario, ¿cuál es el "detalle"?
En mi experiencia, la abstracción suele ser el Renderer. Por ejemplo, podría ser un Stream o XML simple, muy abstracto y estable. O algún diseño bastante estándar. Es más probable que su página sea un objeto comercial personalizado, un "detalle". Y tiene otros objetos comerciales que se deben representar, como "imágenes", "informes", "gráficos", etc. (Probablemente no sea un "tríptico" como en mi comentario)
Pero obviamente depende de su diseño. La página podría ser abstracta, por ejemplo, el equivalente de una
<article>
etiqueta HTML con subpartes estándar. Y tiene muchos "renderizadores" de informes comerciales personalizados diferentes. En ese caso, el Renderer debería depender de la página.fuente
Creo que la mayoría de las clases se pueden dividir en cualquiera de las dos categorías:
Estas son clases que casi no tienen dependencias de nada más. Normalmente son parte de tu dominio. No deben contener lógica o solo lógica que pueda derivarse directamente de su estado. Una clase Employee puede tener una función
isAdult
que puede derivarse directamente de ella,birthDate
pero no una funciónhasBirthDay
que requiera información externa (la fecha actual).Estos tipos de clases operan en otras clases que contienen datos. Por lo general, se configuran una vez e inmutables (por lo que siempre realizan el mismo tipo de función). Sin embargo, este tipo de clases aún puede proporcionar una instancia auxiliar de corta duración con estado para realizar operaciones más complejas que requieren mantener cierto estado durante un período corto (como las clases de Constructor).
Su ejemplo
En su ejemplo,
Page
sería una clase que contiene datos. Debería tener funciones para obtener estos datos y quizás modificarlos si se supone que la clase es mutable. Mantenlo tonto, para que pueda usarse sin muchas dependencias.Datos, o en este caso,
Page
podría representarse de muchas maneras. Podría representarse como una página web, escribirse en el disco, almacenarse en una base de datos, convertirse a JSON, lo que sea. No desea agregar métodos a dicha clase para cada uno de estos casos (y crear dependencias en todo tipo de otras clases, aunque se supone que su clase solo contiene datos).Su
Renderer
es una clase de tipo de servicio típico. Puede operar en un determinado conjunto de datos y devolver un resultado. No tiene mucho estado propio, y el estado que tiene generalmente es inmutable, se puede configurar una vez y volver a usar.Por ejemplo, podría tener ay
MobileRenderer
aStandardRenderer
, ambas implementaciones de laRenderer
clase pero con configuraciones diferentes.Entonces, como
Page
contiene datos y debe mantenerse tonto, la solución más limpia en este caso sería pasar elPage
aRenderer
:fuente