Principle,心法胜于招式!
庞大的工程、复杂的项目并非一蹴而就,都是从最简版本开始,通过一次次迭代逐步完善,这其中关键但便在于:小步快跑 + 逐步迭代 -- 每次迭代仅仅实现适当功能,方便及时检验成果,从而降低回炉成本 ( 这么做的根本原因在于实际操作才能真正统一各方目的意图 )。设计上应避免过度设计,够用就好 ( 最小知识,因对未来的考虑混入过多元素和逻辑容易令人困惑和分散精力 ),这方面可以参考一经验准则:未来扩展的修改成本。同时,唯一不变的便是变化,曾经的优势可能已经成为劣势,而曾经的劣势则可能成为优势,持续重构能保持代码和工程质量 ( 这一过程同样可以使用小布快跑 + 逐步重构 )。
仅仅提供需要的功能和体验, 这个在产品体验和接口设计上非常明显,目前没必要的可以先不用提供 ( 但要注意的是并不是说无需考虑 ),以免造成误解,做加法容易做减法难,保持简洁才能更好得去开发维护和运营。我司公众号有个夜间打车功能,在不符合报销时间打车,标签为灰色,功能不可用,这点算不上一个功能上的需求,但却非非常具有自描述性和良好体验,毕竟要是在非规定时间能被唤起功能,一来是容易误操作,二来则是非常具有迷惑性,你说不能用吧但我又可以唤起!
继承有个问题在于继承爆炸 ( 继承产生树形结构,增加一层抽象,便是增加一层高度,导致的后果可想而知,并且会产生大量多余的叶类 ),并且往往会继承一些不要的属性和方法 ( 这就违背了最小知识原则 ),桥接模式就是为了解决这个问题,而这其中的实现便是组合。但事实上,组合能否被很好地落地,还取决于语言的支持程度,在 Java 中,要使用组合,还得再写一段无聊重复的代码 ( 对于 Java,当然也许可以由 Lombok 完成这个过程 ),也尽管 Kotlin 在这面的特性有所优化,但依旧没有 Ruby Mixins 好用,一个类组合了几个类,那么他们的方法就可以通过外部类进行访问,这才是真正意义上好用的组合。
测试是为了保证代码质量,特别是在需求变更、缺陷修复等中涉及的回归测试,而可测试性则强调的是如何让代码方便测试,基于抽象而非实现编程、使用 Setter 或者 Constructor 注入依赖,以及 DI 容器,就能使得代码具备一定的可测试性,同时运用好测试框架如:JUnit、Mockito 则能使 UT 更得心应手。可测试性,本身以来的就是多态 ( 实现或继承 )!
在众多软件质量评判标准,如可扩展性、可维护性、灵活性、易用性等,你会觉得哪个标准最重要?可扩展性?灵活性?我个人觉得是可读性。关键在于未来不可预期,必然会涉及变更,那么此时能读懂代码才是进下来一系列工作的前提。此外,退一万步讲,即使代码质量其他便准不佳,但凡能读懂代码,那么就可以进行重构!
可读性体现在:模块 ( 大到组件系统、小到类库函数 ) 功能职责专一,以及命名简洁直白!
相信大家都有这么一个体验:喜欢一个产品除了满足自身需求,还足够简单好用,而实际上往往易用也本身就是需求之一,要么是这个功能领域中的唯一,要么就是这个功能领域的最优。如设计模式中的外观模式,正是易用性的表现,这个模式很简单,确非常实在!
分层是从垂直维度 ( 即处理链路,技术维度 ) 划分,如:接入层、逻辑层、存储层等;而模块是从水平维度 ( 即功能职责,业务维度 ) 去划分,如:评论模块、用户模块、文章模块。分层与模块间可以相互嵌套 -- 分层中有模块、模块中有分层。分层和模块的核心在于分而治之,形成高内聚低耦合的结构,简化方能集中精力,隔离方能限制影响!而实际上,一切的原则思想和方法都是为了应对复杂性!
关于分层和模块,实际上是很自然的一事情,怎么说呢?分层跟分模块为了实现高内聚低耦合 ( 核心便是分离关注点,根据关注点去分层模块,我们熟知的 MVC 便是如此 ) ,用一句很形象的话来说,就是隔离修改,让修改限制在一个集中地方,这个集中地方体现在系统内部、模块内部、类内部,我们知道一个设计原则:对扩展开放对修改关,对扩展开放很容易理解,但对修改关闭,我以前是这么以为的:就是不修改,但实际上这个对修改关闭实际上仅仅相对而言,对外部类、模块、系统关闭,内部的修改还是必不可少的,哪怕是以扩展闻名的责任链模式,除了需要定义一个新类,也会设计该类对象的创建,那么就是需要而外在原来的类文件中添加的代码 ( 当然,使用 DI 容器可以把这个也省去了 )。
PO 是 Persistant Object 的缩写,用于表示数据库中的一条记录映射成的 java 对象。PO 仅仅用于表示数据,没有任何数据操作。通常遵守 Java Bean 的规范,拥有 Getter / Setter 方法。 DAO 是 Data Access Object 的缩写,用于表示一个数据访问对象。使用 DAO 访问数据库,包括插入、更新、删除、查询等操作,与 PO 一起使用。DAO 一般在持久层,完全封装数据库操作,对外暴露的方法使得上层应用不需要关注数据库相关的任何信息。 VO 是 Value Object 的缩写,用于表示一个与前端进行交互的 java 对象。有的朋友也许有疑问,这里可不可以使用 PO 传递数据?实际上,这里的 VO 只包含前端需要展示的数据即可,对于前端不需要的数据,比如数据创建和修改的时间等字段,出于减少传输数据量大小和保护数据库结构不外泄的目的,不应该在 VO 中体现出来。通常遵守 Java Bean 的规范,拥有 Getter / Setter 方法。 DTO 是 Data Transfer Object 的缩写,用于表示一个数据传输对象。DTO 通常用于不同服务或服务不同分层之间的数据传输。DTO 与 VO 概念相似,并且通常情况下字段也基本一致。但 DTO 与 VO 又有一些不同,这个不同主要是设计理念上的,比如 API 服务需要使用的 DTO 就可能与 VO 存在差异。通常遵守 Java Bean 的规范,拥有 Getter / Setter 方法。 BO 是 Business Object 的缩写,用于表示一个业务对象。BO 包括了业务逻辑,常常封装了对 DAO、RPC 等的调用,可以进行 PO 与 VO / DTO 之间的转换。BO 通常位于业务层,要区别于直接对外提供服务的服务层:BO 提供了基本业务单元的基本业务操作,在设计上属于被服务层业务流程调用的对象,一个业务流程可能需要调用多个 BO 来完成。 POJO 是 Plain Ordinary Java Object 的缩写,表示一个简单 Java 对象。上面说的 PO、VO、DTO 都是典型的 POJO。而 DAO、BO 一般都不是 POJO,只提供一些调用方法。
在传统的 MVC 模式中,Controller 主要关注入参的校验转换、出参的转换渲染,涉及的便是 Request / Response;Service 就是核心业务逻辑,涉及的便是 BO;Repository 则是与存储相关,涉及的便是 PO ( 又或者是 DO ),这便是分层。实际上你也可以简单划分三层,而对于复杂项目则可以分更多层,例如 Facade 层,实际上这层就是外观模式的一个应用场景,但加入该层类个数比较少的话,跟 Service 放在一起也是可以的。
实际上,为什么要定义这些对象、如何去定义、怎样去使用,都随着项目的复杂度而水到渠成,例如,在与外部交互时,会定义两个对象:Inbound / Outbound,通过这两个领域对象 / 中间对象来应对外界的复杂和变化。你可以看到,一来是可以通过这两个对象保持内部的稳定,二来是一旦有什么变化,那么只需要修改这个中间对象即可,否则可以想象得到,一旦直接引用外界对象数据,那么一旦发生变更,那么内部任何一个地方对其的引用都会设计修改。实际上,与外部交互的那一层,就类似于 DDD 中的防腐层,敌不动我不动,敌动我也可以不动。
不变模式最大的优势便是天然的并发安全性,这就意味着充分利用多核优势!
Record Class
以 Kotlin 为例
class Example( val field: String )
以 Java 结合 Lombok 为例
@Getter @AllArgsConstructor(onConstructor_={ @JsonCreator }) public class Example { private final String field; }
在出口层作一层统一的封装,统一显示,同时可以避免泄漏一些敏感信息,如:在进行远程调用的时候,会在异常信息显示出内部 IP 地址等。
语言的生态非常重要,是否有配套的开发框架,测试工具,版本管理,依赖控制等等,尽管可以由三方提供,但在使用和风格上,多少会有些诧异和适配,最直接的一点:可以提供友好完善的 Startup Usage,借由一步步的提示引出一系列的进阶,这点就是关于自描述,我们可以看到现在的命令行都提供了非常的详细的指引,这就是一种自描述!另外一些自描述就是命名和一些约定俗成的规范。
风格这件事情,实际上更多的是体现在多人协作上,统一的风格一个巨大的优势便是可读性,说到这点,实际上对于代码来说,我优先关注的是便是可读性,至于可扩展性、易用之类的,这些都可以通过重构解决,而且软件的生命周期就是跟变更和重构息息相关的,那么能否读懂代码就非常关键了!如:在 Spring JPA 中,Get 用去获取单个 Record,List 用于获取 Records。风格这事情其实在平时中也是常常接触,就如 AEntity、AController、AService、ARepository 等等,项目名也就是在各类监控追踪服务等的名称。
团队分为两种:乌合之众以及精兵强将,再好的管理和方法,乌合之众能完成也仅仅是从无到有,而精兵强将则能在从无到有的基础上进一步做到从有到精,说到底,能否做好一件事情,除了外驱,更重要的是内在驱动力。
本质上就是复用,复用不仅仅体现在代码架构上,也延申到服务数据上。何时引入中台?这也如分层模块般是水到渠成的事情。
设施的完善和便捷,能为开发测试发布以及迭代能节省非常多的时间,例如一些基本的组件类库,测试框架,联调环境。能自动化的一定不要手动去做,特别是重复性有规律的事情,效率和质量没有保障,更应该把精力放在正真需要解决问题上 ( 利用这些工具和设施带来的优势 )。
这大概也是工程和设计需要考虑的问题,实际上就是针对特定场景限定使用。例如:Kotlin 在语法上原声支持 Null 语义,又如 Golang 实现用同步的方式实现异步,异步编码是一个非常考验细节的工作,不停地需要去踩坑,这儿也再次想到一事:实现功能并不难,难的是如何优雅完善地去实现,Code for exception,Design for failure。需要不停得去打磨! Erlang 在异常处理上提供的机制就非常不错!
通过一层映射,将内外两部分通过这中间映射层隔离开来相互独立,使得任何一方的变动都不会影响到另一方,这一点在不同模块 / 系统间交互的时候非常有用,而逻辑和物理间的映射也正是如此,如:Linux 中虚拟地址和内存地址间的映射 ( 保证连续的内存空间 );SSD 中的逻辑块和物理块之间的映射 ( 为了平衡物理块之间的寿命 )。
信息互联的时代,数据爆炸,技术迭代非常快 ( 相信这点你能切身感受到 ),如何能在这洪流中的坚挺下来?持续学习!必要的基础知识,这是解决技术焦虑的根本途径,当你去深究技术本源的时候,你会找到万源归宗的感觉。
最后,软件行业没有银弹,实际上任何行业都没有,所以并不存在一劳永逸的解决办法,我们在软件工程上所做的一切,包括编程语言,包括框架组建,包括原则范式等等,都是为了应对复杂和变化。此外,不同的场景不同的策略。路漫漫其修远兮,吾将上下而求索,共勉!
欢迎关注我们的微信公众号,每天学习Go知识