三个智者告诉,不要问

Three Wise Men on Tell, Don't Ask

wisemen.jpg

约翰,杜伊和马库斯是三个好朋友。 他们分别在IBM,Cognizant和TCS拥有20多年的Java / Java EE堆栈工作经验。 他们在设计模式和所有新技术方面均具有丰富的经验,并因其对技术堆栈的出色洞察力而受到同事的尊重。

他们计划在即将到来的周末度假,并希望在业余时间里品尝许多汉堡,威士忌和美食。 约翰在附近的村庄有一所房子,所以他们计划开车去那儿。

终于,这一天到了。 他们收拾好啤酒,威士忌和汉堡,直奔约翰家。 他们晚上到达那里准备了一些小吃,然后坐在圆桌旁享用食物和饮料。

突然,电源中断了。

房间太黑了,没人看得到。 从外面,他们可以听到the的声音,而马库斯(Marcus)打开手电筒可以看到对方。

Doe打破沉默,说:"嗯,这种氛围非常适合恐怖故事。任何人都可以分享现实生活中的任何经历吗?"

马库斯干脆地回答:"嗯,我们都是同一镇上的人,忙着工作。没有恐怖的故事,但是我可以告诉你一个有关Java的故事,直到今天。"

约翰和Doe的建筑师的本能大为鼓舞。

问题

" 作为一名架构师,当我设计问题的解决方案时,封装总是让我感到恐惧。我们在客户端程序中暴露了什么部分?

John和Doe点了点头,Marcus继续。

"有许多OOP原则说明如何明智地封装外部的类或API。"

然后,他进入了一个示例,即"不告诉,不问"原则。 它说要始终指示对象做什么。 切勿向他们查询内部状态并基于此做出决策。 您无法控制自己的对象。

让我们看一个简单的例子。 假设我要编写一个包裹递送服务,并且有两个域对象-包裹和客户。 因此,我们应该如何设计-

如果我编写以下代码片段...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.basic;

public class PercelDeliveryService {

    public void deliverPercel(Long customerId){

        Customer cust = customerDao.findById(customerId);
        List<Percel> percelList = percelDao.findByCustomerId(customerId);
        for(Percel percel : percelList){
            System.out.println("Delivering percel to" + cust.getCustomerAddress());
            //do all the stuff related to delivery
        }
    }
}

根据告诉"不要问"的说法,这是违规的行为,应避免使用。 在ParcelDeliveryService中,我尝试获取或查询CustomerAddress,以便执行交付操作。 因此,在这里,我查询客户的内部状态。

那为什么危险?

稍后再说,传递功能发生了变化。 它说现在还包括电子邮件地址或手机号码,所以我必须公开这些详细信息。 现在,我将越来越多地公开内部状态。 考虑其他服务。 他们也可能使用相同的电子邮件地址或客户地址。 现在,如果我想将"客户地址"返回类型(字符串)更改为一个"地址"对象,则需要更改已使用它的所有服务。

这是一项巨大的任务,会增加破坏功能的风险。 另一点是,随着内部状态暴露给更多服务,总是存在使用服务污染内部状态的风险,并且很难检测到哪个服务更改了状态。

简而言之,我无法控制对象,因为我不知道哪个服务使用/修改了对象的内部状态。 现在,如果我的对象由整体应用程序的内部服务使用,则可以在IDE中搜索用法并对其进行重构。 但是,如果对象通过API公开,并且该API被其他组织使用,那么我就是历史。 这确实损害了我们公司的声誉,作为一名建筑师,我将被解雇。

所以,我可以说:

动作:暴露更多内部状态

结果:增加耦合,增加风险并提高刚性

现在,再来看一下我在讲故事时提供的解决方案...

Doe轻笑着猜测Marcusis试图提出的观点。 他打断他,说:"所以,马库斯,您想看看我们是否遵循"不告诉,不问原则"。有两种方法可以重构问题。"

解决方案1:关联

在客户和包裹之间建立关联。 它将被延迟加载,并且delivery方法应该位于Customer对象中,因此,从服务中,我们要求交付。 然后,交付方法获取包裹清单并将其交付。 如果内部返回类型从String更改为Address,则仅应影响该方法。

像这样:

1
2
3
4
5
6
7
package com.example.basic;
public class ParcelDeliveryService {
    public void deliverParcel(Long customerId){
        Customer cust = customerDao.findById(customerId);
        cust.deliver();
    }
}

1
2
3
4
5
6
7
8
9
Public class Customer{
    public void deliver(){
        List<Percel> percelList = getPercelList();
      for(Percel percel : percelList){
            System.out.println("Delivering percel to" + this.getCustomerAddress());
            //do all the stuff for delivery
        }
    }
}

通过这样做,我们可以保持"告诉,不要问得恰当",并降低暴露内部状态的风险。 另外,我们可以自由地修改类的属性,因为所有行为都在本地与属性绑定在一起,这是实现封装的好方法。

解决方案2:命令对象

我们可以创建一个命令对象,将包裹详细信息和命令对象传递给客户模型。 交付方法提取包裹清单并将其交付给相应的客户。

但是,这两项政策都违反了另一个原则-单一责任原则。 SRP说,班级只有一个改变的理由。

但是,如果我以相反的方式思考,为什么我们要编写服务? 因为每个服务都执行一项工作,例如"人员交付"服务负责与包裹相关的交付操作,所以它维护SRP,并且仅当包裹交付机制发生任何更改时,此服务才会更改。 如果它中断了,除非其他服务依赖它,否则其他服务将不会受到影响。

但是根据"告诉,不要问"的原则,所有与客户相关的行为都应移至"客户"类中,以便我们可以告诉/指示/命令"客户"类来完成一项任务。 因此,现在,由于所有与客户相关的服务代码都已纳入客户模型,因此客户类承担了更多责任。 因此,客户有更多更改的理由,从而增加了失败的风险因素。

现在我们回到了同样的问题:风险因素。

如果要公开内部状态,则在修改属性时,如果将所有行为都移到一个类中,则修改功能破坏系统的风险会增加。

因此,在这种情况下,SRP和"告诉,不要问"是矛盾的。

John点了点头,说道:"是的,这是个问题。不仅如此,如果我们要在服务中实现缓存,又该如何实现聚合功能(例如,取决于距离或 帐户类型。要查找发送到某个地方的最多包裹,我们使用了聚合服务,其中要求内部状态并计算结果。"

考虑到这一点,我们经常打破"不告诉,不问"的原则。 即使按照当前的趋势,Model对象也应该是轻量级的,并且不应该相互耦合。 如果业务需要分布在多个模型上的信息,我们可以编写一个聚合器服务并查询每个模型并计算结果,因此我们正在查询内部状态。 考虑一下Spring Data。

现在,如果我们从另一个角度来看,根据领域驱动设计,在包裹交付上下文(有界上下文)中,客户负责交付包裹,

绝对不。

在这种情况下,送货员负责将包裹交付给客户。 为此,送货员需要包裹和客户模型,在这种情况下,仅需要客户的姓名和地址详细信息。 对于包裹ID,将需要包裹名称。 因此,根据DDD,我们创建一个汇总模型DeliveryBoy,其中有两个漂亮的模型:Customer和Parcel。

在这种情况下,我们不需要客户。 在上下文方面,该模型已更改,因此对于客户而言,没有一个大模型可以包含所有属性和行为。 相反,我们使用了两个基于有限上下文的小模型,以及一个查询这些模型以执行工作的聚合模型。

通过这样做,我们可以混合并匹配SRP和"告诉,不要问"。 从服务的角度来看,我们仅告诉/命令DeliveryBoy聚合模型执行某项操作,因此该服务同时维护SRP和"告诉,不要问"。 同时,我们的集合模型还维护SRP,查询Customer和Parcel模型以进行操作。

喜欢:

1
2
3
4
5
6
7
8
package com.example.basic;
public class ParcelDeliveryService {
    public void deliverParcel(Long customerId){

        DeliveryBoy boy = new DeliveryBoy();
        boy.deliver(customerId);
    }
}

1
2
3
4
5
6
7
8
9
10
class DeliveryBoy {
    Customer cust;// Context driven model
    Percel percel;
    public void deliver(Long id){
        //do stuff
//load customer slick model
//load Percel slick model
//Deliver the same by quering those models
   }  
}

Marcus表示,在DDD和聚合器服务的背景下,微服务通常会破坏SRP。 Doe表示:"因此,没有灵丹妙药,并非所有原则在所有情况下都是好的。根据上下文,您必须判断遵循的原则。有时,您需要妥协。"

对于特定的原则,可能是代码很糟糕,但是对于给定的上下文,它是最佳的。

原则是通用的,并且不受上下文限制。 但是现实生活中的解决方案是基于上下文的,因此适合的原理基于上下文,而不是相反。

同时,灯亮了! 瞬间,所有三个人都开始品尝威士忌和食物。

结论

作为叙述者,我对所有观众的问题是,您如何看待他们的演讲? 他们在设计时还有什么需要注意的地方-