思路
我们日常开发,一直强调设计、重构
其实是为了避免代码冗余的问题,尽可能地将相同逻辑地代码封装起来。
开源市场上,各种开发框架层出不穷,在我看来,这些开源软件终究为了:
- 提供开发运行时的IT组件,提升开发效率;
- 整合通用的功能逻辑,提高软件复用度;
- 良好的设计模式,提升系统的可拓展性。
细细思考一下,第2点最终也是为了第1点。在软件设计的基础上,又延申了23种设计模式。纵观这些设计模式,无一不是为了提供软件的拓展性、复用性等等。
如何创建连接池?如何操作redis?如何ORM?如何MVC?如何打印日志(小心log4j的bug)......
各种复杂的底层逻辑都尽可能地被这些框架封装完了,我们基本上只需要关注业务逻辑的实现。
纵观这些开源框架,都会抽象出的一个模型或者说组件,比如Mybaits
的mapper
、Spring
的bean
、vue
的component
,然后开发人员在这抽象模型基础上进行业务逻辑的开发。
框架这些都是技术逻辑的封装,实际上我们还有很多业务逻辑,都面临这样的这样问题,所以可以思考一下是否能有类似的模块化、组件化
的体系来解决业务逻辑复用的问题。
当然,业务逻辑的复用颗粒度是很难界定的,小到一个简单的文件上传的功能,中到订单查询的服务,大到用户权限认证的子系统。
为了方便说明,统一名词,我把这种属于模块化、组件化
特性的服务、功能、方法等均统一称之为模组。
那么是不是可以考虑,如何形成一套机制,把代码里相同的逻辑抽象出来,形成模组;实现一套编排平台技术,在平台上能够编排模组,平台将编排的结果直接生成开发人员喜闻乐见的代码工程。开发人员完成开发后,再由平台去解决部署的事宜。
将简单交给用户,复杂留给平台。让平台来告诉开发人员:xxx模组已经具备这个功能了,拿来就可以使用。这样对开发人员而言,真正做到只需关注业务逻辑的实现了。
正如前面所说,其实类似这样的模块化、组件化
的思想在很多开源框架上都有体现,比如前端的react
、vue
,后端的Spring
、Mybaits
。
前端的框架我不是特别熟,这里借鉴一下Spring
框架的思想。
Spring 的思想
Spring的bean就可以看作模组,业务逻辑的开发其实就是对这些bean的组合。我们要使用一个bean,直接通过@Resouce或@AutoWired
注解拿来即用。
同样的,我们把模组看作bean,那么应该提供什么的方式把模组拿来即用呢?
当然也不能完全把模组看作bean,模组内部之间是不应有相互的调用的,因为这样一来可能就会出现循环依赖的问题。
虽然程序不会出错,但是在逻辑关系上,这样会陷入 ”剪不断、理还乱“ 的矛盾中。
解决循环依赖一个办法是引入事件机制,通过注册bean调用的事件来解耦bean之间的逻辑关系。
Spring天生支持事件机制的,基于ApplicationListener
接口可以很方便的注册自定义的事件,只需要继承ApplicationEvent
类即可。
通过事件驱动,我们可以很直观通过事件把这些bean的逻辑关系串联起来。
你会发现,这样的依赖关系,就是通过事件来触发代码,这就是serverless(无服务器)的一个思想。每一个method(方法),其实就是对应了serverless的函数。
但是这样可能又会引来新的问题:异步事件怎么处理?
其实也不难,我们一般只需要注册一个回调接口,等待异步事件处理完毕,然后等待触发这个回调接口,回调接口的实现就是我们要定义的功能逻辑。
如果我们把这些bean的method拆分出来,独立部署成单个的微服务,这就是分而治之的思想,每一个bean就是一个模组。这样的拆分还有一个好处,可以解耦各个模组无关的依赖,比如ServiceA需要连接A大区的数据库,ServiceB需要连接B大区的数据库,在一个工程里,需要创建两个jdbc连接,而这两个jdbc连接可能ServiceC完全都用不到。根据单一职责原则,每个模组应该只聚焦属于它自己的逻辑。
在领域模型进行解耦,上下文关系,事件机制,这很符合DDD领域的思想。
反过来,假设我们已经有了ServiceA、ServiceB、ServiceC、ServiceD这些模组,需要组合成新的服务,模组之间的逻辑我也理清楚了,那我如何像使用bean一样使用这些模组呢?
所以,这就是需要考虑如何形成一套模组编排机制,支持自由组合模组。模组的编排本质是属于代码的编排,所以,我把这一类的编排统称为“代码编排”。
为什么是代码编排?
个人认为,大部分业务可能更适合通过编码的方式来实现,业务复杂度不说,业务需求总是会各种变,不变的业务当然可以除外。 对于开发人员(比如我),我可能更愿意:通过编排页面,把我需要的模块功能先聚集在一起(可以不需要模块之间的组合逻辑),然后手动开发,编码这些模组之间的调用逻辑,最后实现整个业务系统。如果业务逻辑变化,我可以再引入新的模组,因此,这得需要编排平台生成的代码工程具备良好的拓展性。
—— 有点类似搭积木,假设市场上提供了1000个积木,我(开发人员)在编排页面上选择100个需要积木,然后搭建出我想要的模型出来。 也就说我只需要知道哪100个积木即可,我觉得这个应该是平台要帮助我做的事情。
代码编排思考
我们为模组编排之后生成的结果,表现为可执行的代码文件,这样的编排,我将其定义为“代码编排”。
编排的结果可以任意部署,如果是部署到Serverless平台上,那么这一类编排的结果,可以看作是FaaS
(函数即服务)。
代码编排需要依托的模组的发展,有了足够数量的模组,并且形成了一个模组市场(或者说模组开源社区)。假设我们要实现某个中台能力,已经理清了业务逻辑,知道这个中台能力需要具备哪些功能,那么我们就可以从模组市场上选择具备这些功能的模组,通过编排平台打包,生成该中台能力的代码骨骼工程。
当然,每个模组都可以独立部署,app里只需要生成调用模组的逻辑代码即可。
具体生成的代码,我写了个demo,可以见附录1。
这里可能还得需要强调一下:代码编排并完全不等于代码生成。传统的代码生成一般都是生产商提供了若干应用模型(比如创建博客的wordpress),通过固化好的模板代码,然后根据传入参数最后生成系统。
一些常见的PaaS产品
结合常见的PaaS产品:(目前先暂时不做前端代码编排的思考)
- 规则引擎:业务规则(是or否)的编排;属于代码级别的编排,但是颗粒度较小;
- 服务编排:接口API调用方式的编排;属于服务级别的编排;
- 低代码:业务流程的编排,也是接口API调用方式编排,基本上也可以归于服务编排。
规则引擎不需要多说,仅是规则的组合,true还是false的解释执行,严格来说都不属于编排。组合的结果以何种形式存在?如何解释执行组合编排的结果?这些问题倒是可以参考规则引擎的执行引擎原理。
服务编排平台的局限很大,仅用来组合运行态的服务,提供了若干组合逻辑的工具,比如参数处理、条件判断等工具。但是业务逻辑千变万化,不可能有个通用模式把全部逻辑给工具化,比如事务、异常捕获、异步处理等等,像这些逻辑,就得需要考虑手动编码去实现了。
低代码与服务编排本质上归属同一类,都是服务级别的编排。服务级别的编排一个问题就是依赖业务应用提供的服务能力,并且编排结果运行态一般都是HTTP服务,只能远程调用。想一下,如果我一个服务被编排到若干个中台能力,首先我这些中台能力运行环境都得需要先把与该服务的网络打通,极端情况下,假设我这个服务挂了,那我这几个中台能力都不可用了。
当然低代码的仅为前台业务应用而服务,与我所说的代码编排应用场景可能不太一样,关于代码编排和低代码的区别,我还做了一个对比。
代码编排与低代码的区别
两者思想都差不多,都是本着“少写代码”这一基本思想。
注:模组化平台 仅对 低代码平台的业务流程进行比较,即只是后端的对比。低代码的前端页面这里不做讨论。
低代码平台 | 代码编排平台 | |
---|---|---|
颗粒度 | 服务级别编排 ① | 代码级别编排 ② |
定位 | 编排中台能力,面向前台应用 | 编排中台模组,面向中台能力 |
使用角色 | 业务人员 | 开发人员 |
执行引擎 | 对业务流程的解释执行,输出为运行态的服务 | 对代码的解释执行,输出为可执行的代码文件 |
部署 | 编排结果统一部署在低代码平台环境上 | 支持编排结果在任意环境部署,也可以直接内嵌在业务工程里 |
调试 | 需要部署到专门的沙箱环境中,进行HTTP方式的联调 | 可直接在本地环境进行调试 |
运行态 | 独立的轻量级HTTP服务 | 任意形态,比如微服务、服务网格、轻量级服务等 |
运行依赖 | 依赖于业务系统提供的基础能力和服务 | 代码编译后的执行文件,基本上无需外部依赖 |
日志 | 依赖服务能力自身的日志输出 | 平台可在模组调用前后进行日志埋点,方便后续问题查找 |
运维 | 重量级运维,通过能力运营中心对能力服务统一管控 | 轻量级运维,模组的运维由开发者自行管控 |
演进方向 | 为前台业务场景而持续演进 | 可以演进为构建业务应用、构建中间件甚至构建整个系统 ③ |
名词解释:
① 服务级别编排:将服务的调用方式以一定的逻辑串联起来,最后发布为一个新的服务(一般是HTTP服务),编排的本质是可视化业务流程。
② 代码级别编排:选取若干模组,模组的逻辑可以在页面串联,也可以自己进行二次编码,最后发布成可被解释执行的代码工程,编排的本质是功能逻辑的组合。
③ 模组化平台的演进方向:当期模组化仅面向中台能力,理论上来说,前中后台应用、业务系统,只要能拆分规范的模组(代码+文档),基于代码编排的原理,均可以通过编排平台进行组合。比如通过模组编排平台选取若干子系统模组,编排平台生成系统工程的代码骨骼,开发人员再对骨骼代码进一步填充。不过这得依赖模组的开源社区的成熟度。
一定程度上来说,代码编排将来是可以覆盖低代码后端的功能,甚至可以替代低代码。
编排平台技术发展的方向
考虑形成一套代码编排的工具,运行态的承载以serverless为主。
- 代码编排平台:具备模组目录、可视化编排以及代码功能自动生成的功能。目前仅有服务编排平台、低代码平台做参考,平台可能需要自研实现;
- Serverless平台:解决编排结果部署的问题。计划实现一套函数计算平台(FaaS),可以开源框架作为参考。
个人认为是代码编排可以作为一个单独的PaaS产品,用户模组编排组合以及工程代码文件生成。使用serverless来解决解决部署运行时的问题。当然了,代码编排的结果不一定非要部署到serverless平台上,serverless平台可以承载任意函数的部署。
想象你是一个将军,一个个模组就是你的小兵,代码编排就是如何排兵布阵,serverless就是你把小兵派上的战场。
参考资料
唯一能找到跟代码编排技术相关的社区文章:https://cloud.tencent.com/developer/article/1518034
附录
附录1. 编排页面生成后的代码
通过代码编排平台生成代码是一个难点。之前有做过这方面的研究,但是没能做出来,也跟当时个人技术水平有关。现正在出需研究
一个由编排平台生成后的代码逻辑demo如下:
/**
代码编排后生成的代码,预留了一些拓展接口,供开发人员二次编码
当然实际不应该以这种上下文逻辑代码方式来表述,而更应该通过事件机制来触发模组之间的调用
*/
class Main{
Response hand(JsonObject pin){
// 工厂模式来获取service
Service serviceA = ServiceFatory.getService("a","http"); //a是http服务的模组
Service serviceB = ServiceFatory.getService("b","jar"); //b就是一个jar包sdk的模组
Service serviceC = ServiceFatory.getService("c","dubbo"); //c是一个dubbo 服务的模组
// 注册的回调函数 SPI机制
CallBackInterface callBackInterface = ServiceLoader.load(CallBackInterface.class).iterator().next();
try{
JsonObject pinA1 = callBackInterface.doSthBefore(serviceA,pin); // A服务调用之前自定义操作(AOP思想)
JsonObject pinA2 = serviceA.call(pinA1);
JsonObject pinA3 = callBackInterface.doSthAfter(serviceA,pinA2); //A服务调用之后自定义操作
JsonObject result;
if(pinA3.get("id") == 123){ // if...else 的逻辑是自定义编排的逻辑
JsonObject pinB1 = callBackInterface.doSthBefore(serviceB,pinA3); // B服务调用之前自定义操作
JsonObject pinB2 = serviceB.call(pinB1);
result = callBackInterface.doSthAfter(serviceA,pinB2); //B服务调用之后自定义操作
}else{
JsonObject pinC1 = callBackInterface.doSthBefore(serviceC,pinA3); // C服务调用之前自定义操作
JsonObject pinC2 = serviceC.call(pinC1);
result = callBackInterface.doSthAfter(serviceC,pinC2); //C服务调用之后自定义操作
}
return Response.success(result);
}catch(Exception e){
JsonObject result = callBackInterface.doSthAfterException(e,pin);
return Response.error(result);
}
}
}
/*
为开发人员拓展的接口,封装服务调用前后需要做的操作,AOP思想。
*/
interface CallBackInterface{
JsonObject doSthBefore(Service service,JsonObject pin); //服务调用之前的操作
JsonObject doSthAfter(Service service,JsonObject pin); // 服务调用之后的操作
JsonObject doSthAfterException(Exception e,JsonObject pin); //出现异常后的操作
}
/* 封装服务的调用方式和其他基本信息。不同类型的服务需要继承接口 */
interface Service{
String name(); //方法名
String type(); //服务类型
JsonObject call(JsonObject pin); //调用方式
}
/* http类型的服务 */
interface HttpService extends Service{
void setUrl(String url); //指定下http的请求地址
}
/* jar包类型的服务 */
interface JarService extends Service{
void setJarName(String jarName); //指定jar信息
}
/* dubbo类型的服务 */
interface DubboService extends Service{
void setZkAddress(String zkAddress); //注册中心地址
void setOriginName(String originName); //原服务名
}
/* 创建模组服务的工厂 */
class ServiceFatory{
static Map<String,Document> documentMap ; //文档全集,每个模组都对应一个文档。随平台启动而初始化。
static Service getService(String name,String type){
HttpService httpService = new MyHttpService(); //HTTP服务接口的实现,这里略
Document document = documentMap.get(name);
if(type.equals("http")){
String url = document.get("url");
httpService.setUrl(url);
return httpService;
}else{
//其他类型代码略
}
}
}
/* 模组的文档,本质也是一个Map, 用来存储模组的基本信息 */
class Document extends HashMap{
// 模组的基本信息,本质就是一个Map。
// 通过put的方法设置基本属性,比如模组基本信息、原服务名、注册中心地址、调用依赖等等
}
发表评论