依赖注入与事件编程

13-03-22 banq
依赖注入或者称反转Ioc,通过第三方框架将你需要依赖的类主动注入进来,依赖注入随着Spring和JavaEE6普及,已经成为大家习惯的一种默认处理类关系的方法。

我将依赖注入和事件编程进行联系比较,是源于某天我突然发现,这两者实际是处理依赖关系的不同方式而已。打个比喻,某个工厂缺少某个部件,通过采购快递将部件送到厂里,这是依赖注射;而有的工厂则相反,委托别的工厂生产好部件后,不拉进自己厂里,而是将自己产品拉出去和那个部件进行组装。

是不是有些绕人,以代码来说明:

public class A{
    B b;

   public void a1(){
      b.xxx();

   }

  public A(B b){
     this.b = b;
  }

}


public class B{

   public void xxx(){
      System.println("hello");
   }


}
<p>

A的方法a1()中需要依赖B的xxx()方法,我们就用Ioc框架将B实例通过A的构造器或者Setter方法注入,注意,其实我们没有发现这里有一个很严重的设计问题,依赖是方法行为的依赖,因为方法有依赖,造成两个整类发生依赖,是不是有点影响面扩大呢?

如果说,通过构造器注入可以保证这两个类的对象生命周期一致,那么通过setter方法进行注入,更容易产生两个类的对象生命周期不一致。而我们很多人适应了Spring的setter方法注入,竟然熟视无睹如此丑陋危险的做法,这也是构造器注入要强于setter方法注入的地方(jdonframework只提供构造器注入的原因),当然,这也只是50步笑百步,从根本上讲,我们不能因为两个类的方法有依赖,就将整个类发生关系,这实际是一种结构偏执思维。

那么如何解决这个问题?从行为模式入手,通过事件模型来解决这个问题。

我们知道事件模型类似观察者模式,有事件的产生者和事件的处理者两个角色,这两个角色通过事件产生了一种联系,实际就是一种依赖。

我们用Guava的事件编程改造上述案例:

public class A{
   EventBus bus;

   public void a1(){
      bus.post("xxx");
   }

  public A(EventBus bus){
     this.bus = bus;
  }
}

public class B{

   @ subscribe
   public void xxx(){
      System.println("hello");
   }
}

EventBus bus = new EventBus();
 bus.register(b);
<p>

事件模型的代码和依赖注入代码的区别是:原来A直接依赖B,现在我们更改为A依赖事件总线对象,注意,这个事件总线其实类似Ioc容器是全局的。换句话说,A对B的直接依赖加入了第三者:

原来:

A --> B

A ----> 第三方 ----> B

这种处理松耦合的方式非常类似我们引入接口,也非常类似facade模式 代理模式等等。

更重要的是,我们避免了因为A的一个方法对B的依赖,而将整个B注入的粗糙做法,泼水泼掉孩子的傻事情不能再做了。

从以上看出,事件编程是对传统依赖注入模式的补充,它们的主要区别是依赖方向的不同:

依赖注入是有点facade模式,大包大揽,以自我为中心,其他人都要来配合我,都注入到我的边界内部,最后我的边界内混乱不堪不说,又会制造新的紧耦合。

事件编程的则从我内部发出事件即可,不要把污七八糟什么人都领到家里,做什么事情完全可以到外面去实施,在家里打个电话指示一下就可以。

本文主要通过实践说明处理依赖关系的两种不同角度,特别是我们进行面向对象编程时,要有很强烈的对象生命周期概念,当我们绑定两个对象时,就如同绑定两个人,这两个人有各自自己的生命轨迹,他们可以在一起共同生活多长时间?如果这些Scope时间边界问题不考虑,那么还不如全部使用静态全局变量编程,避免徒增烦恼。

[该贴被banq于2013-03-22 17:25修改过]

[该贴被admin于2013-03-22 18:39修改过]

20
tangxuehua
2013-03-22 17:41
2013-03-22 17:13 "@banq

"的内容

A ----> 第三方 ----> B ...

这里应该是这样吧:

A ----> 第三方 <---- B

第三方 <---- B:这里你的第三方就是eventbus,那这个箭头表示b订阅eventbus publish出来的事件,本质上就是b依赖于eventbus。而eventbus可以注册任意多个订阅者,所以它其实不依赖于任何一个订阅者。

就像依赖注入的原理一样。

本来a依赖于b,现在改为只依赖一个接口,而b实现了该接口。

a----->接口<-----b

a依赖接口,b实现接口。

[该贴被tangxuehua于2013-03-22 17:46修改过]

[该贴被tangxuehua于2013-03-22 17:47修改过]

masterice
2013-03-23 14:58
好文,前几天就有同事问过我这样的问题。我也觉的不太好,可是最终还是选择了让其注入。原因只有一个,既然用了spring就不想用new。归一还是不归一始终是矛盾的。

yqj2065
2013-03-24 18:31
原本“依赖注入和事件编程”是毫无关系的两个术语,但是一个IoC搞得大家云山雾绕。

例子中A依赖于B,你可以用任意的方式使A依赖于AB而AB依赖于B,与事件模型无关。

例如:

public class A{

AB ab;

public void a1(){ab.xxx();} //AB的xxx()调用B的xxx()即可。

}

依赖注入“框架”spring本身就是和事件编程一样,采用“回调”或Ioc作为基本技术;但是,如果仅仅注入依赖——“A依赖于B”,可以简单地使用一个工具类——使用反射机制+配置文件的静态工厂。

MartinChen
2013-03-25 09:41
最近也做个项目,恰好也涉及到这个,说说我的心得吧

--------以下单为例------------

比如说下单后,需要存数据库,通知仓库备货,检查用户信用。

对于这个三个动作,它们需要由【用户已下单的事件】 来触发。

而完成每一个动作需要的资源,可以用依赖注入的方式来获得。

jdon007
2013-03-26 00:35
object.propety = value; /* 属性赋值,对象具体的值 */

object.method(){ /*方法实现,对象具体的行为逻辑*/}

依赖注入,使得对象的属性,可以动态绑定值,或者说延迟对值的绑定;事件机制,使得对象的方法,可以动态绑定行为逻辑,或者说延迟对行为逻辑的绑定

如果没有依赖注入和事件机制,对象属性的值和方法的行为逻辑在编译期就被确定了;但有时候,对象属性的值和方法的行为逻辑,依赖于运行时的上下文,就不得不考虑使用依赖注入和事件机制了。

一般来说,运行时的值或行为逻辑的绑定,更适合以“声明式”(注解、配置)的形式进行描述,但这只是“表象”,以“过程式”(在代码中直接应用设计模式)的形式来描述也没啥问题,只是罗嗦了点。

更值得运用的判断原则是:对象的值、对象的行为逻辑是否依赖于运行时的上下文,如果是,大胆用之;如果不是,慎重,代码还是朴素、直接点好。

下面再仔细说明一下。

一、值的绑定

1)不依赖于运行时的上下文,请直接用 A a = new A(); 不要滥用创建型模式,也不要滥用注解、配置的方式,那是在干没有价值的事;徒增复杂度,却无收益。

2)依赖于运行时的上下文,使用工厂模式或注解/配置的方式,其中注解的方式最为简洁,

建议使用声明式写法:

@Wire

A a; // 比如A是一个接口,其实现方式可能在运行时决定

二、行为逻辑的绑定

1)不依赖于运行时的上下文,请求直接在方法体内写上你的处理逻辑,不要滥用行为型模式

也不要滥用注解、配置的方式,那同样是干没有价值的事。

2)依赖于运行时的上下文,使用行为型模式或注解/配置的方式,同样其中注解的方式最为简洁,也建议使用该写法。

命令式写法,运用了观察者模式。

A a;

a.addEvenListener("click", new EventListener() {

void onEvent() {

doSomething();

}

});

声明式写法,运用了注解、反射、动态代理等技术,如果框架不支持,或自己没时间搞,

用上面命令式的写法,也挺直观的,就是“水泥”多了点(但性能却会略好一点)。

A a;

@Listen(eventSource="a", event="click")

void doSomething();

-------------------------------------------------------------

@banq 斑竹,你好!好久没有来逛JDON了!

忙到没有时间沉淀,真希望偶尔也能有时间能过来和道友讨论问题。

-------------------------------------------------------------

banq
2013-03-26 08:35
2013-03-25 09:41 "@MartinChen

"的内容

这个三个动作,它们需要由【用户已下单的事件】 来触发。

而完成每一个动作需要的资源,可以用依赖注入的方式来获得。 ...

这个方式比较好,通过依赖注入实现资源整合,通过事件实现依赖解耦,各自发挥其特点。这和我在Jdon框架的编程是一样的,不但有设计好处,也有性能扩展性好处,这三个动作可以异步或者并行并发运行。

@jdon007 思辨能力比较强,总结得很好,相当于从名词和动词两个角度分别论述。

[该贴被banq于2013-03-26 12:11修改过]

freeren
2013-03-28 10:38
呵呵...受教了!

先不论jdon007的以场景来决定是否应用的观点(本来还是很支持这个观点的,但是在现实的分析中,由于人员能力的参差不齐,要做到这么细分析还是有困难的)

OK,话多说了!

从bank的分析,让人耳目一新!我们以前的编程,关注的是如何把所谓的需求实现,当系统上线前的集成测试或者UAT,我们会发现离需求又有距离了,于是乎大家又开始了新一轮的修改,这种修改带来的往往是又改变了原先的逻辑了,这样重复一些错误。

而Bank老师的观点,无疑从更高的角度去看系统,这样做法可以说各司其职,在很大程度上减少了依赖,对于后续的修改带来的风险也是相应降低了!谢谢!

当然,这里同样有个疑问,这样的做法,我们对于事件的抽象是否要求比较高呢?实际场景是丰富的!

jdon007
2013-03-28 20:03
2013-03-28 10:38 "@freeren

"的内容

要做到这么细分析还是有困难的 ...

呵呵,实现起来困难时因为方向搞错了或者没有在实质上突破既有的思路的困境。

gameboyLV 《今天终于在一个大型项目中运用了DDD》 ,这无疑是一个朝着正确方向的实现方式,我相信以这种架构开发会舒服很多。

gameboyLv的实现方式与我的方式有一部分非常像,但gameboyLV还不够彻底,实现上既然都有这么大的突破,他应该也抛弃DDD那些术语,仅需保留了DDD(领域驱动设计)这个概念本身。我将也在jdon写个帖子,作为最近两年实践的部分经验的沉淀。届时欢迎讨论。

至于依赖注入和事件机制的讨论,我个人认为更接近语言特性的讨论,事实上动态函数式语言,事件机制和依赖注入实现起来非常非常容易,因为语言本身就具备这种能力。如果要上升到架构层次来讨论,也可以,但两者只能说是架构的局部属性。

想起了,Alan Kay, OOP发明者的一段话:对我而言,面向对象程序设计只意味着消息发送(messaging),状态处理的局部保存、保护和隐藏(local retention and protection and hiding of state-process),还有一切东西的极端迟绑定(extreme late-binding of all things)。

freeren
2013-03-29 09:22
2013-03-26 00:35 "@jdon007

"的内容

呵呵,实现起来困难时因为方向搞错了或者没有在实质上突破既有的思路的困境。 ...

并不是要争论什么!我想是我没完全说明白。

就拿我目前的公司来说吧,开发或者设计人员其实对业务的掌握并不完全彻底,而分析和设计的思维还是传统的先设计表再开发的模式。这种思维方式,的确很难让设计人员跳出来,站到更高的层面去思考问题。其实这也是Jdon一直坚持的方向,就是用最原始的思维去面对我们的业务需求。往往有了数据库思想的绑架,就会在一开始结合考虑太多技术的因素了,这样分析出来的需求往往不够纯。

当然表达这么多,咱并不是要说明什么,只是想描述的是目前还有很多公司或者人员的思维决定着我所谓的困难!

不过,还是很谢谢jdon007的回复,有讨论才能进步,呵呵

chennengfu
2013-03-29 10:42
我觉得这两者有本质的区别。从设计上看,依赖注入相当于工厂,是创建型模式;而事件机制则相当于命令,是行为型模式。

brighthas
2013-03-31 19:03
事件比依赖注入的面更宽,事件很自然,依赖注入只是技术手段。

banq
2013-04-12 17:16
DDD中如果有两个聚合根调用,如何解决?如果还是使用依赖注入,就会发生聚合根嵌套的可笑事情发生。

以代码为例子:有两个聚合根类AggregateRoot1 AggregateRoot2,AggregateRoot1的方法依赖AggregateRoot2实现,如果采取依赖注入,也就是Spring等框架做法如下:

class AggregateRoot1{
    AggregateRoot2 aggregateRoot2;

     public void dosth(){
       ....        
        aggregateRoot2.dosth();
      ...
    }
}
<p>

AggregateRoot1的dosth方法依赖AggregateRoot2的dosth方法,不自觉就将AggregateRoot2使用依赖注入,但是这样就造成了AggregateRoot1聚合AggregateRoot2的现象,AggregateRoot2还是聚合根吗?

什么是聚合?

class A{
   private B b;
   public B getB(){
      return b;
    }
}
<p>

这段代码表达了A聚合了B,也就是说整体A包含了部分B,A和B关系是父与子,整体与部分的关系。

我们使用这种聚合关系表达A是B的父对象,对于DDD聚合,如果A不再被其他对象引用,类似二叉树的一个根节点,那么我们认为A就是聚合根。

但是,因为依赖注入,我们破坏了业务上的聚合根概念。

那么两个聚合根之间相互调用怎么办?

只有通过事件。

banq
2013-05-05 09:36
2013-03-22 17:13 "@banq

"的内容

将依赖注入和事件编程进行联系比较,是源于某天我突然发现,这两者实际是处理依赖关系的不同方式而已 ...

依赖注射用于抽象与实现之间依赖,所谓抽象是指目标做什么,而实现是指怎么达到目标,怎么做。

领域模型表达的是整个业务系统的目标,也就是做什么问题,是各种微观抽象的大聚合。

领域模型(聚合根实体)
   |
   |
做什么1   做什么2  做什么3 
  |              |              |
  |              |              |
怎么做1     怎么做2  怎么做3
<p>

再详细分析:做什么属于一种职责目标,比如总裁有职责目标,经理有职责目标。那么我们就从领域模型的角色职责入手。

领域模型的行为设计:

http://www.jdon.com/45347

迪米特法则(Law of Demeter)与领域模型行为:

http://www.jdon.com/45378

[该贴被banq于2013-05-05 14:29修改过]

JdonEye
2013-05-06 04:32
分析的很到位。

猜你喜欢