Java架构-一些设计上的基本常识

作者:陌霖Java架构
链接:www.juejin.im/post/5be7bc195188255e9b618c93


最近给团队新人讲了一些设计上的常识,可能会对其它的新人也有些帮助, 把暂时想到的几条,先记在这里。

1、API与SPI分离

框架或组件通常有两类客户,一个是使用者,一个是扩展者。 API(Application Programming Interface)是给使用者用的, 而SPI(Service Provide Interface)是给扩展者用的。 在设计时,尽量把它们隔离开,而不要混在一起, 也就是说,使用者是看不到扩展者写的实现的。

比如:

  1. 一个Web框架,它有一个API接口叫Action, 里面有个execute()方法,是给使用者用来写业务逻辑的。然后,Web框架有一个SPI接口给扩展者控制输出方式。
  2. velocity模板输出还是用json输出等, 如果这个Web框架使用一个都继承Action的VelocityAction和一个JsonAction做为扩展方式, 要用velocity模板输出的就继承VelocityAction,要用json输出的就继承JsonAction, 这就是API和SPI没有分离的反面例子。

SPI接口混在了API接口中,合理的方式是,有一个单独的Renderer接口,有VelocityRenderer和JsonRenderer实现, Web框架将Action的输出转交给Renderer接口做渲染输出。

反正例子:

正确例子:

2、服务域/实体域/会话域分离

任何框架或组件,总会有核心领域模型,比如:

实体域:像Spring的Bean,Struts的Action,Dubbo的Service,Napoli的Queue等等 。这个核心领域模型及其组成部分称为实体域,它代表着我们要操作的目标本身, 实体域通常是线程安全的,不管是通过不变类,同步状态,或复制的方式。

服务域:也就是行为域,它是组件的功能集,同时也负责实体域和会话域的生命周期管理。比如Spring的ApplicationContext,Dubbo的ServiceManager等, 服务域的对象通常会比较重,而且是线程安全的,并以单一实例服务于所有调用。

会话域:就是一次交互过程, 会话中重要的概念是上下文,什么是上下文? 比如我们说:“老地方见”,这里的“老地方”就是上下文信息, 为什么说“老地方”对方会知道,因为我们前面定义了“老地方”的具体内容, 所以说,上下文通常持有交互过程中的状态变量等, 会话对象通常较轻,每次请求都重新创建实例,请求结束后销毁。

简而言之: 把元信息交由实体域持有, 把一次请求中的临时状态由会话域持有, 由服务域贯穿整个过程。

实例一

实例二

3、在重要的过程上设置拦截接口

1.如果你要写个远程调用框架,那远程调用的过程应该有一个统一的拦截接口;
2.如果你要写一个ORM框架,那至少SQL的执行过程,Mapping过程要有拦截接口;
3.如果你要写一个Web框架,那请求的执行过程应该要有拦截接口;

等等,就可以自行完成,而不用侵入框架内部。拦截接口,通常是把过程本身用一个对象封装起来,传给拦截器链。

比如:远程调用主过程为invoke(),那拦截器接口通常为invoke(Invocation),Invocation对象封装了本来要执行过程的上下文,并且Invocation里有一个invoke()方法, 由拦截器决定什么时候执行。同时,Invocation也代表拦截器行为本身, 这样上一拦截器的Invocation其实是包装的下一拦截器的过程, 直到最后一个拦截器的Invocation是包装的最终的invoke()过程, 同理,SQL主过程为execute(),那拦截器接口通常为execute(Execution),原理一样, 当然,实现方式可以任意,上面只是举例。

4、重要的状态的变更发送事件并留出监听接口

这里先要讲一个事件和上面拦截器的区别:

拦截器:是干预过程的,它是过程的一部分,是基于过程行为的。 事件:是基于状态数据的,任何行为改变的相同状态,对事件应该是一致的,事件通常是事后通知,是一个Callback接口,方法名通常是过去式的,比如onChanged()。

比如远程调用框架,当网络断开或连上应该发出一个事件,当出现错误也可以考虑发出一个事件, 这样外围应用就有可能观察到框架内部的变化,做相应适应。

5、扩展接口职责尽可能单一,具有可组合性

比如,远程调用框架它的协议是可以替换的, 如果只提供一个总的扩展接口,当然可以做到切换协议, 但协议支持是可以细分为底层通讯,序列化,动态代理方式等等, 如果将接口拆细,正交分解,会更便于扩展者复用已有逻辑,而只是替换某部分实现策略, 当然这个分解的粒度需要把握好。

6、微核插件式,平等对待第三方

大凡发展的比较好的框架,都遵守微核的理念

Eclipse的微核是OSGi, Spring的微核是BeanFactory,Maven的微核是Plexus。

通常核心是不应该带有功能性的,而是一个生命周期和集成容器, 这样各功能可以通过相同的方式交互及扩展,并且任何功能都可以被替换, 如果做不到微核,至少要平等对待第三方, 即原作者能实现的功能,扩展者应该可以通过扩展的方式全部做到, 原作者要把自己也当作扩展者,这样才能保证框架的可持续性及由内向外的稳定性。

7、不要控制外部对象的生命周期

比如上面说的Action使用接口和Renderer扩展接口, 框架如果让使用者或扩展者把Action或Renderer实现类的类名或类元信息报上来。然后在内部通过反射newInstance()创建一个实例, 这样框架就控制了Action或Renderer实现类的生命周期, Action或Renderer的生老病死,框架都自己做了,外部扩展或集成都无能为力。

好的办法是让使用者或扩展者把Action或Renderer实现类的实例报上来, 框架只是使用这些实例,这些对象是怎么创建的,怎么销毁的,都和框架无关, 框架最多提供工具类辅助管理,而不是绝对控制。

8、可配置一定可编程,并保持友好的CoC约定

因为使用环境的不确定因素很多,框架总会有一些配置, 一般都会到classpath直扫某个指定名称的配置,或者启动时允许指定配置路径, 做为一个通用框架,应该做到凡是能配置文件做的一定要能通过编程方式进行, 否则当使用者需要将你的框架与另一个框架集成时就会带来很多不必要的麻烦。

另外,尽可能做一个标准约定,如果用户按某种约定做事时,就不需要该配置项。 比如:配置模板位置,你可以约定,如果放在templates目录下就不用配了, 如果你想换个目录,就配置下。

9、区分命令与查询,明确前置条件与后置条件

这个是契约式设计的一部分,尽量遵守有返回值的方法是查询方法,void返回的方法是命令, 查询方法通常是幂等性的,无副作用的,也就是不改变任何状态,调n次结果都是一样的。比如get某个属性值,或查询一条数据库记录。

命令是指有副作用的,也就是会修改状态,比如set某个值,或update某条数据库记录, 如果你的方法即做了修改状态的操作,又做了查询返回,如果可能,将其拆成写读分离的两个方法。

比如:

  1. User deleteUser(id),删除用户并返回被删除的用户,考虑改为getUser()和void1的deleteUser()。
  2. 另外,每个方法都尽量前置断言传入参数的合法性,后置断言返回结果的合法性,并文档化。

10、增量式扩展,而不要扩充原始核心概念

我们平台的产品越来越多,产品的功能也越来越多, 平台的产品为了适应各BU和部门以及产品线的需求。势必会将很多不相干的功能凑在一起,客户可以选择性的使用, 为了兼容更多的需求,每个产品,每个框架,都在不停的扩展, 而我们经常会选择一些扩展的扩展方式,也就是将新旧功能扩展成一个通用实现。

我想讨论是,有些情况下也可以考虑增量式的扩展方式,也就是保留原功能的简单性,新功能独立实现。我最近一直做分布式服务框架的开发,就拿我们项目中的问题开涮吧。

比如:远程调用框架,肯定少不了序列化功能,功能很简单,就是把流转成对象,对象转成流, 但因有些地方可能会使用osgi,这样序列化时,IO所在的ClassLoader可能和业务方的ClassLoader是隔离的, 需要将流转换成byte[]数组,然后传给业务方的ClassLoader进行序列化。

为了适应osgi需求,把原来非osgi与osgi的场景扩展了一下, 这样,不管是不是osgi环境,都先将流转成byte[]数组,拷贝一次。然而,大部分场景都用不上osgi,却为osgi付出了代价, 而如果采用增量式扩展方式,非osgi的代码原封不动, 再加一个osgi的实现,要用osgi的时候,直接依赖osgi实现即可。

再比如:最开始,远程服务都是基于接口方法,进行透明化调用的, 这样,扩展接口就是,invoke(Method method, Object[] args), 后来,有了无接口调用的需求,就是没有接口方法也能调用,并将POJO对象都转换成Map表示, 因为Method对象是不能直接new出来的,我们不自觉选了一个扩展式扩展, 把扩展接口改成了invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args), 导致不管是不是无接口调用,都得把parameterTypes从Class[]转成String[]。

如果选用增量式扩展,应该是保持原有接口不变, 增加一个GeneralService接口,里面有一个通用的invoke()方法, 和其它正常业务上的接口一样的调用方式,扩展接口也不用变, 只是GeneralServiceImpl的invoke()实现会将收到的调用转给目标接口, 这样就能将新功能增量到旧功能上,并保持原来结构的简单性。

再再比如:无状态消息发送,很简单,序列化一个对象发过去就行, 后来有了同步消息发送需求,需要一个Request/Response进行配对, 采用扩展式扩展,自然想到,无状态消息其实是一个没有Response的Request, 所以在Request里加一个boolean状态,表示要不要返回Response, 如果再来一个会话消息发送需求,那就再加一个Session交互。然后发现,原来同步消息发送是会话消息的一种特殊情况, 所有场景都传Session,不需要Session的地方无视即可。 如果采用增量式扩展,无状态消息发送原封不动。

同步消息发送,在无状态消息基础上加一个Request/Response处理, 会话消息发送,再加一个SessionRequest/SessionResponse处理。

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!

赞(2) 打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。Java 技术驿站 » Java架构-一些设计上的基本常识
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

Chenssy's Blog | 致力打造 Java 精品博客

联系作者优质文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏