2012-09-05 17 views
8

Estoy tratando de seguir la Ley de Demeter (ver http://en.wikipedia.org/wiki/Law_of_Demeter, http://misko.hevery.com/code-reviewers-guide/flaw-digging-into-collaborators/) ya que puedo ver los beneficios, sin embargo me he quedado un poco atascado cuando se trata de objetos de dominio.Ley de Demeter - Objetos de datos

Los objetos de dominio tienen naturalmente una cadena y, a veces, es necesario mostrar la información sobre toda la cadena.

Por ejemplo, una cesta:

Cada pedido contiene un usuario, la información de entrega y una lista de elementos Cada posición de pedido contiene un producto y la cantidad Cada producto tiene un nombre y precio. Cada usuario contiene un nombre y una dirección

El código que muestra la información de la orden tiene que usar toda la información sobre la orden, los usuarios y los productos.

Seguramente es mejor y más reutilizable obtener esta información a través del objeto de la orden, p. "order.user.address.city" que para algún código más arriba para hacer consultas para todos los objetos que enumeré anteriormente, y luego pasarlos al código por separado.

¡Cualquier comentario/sugerencia/sugerencia es bienvenida!

+0

¿Cuál es tu pregunta? – hakre

+1

Sé que no hay una pregunta específica aquí ... Coloqué la recompensa porque * podría haber * y el tema vale la pena discutirlo. Tal vez el OP podría aclarar la pregunta un poco? – rdlowrey

+0

@Tom su problema es que no le gusta inyectar un usuario de clase, información de clase y una matriz de clases de productos en el pedido de clase? son bastante independientes aunque –

Respuesta

7

Un problema con el uso de referencias encadenados, como order.user.address.city, es que las dependencias de orden superior se obtiene "al horno en" la estructura de código fuera de la clase.

Idealmente, en los casos en los que refactorice su clase, sus "cambios forzados" deberían limitarse a los métodos de la clase que se está refactorizando. Cuando tiene varias referencias encadenadas en el código del cliente, la refactorización lo lleva a realizar cambios en otros lugares de su código.

Considere un ejemplo: suponga que desea reemplazar User con OrderPlacingParty, una abstracción que encapsula a los usuarios, las empresas y los agentes electrónicos que pueden realizar un pedido. Esta refactorización presenta inmediatamente múltiples problemas:

  • La propiedad User será llamado otra cosa, y que tendrá un tipo diferente
  • La nueva propiedad no puede tener un address que tiene city en los casos en que se realiza el pedido por un agente electrónico
  • El humano User asociado con el pedido (supongamos que su sistema necesita uno por razones legales) puede estar relacionado con el pedido de forma indirecta, por ejemplo, al ser una persona designada en la definición del OrderPlacingParty.

Una solución a estos problemas sería pasar la lógica de presentación de pedidos a todo lo que necesita directamente, en lugar de tener que "entender" la estructura de los objetos pasados. De esta forma, usted podría localizar los cambios. al código que se está refactorizando, sin distribuir los cambios a otro código que es potencialmente estable.

interface OrderPresenter { 
    void present(Order order, User user, Address address); 
} 
interface Address { 
    ... 
} 
class PhysicalAddress implements Address { 
    public String getStreetNumber(); 
    public String getCity(); 
    public String getState(); 
    public String getCountry(); 
} 
class ElectronicAddress implements Address { 
    public URL getUrl(); 
} 
interface OrderPlacingParty { 
    Address getAddress(); 
} 
interface Order { 
    OrderPlacingParty getParty(); 
} 
class User implements OrderPlacingParty { 
} 
class Company implements OrderPlacingParty { 
    public User getResponsibleUser(); 
} 
class ElectronicAgent implements OrderPlacingParty { 
    public User getResponsibleUser(); 
} 
+0

¡Gracias! Este es el problema que estaba tratando de explicar en mis comentarios en la respuesta de TarkaDaal, pero lo expresaste mucho más elocuentemente de lo que pude. ¡Esta es ciertamente una mejor solución! Trataré de poner esto en práctica, ¡gracias por apuntarme en la dirección correcta! –

1

Usted es correcta y lo más probable es que su modelo de objetos de valor algo como esto

class Order { 
    User user; 
} 

class User { 
    Address shippingAddress; 
    Address deliveryAddress; 
} 

class Address { 
    String city; 
    ... 
} 

Cuando se inicia teniendo en cuenta cómo va a persistir estos datos a una base de datos (por ejemplo ORM) se puede empezar a pensar en actuación. Piense en las compensaciones de carga impacientes y perezosas.

+0

¡Gracias! Sí, eso es lo que pensé. El otro problema que es obvio aquí es la encapsulación. El objeto de pedido no debe conocer realmente los detalles de implementación del usuario o producto ... pero la aplicación debe tocar el suelo en alguna parte. ¿Deberían los objetos que están allí principalmente para servir como estructuras de datos tratarse de manera diferente? –

+0

Es un buen diseño acercarse a * todas * las clases (datos y funcional) de esta manera. En una charla de computadora elegante, esto significa esforzarse por tener clases con baja [Complejidad ciclomática] (http://en.wikipedia.org/wiki/Cyclomatic_complexity) y un alto nivel de [Cohesión] (http://en.wikipedia.org/wiki/Cohesion_% 28computer_science% 29) – Brad

1

En general, me atengo a la Ley de Demeter, ya que ayuda a mantener los cambios en un ámbito reducido, de modo que un nuevo requisito o una corrección de errores no se extienda por todo el sistema. Existen otras pautas de diseño que ayudan en esta dirección, p. los enumerados en this article. Habiendo dicho eso, considero la Ley de Demeter (así como los Patrones de Diseño y otras cosas similares) como pautas de diseño útiles que tienen sus compensaciones y que puede romperlas si juzga que está bien hacerlo. Por ejemplo, generalmente no test private methods, principalmente porque crea fragile tests. Sin embargo, en algunos casos muy particulares, probé un método privado de objeto porque lo consideré muy importante en mi aplicación, sabiendo que esa prueba en particular estará sujeta a cambios si la implementación del objeto cambia. Por supuesto, en esos casos debe tener mucho cuidado y dejar más documentación para que otros desarrolladores expliquen por qué lo hace. Pero, al final, tienes que usar tu buen juicio :).

Ahora, volviendo a la pregunta original. Por lo que entiendo, su problema aquí es escribir la GUI (¿web?) Para un objeto que es la raíz de un gráfico de objetos a los que se puede acceder a través de cadenas de mensajes. Para ese caso, modularizaría la GUI de forma similar a la que creó su modelo, asignando un componente de vista para cada objeto de su modelo. Como resultado, tendría clases como OrderView, AddressView, etc. que saben cómo crear el HTML para sus respectivos modelos. A continuación, puede componer esas vistas para crear su diseño final, ya sea delegando la responsabilidad a ellas (por ejemplo, OrderView crea el AddressView) o teniendo un Mediator que se encarga de componerlas y vincularlas a su modelo.Como ejemplo del primer enfoque que podría tener algo como esto (Voy a usar PHP para el ejemplo, no sé qué idioma que está utilizando):

class ShoppingBasket 
{ 
    protected $orders; 
    protected $id; 

    public function getOrders(){...} 
    public function getId(){...} 
} 

class Order 
{ 
    protected $user; 

    public function getUser(){...} 
} 

class User 
{ 
    protected $address; 

    public function getAddress(){...} 
} 

y luego los puntos de vista:

class ShoppingBasketView 
{ 
    protected $basket; 
    protected $orderViews; 

    public function __construct($basket) 
    { 
    $this->basket = $basket; 
    $this->orederViews = array(); 
    foreach ($basket->getOrders() as $order) 
    { 
     $this->orederViews[] = new OrderView($order); 
    } 
    } 

    public function render() 
    { 
    $contents = $this->renderBasketDetails(); 
    $contents .= $this->renderOrders();  
    return $contents; 
    } 

    protected function renderBasketDetails() 
    { 
    //Return the HTML representing the basket details 
    return '<H1>Shopping basket (id=' . $this->basket->getId() .')</H1>'; 
    } 

    protected function renderOrders() 
    { 
    $contents = '<div id="orders">'; 
    foreach ($this->orderViews as $orderView) 
    { 
     $contents .= orderViews->render(); 
    } 
    $contents .= '</div>'; 
    return $contents; 
    } 
} 

class OrderView 
{ 
//The same basic pattern; store your domain model object 
//and create the related sub-views 

    public function render() 
    { 
    $contents = $this->renderOrderDetails(); 
    $contents .= $this->renderSubViews(); 
    return $contents; 
    } 

    protected function renderOrderDetails() 
    { 
    //Return the HTML representing the order details 
    } 

    protected function renderOrders() 
    { 
    //Return the HTML representing the subviews by 
    //forwarding the render() message 
    } 
} 

y en su view.php que haría algo como:

$basket = //Get the basket based on the session credentials 
$view = new ShoppingBasketView($basket); 
echo $view->render(); 

Este enfoque se basa en un modelo de componentes, donde las vistas son tratados como componentes componibles. En este esquema, usted respeta los límites del objeto y cada vista tiene una responsabilidad única.

Editar (añadido basado en la observación OP)

Vamos a suponer que no hay manera de organizar las vistas en subvistas y que usted necesita para hacer la cesta de identificación, fecha de la orden y el nombre de usuario en una sola linea Como dije en el comentario, para ese caso me aseguraría de que el acceso "malo" se realice en un único lugar bien documentado, dejando la vista inconsciente de esto.

class MixedView 
{ 
    protected $basketId; 
    protected $orderDate; 
    protected $userName; 

    public function __construct($basketId, $orderDate, $userName) 
    { 
    //Set internal state 
    } 


    public function render() 
    { 
    return '<H2>' . $this->userName . "'s basket (" . $this->basketId . ")<H2> " . 
      '<p>Last order placed on: ' . $this->orderDate. '</p>'; 
    } 
} 

class ViewBuilder 
{ 
    protected $basket; 

    public function __construct($basket) 
    { 
    $this->basket = $basket; 
    } 

    public function getView() 
    { 
    $basketId = $this->basket->getID(); 
    $orderDate = $this->basket->getLastOrder()->getDate(); 
    $userName = $this->basket->getUser()->getName(); 
    return new MixedView($basketId, $orderDate, $userName); 
    } 
} 

Si más adelante que reorganizar su modelo de dominio y su clase de ShoppingBasket no puede poner en práctica el mensaje getUser() más, entonces tendrá que cambiar un solo punto en su aplicación, evitar tener que el cambio extendido por todo el sistema.

HTH

+0

Este es ciertamente un buen enfoque cuando la vista puede ser realmente distinta. Pero ¿qué pasa con algo tan trivial como "Hola $ name, tu último pedido se colocó en $ lastOrderDate" o cualquier otro caso donde los conjuntos de datos se mezclan. En este caso, necesita datos de dos conjuntos, el cliente y su último pedido. Y aunque puedo ver algunas ventajas de este enfoque, es potencialmente una gran cantidad de código adicional para un beneficio mínimo.Ciertamente parece una solución mejor, pero estoy seguro de que hay un término medio en alguna parte. –

+0

Si ese fuera el problema (un solo atributo al que se debe acceder en una cadena de objetos) lo delegaría internamente y eventualmente sería flexible con la Ley de Demeter (como dije en la respuesta). Entonces, 'User' implementará' getName() 'y' getLatestOrder() 'y en su vista usted preguntaría a la orden por su fecha. Si fuera un caso aislado en su sistema, entonces no me preocuparía demasiado. Para el caso de datos mixtos, si no ve una forma posible de factorizarlo en las subvistas, entonces debe asegurarse de romper la Ley de Demeter en un solo lugar bien documentado (voy a editar la respuesta para ejemplificar esto) –

2

Creo que, al encadenar se utiliza para acceder a alguna propiedad, que se realiza en dos (o por lo menos dos) situación diferente. Uno es el caso que ha mencionado, por ejemplo, en su módulo de presentación, tiene un objeto Orden y le gustaría simplemente mostrar la dirección del propietario/usuario, o detalles como ciudad. En ese caso, creo que no hay problema si lo haces. ¿Por qué? Debido a que no está realizando ninguna lógica comercial en la propiedad accedida, lo que (potencialmente) puede causar un acoplamiento cerrado.

Pero, las cosas son diferentes si utiliza dicho encadenamiento con el fin de realizar alguna lógica en la propiedad accedida. Por ejemplo, si tiene,

String city = order.user.address.city; 
... 
order.user.address.city = "New York"; 

Esto es problemático. Porque, esta lógica es/debería ser realizada más apropiadamente en un módulo más cercano al atributo objetivo - ciudad. Como, en un lugar donde el objeto Dirección se construye en primer lugar, o si no, al menos cuando el objeto Usuario está construido (por ejemplo, el Usuario es la entidad y la dirección del tipo de valor). Pero, si va más allá de eso, cuanto más avanza, más ilógico y problemático se vuelve. Porque hay demasiados intermediarios involucrados entre la fuente y el objetivo.

Por lo tanto, de acuerdo con la Ley de Demeter, si va a realizar alguna lógica en la "ciudad" atributo en una clase, digamos OrderAssmebler, que accede el atributo de la ciudad en una cadena como order.user.address. ciudad, entonces deberías pensar en mover esta lógica a un lugar/módulo más cercano al objetivo.

0

La Ley de Demeter se trata de métodos de llamada, no de acceso a propiedades/campos. Sé técnicamente propiedades son métodos, pero lógicamente están destinados a ser datos. Entonces, su ejemplo de order.user.address.city me parece bien.

Este artículo es interesante lectura adicional: http://haacked.com/archive/2009/07/13/law-of-demeter-dot-counting.aspx

+0

Pero , el espíritu de la ley del demeter está ahí para evitar cadenas de dependencias a través de la encapsulación. Al solo saber que la API de la encapsulación del vecino más cercano aumenta porque la dependencia puede ser reemplazada, p. con un objeto simulado en las pruebas, sin romper el código. En ese ejemplo, reemplazar al usuario con un objeto de usuario simulado haría que las cosas se rompieran o al menos requiriera una cantidad importante de simulacro de configuración para lograrlo, lo cual es algo que pasa por alto. –

+0

Es cierto que las vistas no se probarán de esa manera, pero es útil cargar un objeto simulado en una vista real con fines de prueba. Si se modificó la API del usuario, p. para tener tanto una dirección de entrega como una dirección de envío en lugar de una sola dirección, las cosas se romperían inesperadamente en lugares que realmente no incluyen una "dirección" como dependencia, lo que está lejos de ser ideal –

+0

@TomB Sí, el espíritu es sobre la ocultación de datos. Su pregunta es explícita sobre los datos * que muestran *. La vista (o lo que sea) presumiblemente va a tener etiquetas para "Usuario", "Dirección", "Ciudad", etc. Por definición, * tiene * que saber acerca de esto. No veo el problema en obtener esta información de la manera más simple posible. – TarkaDaal

Cuestiones relacionadas