我正在尝试遵循Demeter法则(请参阅http://en.wikipedia.org/wiki/Law_of_Demeter,http://misko.hevery.com/code-reviewers-guide/flaw-digging-into-collaborators/),因为我可以看到它的好处,但是当涉及到域对象时,我会变得有点卡住。 / p>
域对象自然有链,有时需要显示整个链的信息。
例如,购物篮:
每个订单都包含用户,递送信息和项目列表 每个订单商品都包含产品和数量 每个产品都有名称和价格。 每个用户都包含一个名称和地址
显示订单信息的代码必须使用有关订单,用户和产品的所有信息。
通过订单对象获取此信息当然更好,更可重复使用,例如“order.user.address.city”比上面列出的所有对象的查询更高,然后将它们分别传递给代码?
欢迎提出任何意见/建议/提示!
答案 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。因此,您将拥有OrderView
,AddressView
等类,知道如何为各自的模型创建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