对象间相互调用时互相控制的几种方法

面向对象的编程鼓励编程者把功能分散到多个对象中,从而使得每个对象只有唯一的功能(SRP),对象足够的高内聚,这样的代码更容易理解,维护,修改和复用;同时,对象必须互相调用才能共同完成复杂的操作,但如果对象间相互调用太多,又会导致对象间高耦合,使得对一处代码的修改影响到多处看似不相关的代码(散弹效应),为实现高内聚,对象间应该做到少依赖;如果依赖,则依赖于必须的最小接口(ISP)。

本文通过一个小例子,尝试分析下对象间调用时调用者和被调用者互相控制的几种方式。

今天早上坐(哦,应该是站)地铁,边站边想对象间的调用方式,正好想到了北京地铁最近要提价了。于是想到一个根据上车站点和下车站点显示票价的例子。具体而言,用户在屏幕上选择上车站,下车站和乘车策略(站点最少和时间最短),然后屏幕上会显示票价。

实现上,类PriceDisplayer负责显示票价,其display方法接受start(表示上车站),end(下车站)和strategy(乘车策略),然后经过计算,会在屏幕上显示票价;又有一个类,StationManager,其方法getStationDistance同样接受start,end,strategy三个参数,返回两个站点之间的最短站数,最快站数和换乘最少站数。PriceDisplayer.display会调用StationManager.getStationDistance获取站点数,然后根据一种计算方法(例如起始2块,满5站加1块)计算出票价,最后调用一个显示的api来显式票价。在其最外层,则有TicketMachine负责创建PriceDisplayer并调用其display。
这里展示了最常见的对象间控制(调用者到被调用用着)和反控制(被调用者到调用者)的方法。调用者(PriceDisplayer)通过参数来控制被调用者(StationManager),被调用者通过返回值(或者传出参数)来反控调用者。在依赖上,只有调用者依赖于被调用者,而被调用者并不知晓调用者。被调用者不会因调用者的改变而改变(因为没有依赖),其创建非常简单(因为不需要创建其依赖类),复用性也是非常强的。相对而言,调用者依赖于一个具体类,会因为被调用者接口和实现的改变而改变。但如果需要对象间合作,这种单向依赖是必须的。

然后我们想增加一种新的乘车策略,换乘最少。此时一种实现是在StationManager中通过if-else增加对该策略的判断,然后strategy参数也增加一个枚举类型。这时StationManager的修改,可能会导致PriceDisplay的已有行为被改变,例如在增加新策略时,不小心修改了原有两种策略的行为(违反OCP)。此时一种选择是使用strategy模式,即抽取出一个接口,StationManagerInterface,其getStationDistance接受start和end两个参数。然后有三种实现分别针对三种策略:MinStationCountStationManager,MinTimeStationManager和MinTransferStationManager。可以创建PriceDisplayer时使用不同的实现来初始化,这样子PriceDisplayer只依赖于接口StationManagerInterface而不依赖于具体实现。此时如果增加新的策略,PriceDisplayer代码不需要任何变化,也完全不会影响原有的三种策略。
这里展示了调用者通过调用同一接口的不同实现来改变行为的方法。就编译时的静态代码而言,调用者只依赖于一个接口,而不似之前依赖于具体类。只要运行时真实的被调用者实现了该接口,就可以被调用。仍然是单向依赖,只不过现在依赖于接口而非实现。此时被调用者的复用性更强(其各个实现一般不需要修改,增加新的功能只需要增加新的实现,所以使用者更加放心),而调用者(PriceDisplayer)因为可以使用不用的具体被调用者实现,也可以更灵活的被该调用者的调用者(TicketMachine)使用,所以复用性也得到一定加强,但不同于前一种方法,此时调用者的调用者(TicketMachine)必须初始化调用者(PriceDisplayer)和被调用者(StationManagerInterface),所以又使得调用者的调用者(TicketMachine)更加复杂。

现在假设StationManager的计算站点距离算法非常费时,需要放在一台服务器上计算,所以PriceDisplay的现实操作只能够异步完成。此时一种实现方法是传入一个Callback接口的实现,Callback接口中有execute方法,供StationManager计算出站点距离时回调。
这里展示的是通过传入可执行的方法(Js,python中可以直接传入方法,java,c++只能传入带有实现方法的对象),控制被调用者。虽然也是通过参数控制,在第一种方法中,参数只是静态数据,而此时,参数是操作。传输操作不仅限于回调,设计模式中的Command模式也是传递作为命令的操作。通过传入操作,调用者可以改变被调用者的流程中的某些步骤。对于我们的例子,只是改变了最后一步的callback;但它也可以改变流程中的任何一步。例如我可以在参数中不传入start和end,而传入包括getStart和getEnd方法的接口传入。再例如前面第二种方法中TicketMachine将具体的StationManager设置到PriceDisplayer中(通过构造函数或setter),也是一种传入可执行对象的例子.
这种方式仍然是单向依赖。PriceDisplayer依赖于StationManager,StationManager依赖于Callback。虽然Callback是在PriceDisplay中定义的(或者是PriceDisplay实现了Callback),但StationManager并不知道。
这种方式既可以正向控制(例如command模式),调用者给被调用者传递命令;也可以反向控制,例如本例中的被调用者回调改变调用者.

同样是上面的场景,另外一种选择是PriceDisplay把自己传给StationManager(而不是Callback的实现)。此时它可以直接回调PriceDisplay的所有方法。此时则是双向依赖。调用者和被调用者紧紧的耦合在一起了。我们可以通过抽取更小的接口(方法3)或者直接把值给作为参数传过去和返回值传回来(方法1)来尽量避免这种过紧的依赖。
因为是互相调用,同样可以正向控制和反向控制.

原文地址:https://www.cnblogs.com/xichengtie/p/3542551.html