10

我正在尝试遵循得墨忒耳法则(参见http://en.wikipedia.org/wiki/Law_of_Demeter, http: //misko.hevery.com/code-reviewers-guide/flaw-digging-into-collaborators/)我可以看到好处,但是在涉及域对象时我变得有点卡住了。

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

例如,一个购物篮:

每个订单包含一个用户、交货信息和一个项目列表每个订单项目包含一个产品和数量每个产品都有一个名称和价格。每个用户都包含一个姓名和地址

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

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

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

4

5 回答 5

10

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

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

考虑一个例子:假设您想替换UserOrderPlacingParty一个抽象,它封装了可以下订单的用户、公司和电子代理。这种重构立即提出了多个问题:

  • 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();
}
于 2012-12-03T18:03:17.187 回答
2

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

但是,如果您使用这种链接来对访问的属性执行某些逻辑,情况就会有所不同。例如,如果你有,

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

这是有问题的。因为,这个逻辑是/应该在更接近目标属性——城市的模块中执行。就像,在首先构造 Address 对象的地方,或者如果不是这样,至少在构造 User 对象时(如果说 User 是实体并且 address 是值类型)。但是,如果它走得更远,它走得越远,它就会变得越不合逻辑和成问题。因为源和目标之间涉及的中介太多了。

因此,根据得墨忒耳法则,如果你在一个类中的“城市”属性上执行一些逻辑,比如OrderAssmebler,它访问像 order.user.address.city 这样的链中的城市属性,那么你应该考虑将此逻辑移动到更接近目标的位置/模块。

于 2012-12-01T17:04:39.297 回答
1

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

class Order {
    User user;
}

class User {
    Address shippingAddress;
    Address deliveryAddress;
}

class Address {
    String city;
    ...
}

当您开始考虑如何将这些数据保存到数据库(例如ORM)时,您是否开始考虑性能。考虑急切与延迟加载的权衡。

于 2012-09-05T15:09:54.143 回答
1

一般来说,我遵守得墨忒耳法则,因为它有助于将更改保持在缩小的范围内,这样新需求或错误修复就不会遍布您的系统。还有其他设计指南可以帮助实现这个方向,例如本文中列出的指南。话虽如此,我认为得墨忒耳法则(以及设计模式和其他类似的东西)是有用的设计指南,它们有权衡取舍,如果你认为这样做是可以的,你可以打破它们。例如我一般不测试私有方法,主要是因为它会创建脆弱的测试. 但是,在一些非常特殊的情况下,我确实测试了一个对象私有方法,因为我认为它在我的应用程序中非常重要,因为我知道如果对象的实现发生更改,该特定测试将受到更改。当然,在这些情况下,您必须格外小心,并为其他开发人员留下更多文档来解释您这样做的原因。但是,最后,您必须使用您的良好判断力:)。

现在,回到最初的问题。据我了解,您的问题是为一个对象编写(网络?)GUI,该对象是可以通过消息链访问的对象图的根。对于这种情况,我会以与您创建模型类似的方式模块化 GUI,为模型的每个对象分配一个视图组件。因此,您将拥有诸如OrderViewAddressView等知道如何为其各自模型创建 HTML 的类。然后,您可以组合这些视图来创建您的最终布局,或者通过将责任委托给它们(例如OrderView创建AddressView)或通过使用调解器负责组合它们并将它们链接到您的模型。作为第一种方法的一个例子,你可以有这样的东西(我将使用 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()消息,那么您将不得不更改应用程序中的一个点,避免该更改遍布您的系统。

高温高压

于 2012-11-28T20:25:46.083 回答
0

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

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

于 2012-12-03T15:25:47.227 回答