得墨忒耳定律 - 数据对象

时间:2012-09-05 14:44:55

标签: oop theory law-of-demeter

我正在尝试遵循Demeter法则(请参阅http://en.wikipedia.org/wiki/Law_of_Demeterhttp://misko.hevery.com/code-reviewers-guide/flaw-digging-into-collaborators/),因为我可以看到它的好处,但是当涉及到域对象时,我会变得有点卡住。 / p>

域对象自然有链,有时需要显示整个链的信息。

例如,购物篮:

每个订单都包含用户,递送信息和项目列表 每个订单商品都包含产品和数量 每个产品都有名称和价格。 每个用户都包含一个名称和地址

显示订单信息的代码必须使用有关订单,用户和产品的所有信息。

通过订单对象获取此信息当然更好,更可重复使用,例如“order.user.address.city”比上面列出的所有对象的查询更高,然后将它们分别传递给代码?

欢迎提出任何意见/建议/提示!

5 个答案:

答案 0 :(得分:9)

使用链式引用的一个问题,例如order.user.address.city,是高阶依赖关系被“烘焙”到类外的代码结构中。

理想情况下,在重构类时,“强制更改”应限于重构类的方法。当您在客户端代码中有多个链式引用时,重构会驱使您在代码的其他位置进行更改。

考虑一个示例:假设您要将User替换为OrderPlacingParty,这是一种封装可以下订单的用户,公司和电子代理的抽象。这种重构立即出现了多个问题:

  • User属性将被调用为其他属性,并且它将具有不同的类型
  • 如果电子代理商下订单,新属性可能没有address city
  • 与订单相关联的人User(假设您的系统因法律原因需要一个)可能间接与订单相关,例如,通过成为定义中的指定定位人员OrderPlacingParty

解决这些问题的方法是将订单表示逻辑直接传递给它所需要的一切,而不是让它“理解”传入的对象的结构。这样你就能够对代码的更改进行本地化被重构,而不将更改传播到其他可能稳定的代码。

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

答案 1 :(得分:2)

我认为,当链接用于访问某些属性时,它会在两种(或至少两种)不同的情况下完成。例如,您在演示模块中提到的情况就是一个订单对象,您只想显示所有者/用户的地址或城市等详细信息。在这种情况下,如果你这样做,我认为没有多大问题。为什么?因为您没有在被访问的属性上执行任何业务逻辑,这可能(可能)导致紧密耦合。

但是,如果为了在被访问属性上执行某些逻辑而使用这种链接,则情况会有所不同。例如,如果你有,

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

这是有问题的。因为,这个逻辑更适合在更接近目标属性 - 城市的模块中执行。比如,在一开始构造Address对象的地方,或者如果没有那样,至少在构造User对象时(如果说User是实体并且对值类型进行寻址)。但是,如果距离越远,它走得越远,它变得越不合逻辑和有问题。因为源和目标之间涉及太多中介。

因此,根据德米特定律,如果您在类中的“城市”属性上执行某些逻辑,请说 OrderAssmebler ,它会按顺序访问链中的城市属性。 user.address.city,那么您应该考虑将此逻辑移动到更接近目标的位置/模块。

答案 2 :(得分:1)

你是对的,你很可能会像这样建立你的价值对象

class Order {
    User user;
}

class User {
    Address shippingAddress;
    Address deliveryAddress;
}

class Address {
    String city;
    ...
}

当您开始考虑如何将此数据保存到数据库(例如ORM)时,您是否开始考虑性能。认为渴望与懒惰加载权衡。

答案 3 :(得分:1)

一般来说,我遵守Demeter法则,因为它有助于在更小的范围内保持更改,因此新的要求或错误修复不会遍布整个系统。还有其他设计指南可以帮助实现这个目标,例如: this article中列出的那些。话虽如此,我认为Demeter法则(以及设计模式和其他类似的东西)作为有用的设计指南,有它们的权衡,如果你判断它是可以的,你可以打破它们。例如,我通常不test private methods,主要是因为它创建了fragile tests。但是,在一些非常特殊的情况下,我确实测试了一个对象私有方法,因为我认为它在我的应用程序中非常重要,因为我知道如果对象的实现发生了变化,那个特定的测试将会发生变化。当然,在这些情况下,您必须格外小心,并为其他开发人员留下更多文档,说明您为什么这样做。但是,最后,你必须运用你的良好判断力:)。

现在,回到最初的问题。据我所知,你的问题是为一个对象编写(web?)GUI,该对象是可以通过消息链访问的对象图的根。对于这种情况,我将通过为模型的每个对象分配视图组件,以与创建模型类似的方式模块化GUI。因此,您将拥有OrderViewAddressView等类,知道如何为各自的模型创建HTML。然后,您可以编写这些视图以创建最终布局,方法是将责任委托给他们(例如OrderView创建AddressView)或者让Mediator负责撰写它们并将它们链接到您的模型。作为第一种方法的一个例子,你可以有这样的东西(我将使用PHP作为例子,我不知道你使用的是哪种语言):

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(){...}
}

然后是观点:

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

在你的view.php中你会做类似的事情:

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

此方法基于组件模型,其中视图被视为可组合组件。在此模式中,您尊重对象的边界,每个视图都有一个责任。

编辑(根据OP评论添加)

我将假设无法在子视图中组织视图,并且您需要在一行中呈现购物篮ID,订单日期和用户名。正如我在评论中所说,对于这种情况,我会确保在一个记录良好的地方执行“坏”访问,让视图不知道这一点。

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

如果稍后重新安排您的域名模型而您的ShoppingBasket课程无法再实施getUser()消息,则您必须更改应用中的单个点,避免将此更改全部传播在你的系统上。

HTH

答案 4 :(得分:0)

Demeter法则是调用方法,而不是访问属性/字段。我知道技术上属性是方法,但从逻辑上讲它们就是数据。所以,order.user.address.city的例子对我来说似乎很好。

本文有趣的是进一步阅读:http://haacked.com/archive/2009/07/13/law-of-demeter-dot-counting.aspx