写这个贴子的启发来自内网文档,此文力求只写自己的思考和探索,不泄露公司内部知识
一个成熟且宏大的项目架构,应该可以分成三个较为独立的软件:应用程序,工具箱和框架。
对于javaer,最常编写的controller、service、dao都属于应用程序层面;而Guava、Apache Commons、甚至自己编写的xxxUtils,这些属于工具箱;Spring,mybatis,vue这些属于框架。这种分层其实是出于不同层需要的抽象程度不同,越是底层需要越高级的抽象,但一个好的工具箱或者一个好的框架并非越抽象越好,还有很多其他评判视角
-
首先我们想想框架到底是什么
- 最抽象的描述:框架是一种半成品的软件解决方案
- 务实的描述:将原本你要写的复杂的东西由别人写好并封装起来,黑盒中的程序已有大致运行逻辑
- 最务实的描述:在OOP中,框架是一类相互协作的类,这些类能够被某一类特定软件高效复用。
在我看来,设计一个框架和设计一门语言也有相似之处,都是实现某种特性,对上层提供API;很形象的一点是:对于协程这个特性,Erlang在语言层面就支持了,而Skynet在框架层面提供支持。
以Spring & SpringBoot为例,这类框架宏观上定义了项目的体系结构,各部分的职责,以及最关键的 类和对象在其中如何协作
其次,框架还会预定义一些参数,便于让开发者专注于更业务向的决定;Springboot的约定大于配置就是说这个。
使用框架编程时,会导致开发的程序和调用的方法间的反向控制;传统我们使用第三方工具时是主动调用,但使用框架时,很多代码写好后是交给框架调用的。为此会提出一系列概念,比如拦截器,过滤器,触发器,回调函数,钩子,响应式编程......
-
什么才是一个好框架
首先,最核心的原则是:对底层技术足够抽象,对上层团队足够友好
-
抽象
抽象的程度很难把握,较多的抽象会提出很多概念,涉嫌过度设计,理解成本较高;较低的抽象使用起来可能会很鸡肋;这就对设计人员有很大的考验
思考1:使用继承 or 组合
在OOP中,功能复用的两类方式是:类继承 and 组合,框架本身定义了对象间的协作关系,所以明确类与类间如何复用是很重要的。
感性理解上,二者的构建逻辑差很多
- 继承:当子类需要具有父类的特性时使用,各个子类和父类很像,子类彼此之间也因此很像;由于复杂性考虑,一个子类一般只有一个父类(或者是一个儿子无法有很多爹的特性)
- 组合:当集成类需要具有局部类的功能时使用,当集成类拥有相似的局部类时,这些集成类就很像;这时一个集成类可以拥有很多局部类(及其功能)
让我们想想一个问题怎么用两种思维解决
如果我们要刻画不同动物有共性(或者共同的功能),但又各有不同
-
如果用继承
1. class Animal{ 1. private void beat(){ 1. System.out.println("心脏跳动..."); 1. } 1. public void breath(){ 1. beat(); 1. System.out.println("吸一口气,呼一口气,呼吸中..."); 1. } 1. } 1. //继承Animal,直接复用父类的breath()方法 1. class Bird extends Animal{ 1. //创建子类独有的方法fly() 1. public void fly(){ 1. System.out.println("我是鸟,我在天空中自由的飞翔..."); 1. } 1. } 1. //继承Animal,直接复用父类的breath()方法 1. class Wolf extends Animal{ 1. //创建子类独有的方法run() 1. public void run(){ 1. System.out.println("我是狼,我在草原上快速奔跑..."); 1. } 1. } 1. public class InheritTest{ 1. public static void main(String[] args){ 1. //创建继承自Animal的Bird对象新实例b 1. Bird b=new Bird(); 1. //新对象实例b可以breath() 1. b.breath(); 1. //新对象实例b可以fly() 1. b.fly(); 1. Wolf w=new Wolf(); 1. w.breath(); 1. w.run(); 1. /* 1. ---------- 运行Java程序 ---------- 1. 心脏跳动... 1. 吸一口气,呼一口气,呼吸中... 1. 我是鸟,我在天空中自由的飞翔... 1. 心脏跳动... 1. 吸一口气,呼一口气,呼吸中... 1. 我是狼,我在草原上快速奔跑... 1. 1. 输出完毕 (耗时 0 秒) - 正常终止 1. */ 1. } 1. }父类描述了子类共有的属性(或能力),像是动物们都可以呼吸;子类继承了这种状态(能力),扩展自身的特殊能力 -
如果用组合
1. //CompositeTest.java 使用组合方式实现目标 1. class Animal{ 1. private void beat(){ 1. System.out.println("心脏跳动..."); 1. } 1. public void breath(){ 1. beat(); 1. System.out.println("吸一口气,呼一口气,呼吸中..."); 1. } 1. } 1. class Bird{ 1. //定义一个Animal成员变量,以供组合之用 1. private Animal a; 1. //使用构造函数初始化成员变量 1. public Bird(Animal a){ 1. this.a=a; 1. } 1. //通过调用成员变量的固有方法(a.breath())使新类具有相同的功能(breath()) 1. public void breath(){ 1. a.breath(); 1. } 1. //为新类增加新的方法 1. public void fly(){ 1. System.out.println("我是鸟,我在天空中自由的飞翔..."); 1. } 1. } 1. class Wolf{ 1. private Animal a; 1. public Wolf(Animal a){ 1. this.a=a; 1. } 1. public void breath(){ 1. a.breath(); 1. } 1. public void run(){ 1. System.out.println("我是狼,我在草原上快速奔跑..."); 1. } 1. } 1. public class CompositeTest{ 1. public static void main(String[] args){ 1. //显式创建被组合的对象实例a1 1. Animal a1=new Animal(); 1. //以a1为基础组合出新对象实例b 1. Bird b=new Bird(a1); 1. //新对象实例b可以breath() 1. b.breath(); 1. //新对象实例b可以fly() 1. b.fly(); 1. Animal a2=new Animal(); 1. Wolf w=new Wolf(a2); 1. w.breath(); 1. w.run(); 1. /* 1. ---------- 运行Java程序 ---------- 1. 心脏跳动... 1. 吸一口气,呼一口气,呼吸中... 1. 我是鸟,我在天空中自由的飞翔... 1. 心脏跳动... 1. 吸一口气,呼一口气,呼吸中... 1. 我是狼,我在草原上快速奔跑... 1. 1. 输出完毕 (耗时 0 秒) - 正常终止 1. */ 1. } 1. }局部类描述了集成类们的共有功能,集成类通过引用内部集成的局部类来实现此功能;集成类也可以额外定义自身功能 -
继承:is-a
- 特点:白箱复用(子类对继承自父类的方法如何实现是明确的)
- 优点:
- 继承后的子类可以通过重写 方便的改变实现
- 子类自动继承父类接口
- 创建子类对象时,无需创建父类对象
- 功能拓展简单直观
- 缺点:
- 多继承的复杂性很高
- 继承关系无法在运行时改变,只能在编译时声明+确定;
- 子类无法改变父类定义好的接口
- 所谓”破坏了封装型“,子类对父类拥有读和写(重写)的权限
- 当多个子类都有一个父类不应该有的属性时,这个属性无法复用,只能在多个子类中冗余着存在,要解决可能只能再在父子间加一层
- ...
-
组合:has-a
- 特点:黑箱复用
- 优点:
- 因为在上层调用下层过程中,有一层接口层,从而上层无需感知下层实现
- 组合依赖的引用关系在运行时也可以修改,例如Java在运行时通过类加载器加载出新的实现类来,或动态代理生成新的类来
- ...
- 缺点:
- 整体类无法自动获得局部类同样的接口
- 创建一个新的整体类对象时,需要创建所有局部类的对象(或通过类似Spring中IOC容器中注入单例bean解决问题)
-
在决定选择哪种对象复用结构时,前人给出了一些原则,可以在设计阶段判断这个框架是否好用:
- 里氏代换原则
子类型必须能够替换基类型,反过来未必成立
- 开放 - 封闭原则
整个类结构应该对扩展开放,对修改关闭(或者是版本更新无需修改源代码,只需添加新代码)
- 依赖倒置原则
高层模块不应该依赖于底层模块,而是二周都依赖于抽象(或者是面向接口编程,而不是面向实现编程)
- 单一职责原则
指 不要存在多于一个导致变更的原因
- ...
- 里氏代换原则
思考二:业务需要封闭什么?开放什么?
不同的业务具有不同的侧重,自然需要不同的结构;在web开发中可能体现乏力,尤其Java世界里出现的大一统级别的框架似乎能解决全部问题(当然由于我见识有限,各种优秀的web开发框架或子框架还没接触到),比如说现在流行的Spring-SpringMVC-myBatis,基本上给crud的业务提供了宝宝级的服务;但是在游戏行业有大不同,各种游戏由于玩法不同,似乎很难抽象出统一的开发框架,更常见的情形是:对于SLG游戏开发相应框架,对于Moba游戏开发相应框架,对于MMO游戏开发相应框架...
所以这对框架设计带来的启示是:业务需要怎么抽象?哪些内容是一定的,哪些又是需要开发的
- 对于Spring而言,bean对象的管理和控制反转是更高效的,AOP面向切面编程是适合业务的,而如何使用这些对象操作数据流转是不确定的,由开发者编写
- 对于myBatis-plus而言,似乎什么都是确定的,很多场景下完全不需要程序员再二次开发,调接口就完事了
- 假如要对Moba游戏开发一种框架,肯定需要考虑到程序的运行只有十个玩家和一堆NPC,确定的内容是需要提供一种高效高可用的对象间通信或方法调用的方式,需要开发的是每个英雄的技能效果,游戏不同机制等等
- 假如对MMO游戏开发一种框架,需要考虑到在大世界下,一个服务器内成百上千个玩家间交互,框架层面解决大量玩家同时在线时的高并发处理和数据同步问题。
- 如果要对SLG游戏开发一种框架...
-
-
友好:
这个层面对框架的评估就很直观,因为本质上评价直接解决程序员痛点的能力。
-
框架的报错体系是否合理 在对不可调试的代码debug过程中,我们都希望traceback能够直接准确的指向人为编写的错误的代码,而不是顾左右而言他,或者指到框架内部的莫名其妙的位置
-
框架是否方便扩展;程序员想要按照框架定义的玩法玩下去,自然不能让玩的成本太高,也就是说:业务代码基于框架,并且其中没有多少用于接入框架的代码
-
框架的学习成本是否合理,也就是说是否为了抽象而抽象出太多概念导致难以学习。
-