En los lenguajes orientados a objetos, ¿cuándo deberían los objetos realizar operaciones sobre sí mismos y cuándo deberían realizarse operaciones sobre los objetos?

11

Supongamos que hay una Pageclase, que representa un conjunto de instrucciones para un procesador de páginas. Y supongamos que hay una Rendererclase 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 $datapara que funcione.

Pero, puedo crear una PdfRendererclase 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í $rendererrecibe el PageSnippety todo lo $datanecesario 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.

Dennis
fuente
2
Desafortunadamente, te has metido en el mundo del software "guerras religiosas" aquí, en la línea de si usar espacios o pestañas, qué estilo de llaves usar, etc. No hay "mejor" aquí, solo opiniones fuertes de ambos lados. Haga una búsqueda en Internet de los beneficios y desventajas de los modelos de dominio rico y anémico y forme su propia opinión.
David Arno
77
@DavidArno ¡Usa espacios paganos! :)
candied_orange
1
Ja, en serio no entiendo este sitio a veces. Las preguntas perfectamente buenas que obtienen buenas respuestas se cierran en poco tiempo por estar basadas en opiniones. Sin embargo, una pregunta obviamente basada en la opinión como esta aparece y esos sospechosos habituales no se encuentran por ningún lado. Oh, bueno, si no puedes vencerlos y todo eso ... :)
David Arno
@Erik Eidt, ¿podría recuperar su respuesta, por favor, ya que creo que es una muy buena respuesta "cuarta opción".
David Arno
1
Además de los principios SOLID, puede echar un vistazo a GRASP , especialmente en la parte de expertos . La pregunta es ¿cuál tiene la información para que usted cumpla con la responsabilidad?
Onésimo Sin

Respuestas:

13

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).

amon
fuente
Er espera un minuto. Todavía puedo obtener renderizado polimórfico si la página contiene una referencia al renderizador pero no tiene idea de qué renderizador tiene. Simplemente significa que el polimorfismo está un poco más abajo en la madriguera del conejo. También puedo elegir qué pasarle al renderizador. No tengo que pasar toda la página.
candied_orange
@CandiedOrange Ese es un buen punto, pero quisiera reservar su argumento bajo el SRP: sería la responsabilidad de Capital-R de la página decidir cómo se representa, tal vez utilizando algún tipo de estrategia de representación polimórfica.
amon
Pensé que $rendereriba a decidir cómo renderizar. Cuando las $pageconversaciones con $renderertodo lo que dice es qué hacer. No como. El $pageno tiene idea de cómo. Eso me mete en problemas SRP?
candied_orange
Realmente no creo que estemos en desacuerdo. Intenté ordenar su primer comentario en el marco conceptual de esta respuesta, pero es posible que haya usado palabras torpes. Me recuerdas una cosa que no mencioné en la respuesta: el flujo de datos de decir-no-preguntar también es una buena heurística.
amon
Mmm, ok. Tienes razón. Lo que he estado hablando seguiría a tell-don't-ask. Ahora corrígeme si me equivoco. La otra estrategia, donde el renderizador toma una referencia de página, significa que el renderizador tendría que darse la vuelta y pedirle cosas a la página, usando los captadores de páginas.
candied_orange
2

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í:

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

En caso de que esté interesado, David West habla sobre los principios originales de OOP en su libro, Object Thinking .

Zapadlo
fuente
1
Para decirlo sin rodeos, ¿a quién le importa lo que alguien dijo sobre algo relacionado con el desarrollo de software, hace 15 años, excepto por interés histórico?
David Arno
1
" Me importa lo que un hombre que inventó el concepto orientado a objetos dijo acerca de qué objeto es " . ¿Por qué? Más allá de adormecerlo con el uso de falacias de "apelación a la autoridad" en sus argumentos, ¿qué posible influencia podrían tener los pensamientos del inventor de un término sobre la aplicación del término 15 años después?
David Arno
2
@Zapadlo: No presenta un argumento de por qué el mensaje es de Página a Renderer y no al revés. Ambos son objeto y, por lo tanto, adultos, ¿verdad?
JacquesB
1
"La falacia de apelación a la autoridad no se puede aplicar aquí " ... " Entonces, el conjunto de conceptos que en su opinión representa POO, en realidad es incorrecto [porque es una distorsión de la definición original] ". Supongo que no sabes lo que es una apelación a la falacia de la autoridad. Pista: usaste uno aquí. :)
David Arno
1
@David Arno Entonces, ¿están mal todas las apelaciones a la autoridad? ¿Prefieres "Apelar a mi opinión"? Cada vez que alguien cita un tío Bobism, va a quejarse de apelación a la autoridad Zapadio proporciona una fuente muy respetada Puede estar en desacuerdo o conflicto citar fuentes, pero repeatefly quejándose de que alguien le había proporcionado una citación no es constructivo?..
user949300
2

$page->renderMe();

Aquí tenemos que pageser 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". ".

$page->renderMe($renderer);

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:

  1. La página simplemente necesita conocer las reglas de representación (qué métodos llamar en qué orden) para crear esa representación. La encapsulación se conserva, pero el SRP todavía está roto ya que la página todavía tiene que supervisar el proceso de representación, o
  2. La página solo llama a un método en el objeto de representación, pasando sus detalles. Nos estamos acercando a respetar el SRP, pero ahora hemos debilitado la encapsulación.

$renderer->renderPage($page);

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.

David Arno
fuente
No estoy de acuerdo con que V3 respete SRP. El procesador tiene al menos 2 razones para cambiar: si la página cambia, o si cambia la forma en que la representa. Y un tercero, que cubre, si Renderer necesita renderizar objetos que no sean Páginas. De lo contrario, buen análisis.
user949300
2

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 Pagees 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 establece

Cada unidad solo debe tener un conocimiento limitado sobre otras unidades

A la Pageclase no le importa si existe un a Rendereren el universo. Solo le importa ser una representación de una página. Por lo tanto, la clase o la interfaz Renderernunca deben mencionarse dentro de a Page.


RESPUESTA ACTUALIZADA

Si recibí su pregunta correcta, la PageSnippetclase solo debería preocuparse por ser un fragmento de página.

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

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

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

PdfRenderer se preocupa por el renderizado.

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

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

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

Uso del cliente

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

Par de puntos a considerar:

  • Es una mala práctica pasar $datacomo una matriz asociativa. Debería ser una instancia de una clase.
  • El hecho de que el formato de página esté contenido dentro de la htmlpropiedad de la $datamatriz es un detalle específico de su dominio, y PageSnippetestá al tanto de estos detalles.
Juzer Ali
fuente
Pero, ¿qué pasa si, además de Pages, tienes imágenes, artículos y Triptichs? En su esquema, un Renderer tendría que saber sobre todos ellos. Eso es mucha fuga. Solo comida para pensar.
user949300
@ user949300: Bueno, si el Renderer necesita poder renderizar imágenes, etc., obviamente, necesita saber sobre ellas.
JacquesB
1
Smalltalk Best Practice Patterns de Kent Beck presenta el patrón del Método de inversión , en el cual ambos son compatibles. El artículo vinculado muestra que un objeto admite un printOn:aStreammé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.
Graham Lee
2
En cualquier caso, tendrá que romper / falsificar SRP, pero si Renderer necesita saber cómo representar muchas cosas diferentes, eso es realmente "mucha responsabilidad" y, si es posible, evitarlo.
user949300
1
Me gusta su respuesta, pero estoy tentado a pensar que Pageno ser consciente de $ renderer es imposible. Agregué un código a mi pregunta, vea la PageSnippetclase. 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 una PageSnippetclase 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 $pdfen PageSnippet, a expensas de una complejidad adicional
Dennis
1

Idealmente, 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 Pagecontiene "un conjunto de instrucciones para un procesador de página". Me imagino algo como esto:

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

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.

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

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:

renderPage($page, $renderer)

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 un ScreenRenderery agregas un PrintRenderer? La misma página puede ser representada por ambos.

JacquesB
fuente
En el contexto de EPUB o HTML, el concepto de página no existe sin un renderizador.
Mouviciel
1
@ Mouviciel: No estoy seguro de entender lo que quieres decir. ¿Seguramente puedes tener una página HTML sin renderizarla? Por ejemplo, el rastreador de Google procesa páginas sin representarlas.
JacquesB
2
Hay una noción diferente de la página de palabras: el resultado de un proceso de paginación cuando una página HTML formateada para imprimirse, tal vez eso es lo que @mouviciel tenía en mente. Sin embargo, en esta pregunta, a pagees claramente una entrada para el renderizador, no una salida, a esa noción claramente no cabe.
Doc Brown
1

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.

user949300
fuente
0

Creo que la mayoría de las clases se pueden dividir en cualquiera de las dos categorías:

  • Clases que contienen datos (mutable o inmutable no importa)

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 isAdultque puede derivarse directamente de ella, birthDatepero no una función hasBirthDayque requiera información externa (la fecha actual).

  • Clases que brindan servicios

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, Pageserí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, Pagepodrí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 Rendereres 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 MobileRenderera StandardRenderer, ambas implementaciones de la Rendererclase pero con configuraciones diferentes.

Entonces, como Pagecontiene datos y debe mantenerse tonto, la solución más limpia en este caso sería pasar el Pagea Renderer:

$renderer->renderPage($page)
john16384
fuente
2
Muy lógica de procedimiento.
user949300