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

12

Supongamos que hay una clase Page , que representa un conjunto de instrucciones para un procesador de páginas. Y suponga que hay una clase Renderer 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?

BACKGROUND

Para agregar un poco más de fondo, me encuentro usando ambos enfoques en el mismo código. Estoy utilizando una biblioteca de PDF de terceros llamada TCPDF . En algún lugar de mi código, tengo para que funcione la representación de PDF:

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

Diga que deseo crear una representación de la página. Podría crear una plantilla que contenga instrucciones para renderizar un fragmento de página PDF de la siguiente 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 $snippet se ejecuta solo , como en mi primer ejemplo de código. También necesita saber y estar familiarizado con el $pdf , y con cualquier $data para que funcione.

Pero, puedo crear una clase PdfRenderer 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 cualquier $data requerido para que funcione. Esto es similar a mi segundo ejemplo de código.

Entonces, aunque el renderizador recibe el fragmento de página, dentro del renderizador, el fragmento se ejecuta solo . Es decir que ambos enfoques están en juego. No estoy seguro de si puede restringir su uso de OO solo a uno o solo al otro. Es posible que ambos sean necesarios, incluso si enmascara uno por otro.

    
pregunta Dennis 05.02.2018 - 22:33

7 respuestas

14

Esto depende completamente de lo que crees 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 = envío virtual / polimorfismo, la operación debe ser parte del objeto si debe enviarse dinámicamente, es decir, si se llama a través de una interfaz.

Para OO = encapsulación, la operación debe ser parte de la clase si utiliza 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, usando el punto de vista del polimorfismo: si tiene diferentes estrategias de representación (como diferentes formatos de salida o diferentes motores de representación), $renderer->render($page) tiene mucho sentido. Pero si tiene diferentes tipos de páginas que deberían representarse de manera diferente, $page->render() podría ser mejor. Si la salida 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 es una solución perfectamente fina (y maravillosamente simple).

    
respondido por el amon 05.02.2018 - 22:50
2

Según Alan Kay , los objetos son autosuficientes, "adultos" y responsables organismos Los adultos hacen cosas, no son operados. Es decir, la transacción financiera es responsable de guardándose solo , la página es responsable de renderizado , etc., etc. Más concisamente, la encapsulación es la gran cosa en OOP. En particular, se manifiesta a través del famoso principio de no pedirle (que a @CandiedOrange le gusta mencionar todo el tiempo :)) y la reprobación pública de getters and setters .

En la práctica, resulta en 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 como la siguiente:

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 la POO en su libro, Pensamiento de objetos .

    
respondido por el Zapadlo 06.02.2018 - 11:39
2
  

$page->renderMe();

Aquí tenemos page siendo completamente responsable de la representación. Puede haberse suministrado con un render a través de un constructor, o puede tener esa funcionalidad incorporada.

Aquí ignoraré el primer caso (suministrado con un render a través de un constructor), ya que es bastante similar a pasarlo como parámetro. En su lugar, analizaré los pros y los contras de la funcionalidad incorporada.

El pro es que permite un nivel muy alto de encapsulación. La página no necesita revelar nada acerca de su estado interior directamente. Sólo lo expone a través de una representación de sí mismo.

La contra es que rompe el principio de responsabilidad única (SRP). Tenemos una clase que se encarga de encapsular el estado de una página y también está codificada de manera rígida con reglas sobre cómo rendirse y, por lo tanto, es probable que haya una gran variedad de otras responsabilidades, ya que los objetos deberían "hacerse cosas a sí mismos, no hacer que otros los hagan. ".

  

$page->renderMe($renderer);

Aquí, todavía requerimos una página para poder representarse a sí misma, pero le estamos proporcionando un objeto auxiliar que puede hacer la representación real. Dos escenarios pueden surgir aquí:

  1. La página simplemente necesita conocer las reglas de representación (a 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 aún tiene que supervisar el proceso de representación, o
  2. La página solo llama a un método en el objeto renderizador, pasando sus detalles. Estamos cada vez más cerca de 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 completamente 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 renderizar algo diferente a una página?

¿Cuál es mejor? Ninguno de ellos. Todos tienen sus defectos.

    
respondido por el David Arno 06.02.2018 - 12:55
2

La respuesta a esta pregunta es inequívoca. Es $renderer->renderPage($page); que es la implementación correcta. Para comprender cómo llegamos a esta conclusión, debemos 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 el Page es una representación y no la visualización en sí. ¿Existe una representación sin ser representada? ¿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 página? ¿Puede un renderizador renderizar sin página? No. Entonces, una interfaz de Renderer necesita el método renderPage($page); .

¿Qué pasa con $page->renderMe($renderer); ?

Es el hecho de que renderMe($renderer) todavía tendrá que llamar internamente a $renderer->renderPage($page); . Esto viola la Ley de Demeter que declara

  

Cada unidad debe tener un conocimiento limitado sobre otras unidades

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

RESPUESTA ACTUALIZADA

Si su pregunta es correcta, la clase PageSnippet 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 la representación.

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

Un par de puntos a considerar:

  • Es una mala práctica pasar alrededor de $data como 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 propiedad html de la matriz $data es un detalle específico de su dominio, y PageSnippet conoce estos detalles.
respondido por el Juzer Ali 06.02.2018 - 09:04
1

Idealmente, usted desea la menor cantidad de dependencias entre clases como sea posible, ya que reduce la complejidad. Una clase solo debería tener una dependencia de otra clase si realmente la necesita.

Usted declara que Page contiene "un conjunto de instrucciones para un procesador de páginas". Me imagino algo como esto:

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

Entonces, sería $page->renderMe($renderer) , ya que la página necesita una referencia al renderizador.

Pero, alternativamente, las instrucciones de representación también se pueden expresar 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 Representador real obtendría esta estructura de datos de la Página y la procesaría ejecutando las instrucciones de representación correspondientes. Con un enfoque de este tipo, las dependencias se revertirían: la Página no necesita saber acerca del Representador, pero se debe proporcionar al Representante una Página que luego se pueda representar. Así que la opción dos: $renderer->renderPage($page);

Entonces, ¿cuál es mejor? El primer enfoque es probablemente el más sencillo de implementar, mientras que el segundo es mucho más flexible y poderoso, por lo que supongo que depende de sus requisitos.

Si no puede decidir, o cree que podría cambiar de enfoque en el futuro, puede ocultar la decisión detrás de una capa de direccionamiento indirecto, una función:

renderPage($page, $renderer)

El único enfoque que no recomendaré es $page->renderMe() ya que sugiere que una página puede tener solo un renderizador. ¿Pero qué sucede si tiene un ScreenRenderer y agrega un PrintRenderer ? La misma página puede ser representada por ambos.

    
respondido por el JacquesB 06.02.2018 - 10:59
1

La D parte de SOLID dice

"Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones".

Entonces, entre Página y Renderizador, ¿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 simple Stream o XML, 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 de negocio que se van a representar, como "imágenes", "informes", "gráficos", etc. ... (Probablemente no sea un "tríptico" como en mi comentario)

Pero obviamente depende de tu diseño. La página podría ser abstracta, por ejemplo, el equivalente de una etiqueta HTML <article> con subpartes estándar. Y tienes muchos "renderizadores" de informes empresariales personalizados. En ese caso, el Renderizador debe depender de la página.

    
respondido por el user949300 07.02.2018 - 03:23
0

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

  • Las clases que contienen datos (mutables o inmutables no importa)

Estas son clases que casi no tienen dependencias en nada más. Normalmente son parte de su dominio. No deben contener ninguna lógica o solo una lógica que pueda derivarse directamente de su estado. Una clase de empleado puede tener una función isAdult , ya que puede derivarse directamente de su birthDate , pero no una función hasBirthDay , ya que requiere información externa (la fecha actual).

  • Clases que proporcionan servicios

Estos tipos de clases operan en otras clases que contienen datos. Por lo general, se configuran una sola vez y son inmutables (de modo que siempre realizan el mismo tipo de función). Sin embargo, este tipo de clases aún puede proporcionar una instancia de ayuda de corta duración con estado para realizar operaciones más complejas que requieren mantener algún estado durante un período corto (como las clases de Builder).

Tu 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. Manténgalo tonto, para que pueda usarse sin muchas dependencias.

Datos, o en este caso su Page podría representarse de muchas maneras. Podría representarse como una página web, escribirse en un 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 clases, aunque se supone que su clase solo contiene datos).

Su Renderer es una clase de tipo de servicio típica. Puede operar en un determinado conjunto de datos y devolver un resultado. No tiene mucho estado propio, y el estado que tiene es generalmente inmutable, se puede configurar una vez y luego reutilizarlo.

Por ejemplo, podría tener MobileRenderer y StandardRenderer , ambas implementaciones de la clase Renderer pero con diferentes configuraciones.

Por lo tanto, como Page contiene datos y se debe mantener sin sentido, la solución más limpia en este caso sería pasar Page a Renderer :

$renderer->renderPage($page)
    
respondido por el john16384 06.02.2018 - 15:05

Lea otras preguntas en las etiquetas