wow got
#2 楼 @jasl
不同意这个说法。‘’在模型里声明模型间关联就代表这些类有耦合了,增加 Service 层改变不了这点。但是如果一个业务涉及两个不耦合的模型或者涉及到外部系统,可以增加 Service 层,这样两个模型和 Service 单向依赖,结构更美观点“。原因是透过 ActiveModel 看本质,通过 model 关联还是不通过 model 关联的类,还是通过 http 接口关联的服务,从设计上来看没有任何本质区别,而 ls 提出不增加 Service 类的设计违背了单一职责原则。(写过基础服务的亲们这点体会应该会更深,因为基础服务打成库给业务开发用的时候也就是只剩下一个 rpc 类的方法名了,其实 activemodel 就是把数据库服务打包了阿,只是这些 rpc 类设计的都很优秀很稳定,因为我们一般没机会去处理相关的错误,感觉不到这点。)
案例分析如下: 首先,明确一个事情,两个 class 之间耦合的含义是”两个 class 之间通过双方约定的接口耦合(ruby 没 interface 呐就是使用 public 方法了),两个 class 的维护者保持接口语义的不变的前提即可任意维护自己 class 的实现。“这是面向对象的基本思想。
然后,我们如果跳出 ActiveModel 来看,也不套用设计模式,我们只考虑一般的类设计。出现一个操作需要同时更新两个现在还无关联的 class 的时候,一般有两个选择: 选择一,将类 O,P 的实例 o,p 作为另一个类 S 的成员,由类 S 提供一个方法 m,s.m 调用 p.mp,o.mo,m 保证关联调用的一致性等,作为接口供调用,o.mo,p.mp 只负责读写对象 o,p 内的数据,三个类各司其职,维护对外承诺的 m,mo.mp 三个接口的语义。 选择二,根据情况如果这两个类中会有一个起主导作用的,比如对订单操作的类 P,应该有信息需要从订单类 O 获得,这种情况类 P 因为总是是订单流程中的附属操作而不是反过来,所以就选择让订单类多一个成员 p,由订单类提供对外的接口 m 调用自身的方法 o.mo,和成员 p 的方法 p.mp。也很好,少了一个类,而且还可能让 O 中的方法少了几个入参。
然后我们再看,对应的 ActiveModel 来说,就是 belongs_to,has_many 提供了语法糖来定义类的成员关系,也就是 ls 说的“他们已经耦合了”,那么自然看到 ls 是第二种选择。到此,从类设计的角度,我们可以很容易看到,选择二的问题在于:
1.变化发生,逻辑复杂了一点,由两者耦合变为三者耦合时候,比如又有了需要在工单处理过程中,运营人员类 U 需要对操作类 P 做操作的时候,对第一个选择依然是在 s.m 中添加 u.mu 处理三者的关系,保持接口不变。对选择二,如果保持接口不变,则要依然认为 p 属于 o,在 o.m 里添加 u.um;那么既然是三者关系,认为 p 应该属于 u,改变接口为 u.m 里调用 p.mp,o.mo 是否也可以呢;还是 P 本身变得重要了,需要 p.m 作为接口调用 u.um,o.om。那么我们看到当一个设计让修改发生时的选择太多时候,这个设计本身就很可能有问题。
2.继续分析,可能亲们已经有异议了,此时依照选择二背后的思路更好的选择是 P 作为接口 p.m 调用 o.om,u.um,顺着人家 ls 的思路,这确实也是一个很好的解决方法阿?而问题正是在此出现,这个更好的设计为何在一开始功能逻辑简单为“只有订单操作需要记录”的时候很容易理解为 options 是订单的附属,应该由订单来提供接口呢?这正是这个设计思路的根本问题所在,这个想在多者关系中总选择一个主导者有其来提供接口的思路本身是难以应对变化的思路,不是一个好思路。这也正是经典的原则和思路能被后人一而再的承认的原因。因为设计模式、设计原则思路也好,都是前人在编写大规模超大规模程序的时候,在超出人脑处理复杂度之外而总结的,当面对一两千行代码时候那真的是咋写都行真没啥大区别,而教这些理念的书籍又不可能贴上十万行代码然后告诉你“二货,打开 vim,脑子一片空白了吧,得靠好好设计了吧。”,那么在有限的篇幅里举出的猪猫狗这些 class 做的例子很容易被觉得是多此一举,可有可无的。而有更多语法糖的语言、越来越多成熟的库、更面向具体领域的框架、也一直在飞速降低同样功能的程序的代码行数,更多优秀架构师在产生的同时同样的人力资源的价格越来越贵,因此越是架构师在更高的层面解耦系统解耦代码库后再给其他人分工,设计的好坏越来越难有更多的人体会。
闲话扯太多,不增加一个 service 不好的本质原因是,这种设计违背了单一职责原则,这个出自敏捷始书里的经典面向对象原则。通过增加类 S,我们界定清楚了 S 的职责是且仅仅是组合 o,p,u;O,P,U 的职责也是且仅仅是维护对象自己。而不增加,无论如何取舍,这份逻辑找不到好的归处。
至于在开发过程中只要有跨 model 的写就必须有 service,是严谨的设计对开发效率的折中,读的接口跨 model 也应该抽象,但不做简单的读接口的抽象的危害没有那么大,而当写操作出现的时候意味着会影响到其他读接口的语义能否在增加这个写操作之后依然正常工作,错误在这时候会加速扩散,这个时候就必须增加一个 service 类来进行更谨慎的设计防止错误扩散难以解决。另一个原因是,这个原则如此容易理解和执行和检查,KISS。
呼唤大牛