【架构&设计】java 架构设计 - hippowc/hippowc.github.io GitHub Wiki
关于设计
面向接口编程
接口的本质
接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界“如果你是……则必须能……”的理念。
关于抽象
- 抽象的粒度或者深度
- 抽象的维度,关注点。
做系统设计,最主要是把系统中不变的地方抽象出来
个人思考
设计架构的重点不是如何使用框架,是如何使用最简单的抽象的方式将系统主干描述出来
实现时考虑的两件事:
- 如何进行组件拆分:接口,抽象,相同点
- 如何把组件组合在一块:依赖反转,spring、guice框架等
架构设计书籍笔记
架构整洁之道
- 软件架构的终极目标是,用最下的人力成本来满足构建和维护系统的需求,一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。
编程范式
程序的编写模式,与具体的编程语言关系相对较小,目前一共只有三种编程范式,未来也几乎不会有新的。
- 结构化编程:主张使用if else,do while等语句来代替goto语句。结构化编程对程序控制权的直接转移进行了限制的规范。
- 面向对象编程:主张利用多态来限制用户函数指针的使用。对程序控制权的间接转移进行了限制和规范。
- 函数式编程:某个符号对应的值永远不变,理论上来说,函数式语言应该没有赋值语句。函数式编程对程序中的赋值进行了限制和规范。
编程范式不是增加能力,而是对编程规范设置限制。多态是跨域架构边界的手段;函数式编程是规范和限制数据存放位置与访问权限的手段;结构化是各模块算法实现的基础。
结构化编程
- goto语句的某些用法使的某个模块无法被拆分为更小的单元。
- goto语句的实际效果其实和更简单的:分支结构if-else,循环结构while是一致的
- 顺序结构、分支结构、循环结构可以构造任何程序。
结构化编程可以将模块按功能进行降解拆分,这样一来一个大型问题可以拆分为一系列高级函数的组合,高级函数有可以拆分为各种低级函数的组合。结构化编程范式最有价值的地方就是,它赋予了我们创造可以证伪的程序单元的能力,这样我们只要证明每个单元功能正确就能确保整体程序的正确性。 这也是为什么现代编程语言一般不支持无限制的goto语句的原因,同时也是为什么架构设计领域,功能降解拆分是最佳实践之一的原因。
面向对象编程
什么是面向对象?数据与函数的组合?对真实世界进行建模的方式?封装、继承、多态的有机组合?都不并不准确。
-- 封装:由于面向对象的编程语言为我们方便的封装数据和函数提供了有力的支持,导致封装这个概念经常被引用为面向对象编程的一部分。但实际上这个特性并不是面向对象编程所独有的,C语言也支持完整的封装(使用struct),通过头文件进行数据结构以及函数定义的声明,程序文件中的具体实现细节对使用者不可见,这正是完美的封装。而Java和C#抛弃了头文件与实现文件分离的方式,恰恰消弱了封装性,因为我们无法区分类的声明和定义。
-- 继承:其主要作用是让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖,其实C程序员早就做好了。通过将子结构体作为父结构体的一个超集,并保持共同成员的顺序一致就可以在使用父结构体的地方使用子结构体。实际上C++内部就是这样实现单继承的。从这个方面来讲,面向对象语言虽然在继承方面没有开创出新,但的确在数据结构的伪装性上提供了相当程度的便利性。
-- 多态:同样的,在C语言中也是支持多态的,多态其实不过是函数指针的一种应用。面向对象语言虽然对多态没有理论上的创新,但确实让多态变得更加安全和可用了。
-- 接口与依赖反转
面向对象的本质:面向接口。在没有接口之前,一个高层函数HL调用多种实现的底层函数LL1,LL2,那么在源代码级别,必然需要引入LL1,和LL2的模块(以import的形式或者其他),当有了接口之后,源代码的依赖关系发生变化,HL只需要引入LI的依赖,而LL1,LL2也引入LI的依赖并实现。在源代码级别就实现了HL和LL1,LL2的依赖,而这个接口LI,就是业务逻辑的接口,这就叫做依赖反转。
这种能力有什么用呢?无论我们面对怎样的源代码级别的依赖关系,都可以将其反转。譬如某一个业务模块依赖于数据库模块,通过接口的方式,我们完全可以让数据库模块依赖业务接口,而非相反,这样就可以将数据库模块作为业务逻辑的插件,于是,业务组件就可以独立于数据库来部署了。
所以,面向对象编程是什么?就是以多态的手段对源代码中的依赖关系进行控制的能力,这种能力可以让架构师构建出某种插件式的架构,让高层策略性组件与底层实现型组件分离,底层组件可以编译成插件独立与高层组件进行开发和部署。
函数式编程
函数式编程依赖的原理,很多方面是早于编程本身出现的,这种范式强烈依赖于lamda演算。
-- 变量的可变性:所有的竞争问题,死锁问题,都是由于可变变量导致。一切并发应用遇到的问题,如果没有可变变量就不可能发生。一个架构良好的应用程序应该将状态修改部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制保护可变量。
事件溯源
以账户交易为例,维护客户账户余额信息,当进行存取款事务时,就要同时修改余额记录。另外一种思路,如果我们不修改余额,仅仅保存日志,就可以不用修改任何变量,但是如果要查询余额时,就需要将所有交易记录取出,并进行累计。当然这种设计有些不合理,但是我们并不需要这个设计永远可行,而且在程序的生命周期内,我们有足够的存储和处理能力来满足它。这就是事件溯源,我们值存储事务记录,而不存储具体状态,当需要具体状态时,我们只需要从头开始计算所有事务即可。
这种数据存储模式不存在删除和更新的情况,我们的应用程序不是CRUD,而是CR,自然也不会存在并发的问题。如果我们有足够强大的存储量和处理能力,应用程序就可以用完全不可变的纯函数式的方式来编程。
这听起来可能不太靠谱,但是我们现在使用的源代码管理程序,它们正是用这种方式工作的。
软件架构
-- 软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解,易于修改,方便维护,轻松部署。一个软件的架构质量和该系统能否正常工作的关系并不大,毕竟世界上有很多架构设计糟糕,但是工作正常的软件系统。真正的麻烦并不会在软件运行的过程中出现,而是在系统的开发,部署,以及后续的补充开发中。
企业应用架构模式
1、在对复杂的软件系统进行分解时,使用的最多的就是分层。三个基本层次:数据源层(与数据库,消息系统,事务软件通信),领域层(逻辑,系统真正的核心),表现层(提供服务,显示信息)
优秀框架设计赏
spring中的一些设计理念和模式
TODO
netty中的一些设计理念和模式
异常与错误码的选择
对于错误的处理,包括不能预期的错误和能够预期的错误,甚至是某些不符合条件的问题,都应该首选异常的方式,毕竟异常是一种专用的错误处理取到,而错误码会占用正常的代码逻辑。
异常的一些优势: 1 异常自动向上层调用传播,一旦异常被抛出,执行流会立即终止,取而代之的是自动的stack-unwinding操作,直到找到一个适当的catch子句;而错误码需要在调用处手动检查错误,并手动向上传播,对于调用较深的逻辑,会很复杂。(一个是检查错误,一个是转发或者处理错误)这样进一步会带来的问题是:错误类型多时,手动检查麻烦且容易出错;错误处理逻辑与正常逻辑过度耦合,且错误处理逻辑难以复用。
2 异常的传播使用的是一个单独的信道,而错误代码则占用了函数的返回值;错误码的方式不仅可表示的异常数量变少,而且携带的异常信息也少。而异常可以携带更多更专业的信息。
3 异常是面向对象的。这样在catch处理异常时,异常的处理粒度可以自己控制,可以统一处理,也可以针对处理
4 异常是强类型,错误码是弱类型,这个其实可以归结为强类型的好处,可以防止误判。
5 异常是first-class的语言机制。代码分析工具可以识别出异常并进行各种监测或分析。
所以,异常是一个更高级更专业的错误处理方式。有些质疑说异常可能性能比较差,但这个未经过验证,存疑。
但是没有错误码使用的场景吗?有,我认为错误码应该在业务的边界使用。有些类似与controller返回给用户时的信息。如果在业务边界使用异常的话,需要对方业务识别异常并配备相应处理机制,这可能对其他业务就有侵入了,其他业务系统也不想对你系统的业务异常做详细的处理,所以传递过去的错误要尽量简单。这时异常的意义也就不是很大了,而相对的错误码会更简单一些。
所以在边界处(这个边界包括用户和其他业务系统),内部的异常应该是被处理掉的,并总结成为错误码,传递给其他业务。