桥接模式于外发设计的应用思路

一 设计模式

  今天在同事工位上看到了一本《大话设计模式》,倍感亲切。本科曾经开过这门课程,很清楚记得当时老师推荐的就是这本书。遗憾的是当时并没有特别深入的学习了解,直到后来参加了工作,才越发的重视设计模式。

  其实到目前为止,我依然不建议开发者主动去套用设计模式,因为以绝大多数开发者的架构能力,尚不足以在动手之前将模式跃然于心,更多的时候生搬硬套反而舍得其反。

  另外,我始终认为设计模式不是对代码设计的指导,它仅仅是对过往的采用面向对象设计的项目应用的一种优雅经验总结。我更建议所有的开发者以实现为主,重构为辅,在对程序的不断优化重构中,模式就渐渐的体现出来了。这样做不仅能节约人力成本、时间成本,也符合常规项目开发的节奏。

  但是我依然推荐大家多看设计模式相关的知识,一是能够扩展视野,二是能够反哺我们对代码的重构设计。这里除了推荐《大话设计模式》,还推荐一本书《重构——改善既有代码的设计》,两者相辅相成,而且都是作为一名开发者必备的技能,而而而且是硬技能,通吃各个领域的。

二 设计原则

  今天在看到《大话设计模式》之后便顺嘴问了同事一句:如果想要将设计模式应用于项目,必须准守的六大原则是什么?其实连我自己都记不大清楚了,但是这些年的工作经验告诉我即使不应用设计模式,项目设计中也应该尽量遵守这些设计原则,这里我再挖个坟,简单列一下:

2.1 单一职责

  将函数功能细分,每个函数仅处理一种业务逻辑, 目的是对代码解耦,更好的复用。

  讲道理,真正在实施项目的时候,很少有人能遵照这个原则进行设计,大多数开发者都是胡乱一气的编写函数,我曾在南京的一家公司负责WEB产品设计,其中一个做服务端开发的女孩子,一个函数3000多行……我不得不将其逐步重构,以便未来更好的维护。

  还有一个做前端的小男孩,整篇JS将近8000行,各种逻辑参杂在一起,我曾建议过他对功能函数进行拆分,但是他嗤之以鼻——又不给我加工资咯。当然结果很惨,后来一次功能调整需要在所有终端上同步更新用户头像,然后他离职了——因为程序已经没办法维护了,处处修改,各种漏洞,牵一发动全身。

2.2 里氏替换

  原则的定义为“任何基类出现的地方,子类一定可以出现”。

  很拗口,也很难理解。我把它当白话说,就是派生类除了自身功能的扩展需求外,不应该改写父类的实现,而且对父类的方法引用不应该轻易的调整。

  为什么?

  举个简单的例子,Java中Object作为超类其实现了toString()方法,现在一个新类重写并返回null,后续从该类派生的其他子类并未关注,那么在通过toString()方法输出对象信息时,会产生难以理解甚至程序崩溃的情况。

  再者,如果不能通过父类声明的方法来实现派生类多态的逻辑实现,每次做需求调整,都需要对父类方法调用位置进行代码修改,那么工作量和项目风险就骤然上升,甚至整个架构体系都会出现问题。

  出现这种情况一般是在设计之初没有真正的理清需求场景,所以没办法对模型设计做良好的抽象,最终导致整体设计结构被推翻。

  里氏替换原则其实归根到底就是要告诉我们——事先做好抽象工作,搭建底层函数调用体系,顶层应用如无特殊需求,不允许修改基类的功能实现。

2.3 依赖倒置

  高层次模块设计不允许依赖低层次模块设计的实现,简而言之就是依赖接口,而非实现。

  说个很好玩的例子,电脑主板设计如果依赖CPU的实现,那么如果想更换CPU是无法实现的,除非换掉整个主板。而可插拔的主板仅依赖CPU接口,希望升级CPU的时候,拔掉换一个就是了。

2.4 接口隔离

  接口定义时,追求简单。更直白点,如果一个接口中定义了两个方法,那么更好的做法是定义两个接口……(无语,接口定义的多就真的好么,除了功能组合和接口复用确实便利,维护和阅读真尼玛烦,JDK8突然就多了N多个接口定义,我都懒得看)

2.5 知道最少

  这个原则也叫迪米特原则,简而言之我要实现某个功能,需要依赖其他类的方法,那么我并不关心这个类的这个方法的内部实现细节,我只管函数调用的处理结果及返回。

2.6 开闭

  这个原则是我认为最应该作为所有项目实施的必须遵守的原则,对功能扩展开放,对修改关闭。

  啥意思,新增功能可以,怎么添加代码都无所谓,但是!!!不允许修改既有代码!!!

  因为设计结构的不合理,常常就会出现对现有代码的调整,每次调整的代价就是承担修改所带来的额外Bug风险,给测试增加负担,因为之前的验收都白费了,这也是我为什么要写这篇博客的原因。

  综上,设计模式的六大原则告诉我们:

  1. 单一职责原则告诉我们实现类要职责单一
  2. 里氏替换原则告诉我们不要破坏继承体系
  3. 依赖倒置原则告诉我们要面向接口编程
  4. 接口隔离原则告诉我们在设计接口的时候要精简单一
  5. 知道最少原则告诉我们要降低耦合
  6. 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭

三 外发需求

  客户端通过核心将请求转发给其他外围系统,此种场景为请求外发。

  因客户端无法直接与外围系统建立通讯,只能将请求交予核心,由核心负责与外围系统通讯,实现请求外发。

  初始对接较为简单,仅核心以TCP协议与一个外围通信,并且约定请求报文格式为XML,此时系统设计为:

/**
 * 外发请求
 * 
 * @param request XML请求
 * @return XML应答
 */
public static Document redirectRequest(Document request) {
	Document response = null;
	// TODO
	// 1. 建立Socket通信
	// 2. 发送请求request
	// 3. 接收应答response
	// 4. 返回应答
	return response;
}

  再后来,业务复杂了,外围系统越来越多,通讯方式也很多,Kafka、MQ等等,所以…… 改代码:

/**
 * 外发请求
 * 
 * @param request           XML请求
 * @param ip                外围IP
 * @param port              外围端口
 * @param communicationType 通讯模式
 * @return XML应答
 */
public static Document redirectRequest(Document request, String ip, int port, int communicationType) {
	Document response = null;
	// TODO
	// 1. 判断通讯类型:1-同步短连接,2-KAFKA
	// 2. 建立不同类型的通讯
	// 3. 发送请求request
	// 4. 接收应答response
	// 5. 返回应答,如果目标为KAKFA仅返回回执
	return response;
}

  没多久,需求又来了,这次连报文格式都变了,有JSON、有其他格式……我日,继续改吧:

/**
 * 外发请求
 * 
 * @param request           XML请求
 * @param ip                外围IP
 * @param port              外围端口
 * @param communicationType 通讯模式
 * @return XML应答
 */
public static Document redirectXMLRequest(Document request, String ip, int port, int communicationType) {
	Document response = null;
	// TODO
	// 1. 判断通讯类型:1-同步短连接,2-KAFKA
	// 2. 建立不同类型的通讯
	// 3. 发送请求request
	// 4. 接收应答response
	// 5. 返回应答,如果目标为KAKFA仅返回回执
	return response;
}

/**
 * 外发请求
 * 
 * @param request           JSON请求
 * @param ip                外围IP
 * @param port              外围端口
 * @param communicationType 通讯模式
 * @return XML应答
 */
public static JsonObject redirectJSONRequest(JsonObject request, String ip, int port, int communicationType) {
	JsonObject response = null;
	// TODO
	// 1. 判断通讯类型:1-同步短连接,2-KAFKA
	// 2. 建立不同类型的通讯
	// 3. 发送请求request
	// 4. 接收应答response
	// 5. 返回应答,如果目标为KAKFA仅返回回执
	return response;
}

  最后好不容易上线了,需求又一次不约而至,老子不想干了……怎么办?因为每次需求变更,都需要调整现有代码,不仅核心很心痛,应用一样难受,这时候已经凸显出程序的难以维护了。

  其实问题在于设计之初没有很好的控制结构,没能把对外发过程中涉及到的各个因素进行良好的抽象,此时首要考虑的就是如何重构,桥接模式就这样突然蹦了出来。

四 桥接模式

  桥接模式又称桥梁模式,也有叫柄体(Handle and Body)模式或接口(Interface)模式的,这种设计模式是对象的结构模式。它的用意是“将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化”。

  用我自己的理解来说,就是将需求中的不同维度抽象出来,使不同维度的抽象变化独立开来,因此可以绕开对既有代码的不断修改,完美的适应需求扩展。

  桥接模式的类结构图如下:

桥接模式类结构图

  桥梁模式虽然不是一个使用频率很高的模式,但是熟悉这个模式对于理解面向对象的设计原则,包括“开-闭”原则以及组合/聚合复用原则都很有帮助。理解好这两个原则,有助于形成正确的设计思想和培养良好的设计风格。
  桥梁模式的用意是“将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化”。这句话很短,但是第一次读到这句话的人很可能都会思考良久而不解其意。
  这句话有三个关键词,也就是抽象化、实现化和脱耦。理解这三个词所代表的概念是理解桥梁模式用意的关键。
抽象化
  从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征,就是抽象化。例如苹果、香蕉、生梨、 桃子等,它们共同的特性就是水果。得出水果概念的过程,就是一个抽象化的过程。要抽象,就必须进行比较,没有比较就无法找到在本质上共同的部分。共同特征是指那些能把一类事物与他类事物区分开来的特征,这些具有区分作用的特征又称本质特征。因此抽取事物的共同特征就是抽取事物的本质特征,舍弃非本质的特征。 所以抽象化的过程也是一个裁剪的过程。在抽象时,同与不同,决定于从什么角度上来抽象。抽象的角度取决于分析问题的目的。
  通常情况下,一组对象如果具有相同的特征,那么它们就可以通过一个共同的类来描述。如果一些类具有相同的特征,往往可以通过一个共同的抽象类来描述。
实现化
  抽象化给出的具体实现,就是实现化。
  一个类的实例就是这个类的实例化,一个具体子类是它的抽象超类的实例化。
脱耦
  所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦。在这里,脱耦是指将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。
  所谓强关联,就是在编译时期已经确定的,无法在运行时期动态改变的关联;所谓弱关联,就是可以动态地确定并且可以在运行时期动态地改变的关联。显然,在Java语言中,继承关系是强关联,而聚合关系是弱关联。
  将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改换成为弱关联。因此,桥梁模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用聚合关系而不是继承关系,从而使两者可以相对独立地变化。这就是桥梁模式的用意。
  摘自《JAVA设计模式》之桥接模式(Bridge)

五 维度抽象

  这里我简单的将外发涉及的要素抽象如下:

  1. 报文类型,可支持XML、JSON或其他格式
  2. 通讯类型,可支持同步短连接、长连接以及KAFKA、MQ等等各种模式
  3. 外发参数,将所有外发要素配置于数据表,并且提供DAO对其访问

  那么可以定义如下接口和基类了:

/**
 * 用以定义外发请求的通讯模式的实现接口
 */
public interface RedirectRequestCommunicator {

	/**
	 * 发送请求报文
	 * 
	 * @param request 请求报文
	 * @param param   外发参数
	 * @return 应答报文
	 */
	String sendRequest(String request, RedirectRequestParam param);
}

/**
 * 外发处理器基类,按报文格式可派生出具象类型
 */
public class RedirectRequestProcessor {
	private RedirectRequestCommunicator communicator;
	
	/**
	 * 供处理器访问
	 * 
	 * @return 通讯处理器实例
	 */
	public RedirectRequestCommunicator getCommunicator() {
		return this.communicator;
	}

	/**
	 * 构造
	 * 
	 * @param communicator 通讯处理器
	 */
	public RedirectRequestProcessor(RedirectRequestCommunicator communicator) {
		this.communicator = communicator;
	}

	/**
	 * 处理外发请求
	 * 
	 * @param request 外发报文
	 * @param param   外发参数
	 * @return 应答报文
	 */
	public String processRequest(String request, RedirectRequestParam param) {
		return this.communicator.sendRequest(request, param);
	}
}

  注意,Processor关注报文的格式,Communicator关注通讯的类型,而Processor持有一个Communicator类型的成员,也即通过两者的结合才能实现一次满足业务需求的外发。

  并且两者均面向接口和面向对象,在业务场景越来越复杂时,只需要从Processor和Communicator派生出更为具象的类型即可实现自由装配。

六 应用装配

  至此,框架搭建完毕,后续只需要根据具体的业务需求来实现不同的Processor和Communicator即可,比如说我们需要一个支持同步短连接的XML格式报文外发,那么只需要如下实现:

/**
 * 实现同步短连接的外发通讯模式
 */
public class SocketRedirectRequestCommunicator implements RedirectRequestCommunicator {

	/**
	 * 实现同步短连接通讯
	 */
	@Override
	public String sendRequest(String request, RedirectRequestParam param) {
		// TODO
		// 1. 建立通讯(param中包含外围服务器IP端口等信息)
		// 2. 发送请求报文
		// 3. 接收应答报文
		return null;
	}
}

/**
 * 实现XML格式报文的外发处理
 */
public class XmlRedirectRequestProcessor extends RedirectRequestProcessor {

	public XmlRedirectRequestProcessor(RedirectRequestCommunicator communicator) {
		super(communicator);
	}

	/**
	 * 处理外发请求
	 */
	public String processRequest(String request, RedirectRequestParam param) {
		// TODO
		// 1. 格式化请求报文为XML格式
		// 2. 其他业务处理
		return getCommunicator().sendRequest(request, param);
	}
}

  非常舒服了,应用层只需要根据实际业务需求实例化不同格式的外发报文处理器以及不同通讯模式的实现接口,即可装配各种外发场景:

public static void main(String[] args) {
	// 由应用传递
	String request = null;
	// 通过数据库查询获取
	RedirectRequestParam param = null;
	// 实现外发
	String response = new XmlRedirectRequestProcessor(new SocketRedirectRequestCommunicator())
			.processRequest(request, param);
	// 处理应答
	System.out.println(response);
}

  不知道各位看官有没有直观感受,反正我是舒服了,以后只要有新的报文类型或者通讯类型,只管派生出新的实现即可,终于不再需要反反复复的修改程序了。

七 加深优化

  这一部分再简单说下上述实现思路中可能存在的优化点:

  1. 参数RedirectRequestParam可作为静态参数存于Redis,提高访问效率;
  2. 应用不必知道核心有那些具体的通讯实现类型以及报文实现类型,应用应该只面对业务,核心可提供两者的工厂(工厂设计模式读者可自行了解)对实现进一步封装,如果param.getCommunicator==“SOCKET”,那么工厂返回SocketRedirectRequestCommunicator对象即可,也即是说应用只需要了解核心的RedirectRequestCommunicator和RedirectRequestProcessor;
  3. 进一步提供工具函数,对RedirectRequestCommunicator和RedirectRequestProcessor对象的获取动作进行封装,应用仅面向工具函数doRedirectRequest(RedirectRequestParam param, String request);
  4. 应用甚至不应该了解RedirectRequestParam,应用应该只传递参数访问的唯一键即可,所以函数定义为doRedirectRequest(String paramId, String request);
  5. 进一步封装请求类型,显然请求报文用String类型描述已经不合适了,可提供Request基类,并由此派生出各类外发报文的类型,所以函数调整为doRedirectRequest(RedirectRequestParam param, Request request);

  当然,本文只是借着外发业务场景来介绍桥接模式,目前我参与的大型分部署项目,其业务场景远远比之复杂,但归根结底了解模式不是最关键的,如何通过巧妙的设计来重构代码,使之变得更加健壮、更好维护才是每一个开发者的终极目的。

八 结语

  如果想关注更多硬技能的分享,可以参考积少成多系列传送门,未来每一篇关于硬技能的分享都会在传送门中更新链接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章