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

简介:

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

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接口做渲染输出。

反正例子:

030170259741e334bed95faa38ebd075af8e0ee5

正确例子:

837fc2b88621132186f654e8c1d66d1aad791c0a

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

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

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

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

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

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

实例一

9c3838a6b4604cbb0d7a1ee1ffca4d5c589783b6

实例二

284640b56e0033cb2647e8ecd399c68edcc2a470

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

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

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

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

01c8a113fabcddcd79703efeaef71da257719161

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

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

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

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

cfef7be8dfe2b2a7615d83a85522a1afa4dc06e1

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处理。

91d028f025fde46e13a98e69444f42d81b2ea3f1 e7c402272b05e88826eb6e15f2dd8077f2f0d01e陌霖Java架构

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

原文发布时间为: 2018-11-28
本文作者:陌霖Java架构
本文来自云栖社区合作伙伴“ Java技术驿站”,了解相关信息可以关注“ Java技术驿站”。
相关文章
|
消息中间件 Java Kafka
Java 事件驱动架构设计实战与 Kafka 生态系统组件实操全流程指南
本指南详解Java事件驱动架构与Kafka生态实操,涵盖环境搭建、事件模型定义、生产者与消费者实现、事件测试及高级特性,助你快速构建高可扩展分布式系统。
605 7
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
消息中间件 Java 数据库
Java 基于 DDD 分层架构实战从基础到精通最新实操全流程指南
本文详解基于Java的领域驱动设计(DDD)分层架构实战,结合Spring Boot 3.x、Spring Data JPA 3.x等最新技术栈,通过电商订单系统案例展示如何构建清晰、可维护的微服务架构。内容涵盖项目结构设计、各层实现细节及关键技术点,助力开发者掌握DDD在复杂业务系统中的应用。
2237 0
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
559 5
|
负载均衡 Java API
grpc-java 架构学习指南
本指南系统解析 grpc-java 架构,涵盖分层设计、核心流程与源码结构,结合实战路径与调试技巧,助你从入门到精通,掌握高性能 RPC 开发精髓。
1084 8
Java程序员必学:JVM架构完全解读
Java 虚拟机(JVM)是 Java 编程的核心,深入理解其架构对开发者意义重大。本文详细解读 JVM 架构,涵盖类加载器子系统、运行时数据区等核心组件,剖析类加载机制,包括加载阶段、双亲委派模型等内容。阐述内存管理原理,介绍垃圾回收算法与常见回收器,并结合案例讲解调优策略。还分享 JVM 性能瓶颈识别与调优方法,分析 Java 语言特性对性能的影响,给出数据结构选择、I/O 操作及并发同步处理的优化技巧,同时探讨 JVM 安全模型与错误处理机制,助力开发者提升编程能力与程序性能。
Java程序员必学:JVM架构完全解读
|
算法 架构师 Java
Java 开发岗及 java 架构师百度校招历年经典面试题汇总
以下是百度校招Java岗位面试题精选摘要(150字): Java开发岗重点关注集合类、并发和系统设计。HashMap线程安全可通过Collections.synchronizedMap()或ConcurrentHashMap实现,后者采用分段锁提升并发性能。负载均衡算法包括轮询、加权轮询和最少连接数,一致性哈希可均匀分布请求。Redis持久化有RDB(快照恢复快)和AOF(日志更安全)两种方式。架构师岗涉及JMM内存模型、happens-before原则和无锁数据结构(基于CAS)。
481 5
|
缓存 Java 数据库
Java 项目分层架构实操指南及长尾关键词优化方案
本指南详解基于Spring Boot与Spring Cloud的Java微服务分层架构,以用户管理系统为例,涵盖技术选型、核心代码实现、服务治理及部署实践,助力掌握现代化Java企业级开发方案。
608 2
|
机器学习/深度学习 人工智能 Java
Java 技术支撑下 AI 与 ML 技术融合的架构设计与落地案例分析
摘要: Java与AI/ML技术的融合为智能化应用提供了强大支持。通过选用Deeplearning4j、DJL等框架解决技术适配问题,并结合Spring生态和JVM优化提升性能。在金融风控、智能制造、医疗影像等领域实现了显著效果,如审批效率提升3倍、设备停机减少41%、医疗诊断延迟降低80%。这种技术融合推动了多行业的智能化升级,展现了广阔的应用前景。
855 0