1. 设计原则

1.1. 开闭原则(OPC,Open-Closed Principle)

  • 一个软件实体应当对扩展开放,对修改关闭。

    1.2. 里氏代换原则(LSP,Liskov Substitution Principle)

  • 子类型必须能够替换它们的基类型。反过来的代换不成立。

    1.3. 依赖倒置原则(DIP,Dependence Inversion Principle)

  • 具体要依赖于抽象,抽象不要依赖于具体。里氏替换原则是依赖倒转原则的基础。

    • 抽象不应当依赖于细节;细节应当依赖于抽象
    • 要针对接口编程,不针对实现编程
    • 传递参数,或者在组合聚合关系中,尽量引用层次高的类
    • 在处理类之间的耦合关系时,尽量使用抽象耦合的形式。在构造对象时可以动态的创建各种具体对象,当然如果一些具体类比较稳定,就不必再弄一个抽象类做它的父类,这样有画蛇添足的感觉
  • 三种耦合关系
    • 零耦合关系,如果两个类没有耦合关系,就称之为零耦合
    • 具体耦合,具体耦合发生在两个具体的类之间,经由一个类对另外一个具体类的直接引用造成的
    • 抽象耦合关系,抽象耦合关系发生在一个具体类和一个抽象类之间,使用两个必须发生关系的类之间存在有最大的灵活性
  • 工厂模式、模板模式、迭代子模式都是对依赖倒转原则的体现。

    1.4. 接口隔离原则(ISP,Interface Segregation Principle)

  • 使用多个专门的接口比使用单一的总接口总要好。根据客户需要的不同,而为不同的客户端提供不同的服务是一种应当得到鼓励的做法。就像“看人下菜碟”一样,要看客人是谁,再提供不同档次的饭菜

    • 一个类对另外一个类的依赖性应当是建立在最小接口上的
    • 过于臃肿的接口是对接口的污染
    • 不应该强迫客户依赖于它们不用的方法
    • 定制服务的例子,每一个接口应该是一种角色,不多不少,不干不该干的事,该干的事都要干
  • 接口隔离原则讲的是为同一个角色提供宽、窄不同的接口,以应对不同客户端的需求,

下例以set为例讲解:

  • TreeSet是一种使用树状数据结构的可排序的Set容器,它既实现了Set接口(通过继承AbstractSet),又实现了SortedSet接口。这里并没有提供一个总的既有排序功能又有Set功能的总接口,而是针对不同的需求,将两种角色分别定义成两种接口,这样的设计,是符合接口隔离原则
    • 接口污染:将不同角色的接口合并为一个臃肿的接口就是对接口的污染。这种做法同时违反了可变性封装原则,它将不同的可变性封装到了同一个软件实体中。因此客户程序应该仅仅依赖他们实际需要调用的方法
    • 对接口隔离原则的具体应用可以参考备忘录模式和迭代器模式

      1.5. 合成/聚合复用原则(CARP,Composite/Aggregate Reuse Principle)

  • 就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。简而言之,要尽量使用合成/聚合,尽量不要使用继承。区分Has a和Is a的问题

    • 合成:一荣俱荣,一损俱损,整体和部分的生命周期是一样的。是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分对象的生命周期,合成关系是不能共享的。代表整体的对象需要负责保持部分对象和存活,在一些情况下将负责代表部分的对象湮灭掉。代表整体的对象可以将代表部分的对象传递给另一个对象,由后者负责此对象的生命周期。换言之,代表部分的对象在每一个时刻只能与一个对象发生合成关系,由后者排他地负责生命周期
    • 聚合:部分可以是整体的一部分,也可以脱离整体而存在。例如,汽车类与引擎类、轮胎类,以及其它的零件类之间的关系便整体和个体的关系。聚合关系也是通过实例变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分

      1.6. 迪米特法则(LOD,Law Of Demeter

  • 又叫最少知识原则(Least Knowledge Principle或简写为LKP):一个对象应当对其它对象有尽可能少的了解

    • 只与你直接的朋友们通信
    • 不要跟“陌生人”说话
    • 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位
  • 狭义的迪米特法则

    • 如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用
    • 如果其中的一个类需要调用另外一个类的某一个方法,可以通过第三者转发这个调用

      1.7. 单一职责原则(SRP,Simple responsibility pinciple)

  • 就一个类而言,应该仅有一个引起它变化的原因

  • 如果你能想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。应该把多于的指责分离出去,分别再创建一些类来完成每一个职责

    1.8. 面向对象的五大原则

  • 单一职责原则

  • 开放闭合原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则

    1.9. KISS( Keep It Simple, Stupid)原则

  • 保持简单,愚蠢

    • 编程语言是为人类所理解的,所以保持编码的简单和直接,让人类理解
    • 保证你的方法尽量很小,每个方法都不应该超过40-50行代码
    • 每个方法应该只解决一个小问题,而不是实现很复杂的功能
    • 如果你在方法中有很多条件,把它们分解成更小的单独的方法
  • KISS原则使代码简单、清晰、易于理解。它不仅更易于阅读和维护,而且可以更快地发现bug

    1.10. DRY(Don’t Repeat Yourself)原则

  • 不要重复你自己。这是软件开发的一个基本原则,目的是减少信息的重复,“每一个知识或逻辑必须在一个系统中有一个单一的、明确的表示。”

    • 把你的系统分成几部分
    • 将代码和逻辑划分为更小的可重用单元,并通过在需要的地方调用代码来使用这些单元代码
    • 不要编写过于冗长的方法,要进行逻辑拆分,并尽量使用现有方法中已经写好的逻辑
  • 更少的代码是好的,它节省了时间和精力,易于维护,并且减少了bug的几率

    1.11. 个人感觉

  • 分层解耦

    • 好处:中间增加一个中间层,可以使得上下两层由直接耦合转为间接耦合,公共交互的部分归结在中间层。在保证约定的情况下,可以互不影响进行修改各自的那部分。
      • 传统的controller、service、dao层层解耦,插件型中间件,OSI七层网络模型
    • 坏处:多增加一层,可能会导致数据不一致的情况
      1. - CPU与内存之间的高速缓存,内存与硬盘之间的硬盘缓存
      2. - 服务本地内存缓存与数据持久存储层之间的数据缓存层,近用户侧数据与缓存层之间的服务本地内存缓存层
  • jdk8的接口的默认方法
    • 极大降低了接口增加新方法的成本
  • 抽象类与接口的区别

    • 模板方法使用final修饰,置于抽象类中。定义整体流程规范,不关注具体的实现,具体实现细则交由子类处理
    • 接口是普适性行为的抽象,类是物体(属性和特有行为)的抽象

      2. 架构设计

      2.1. CQRS 查询职责分离

  • Command Query Responsibility Segregation 读写分离

熔断
不让请求,直接返回错误信息
限流
限制部分流量访问
降级
在处理请求时,出现了错误。部分正常部分失败,返回正常数据,失败数据使用默认值

3. 分布式协议/算法

Raft
http://thesecretlivesofdata.com/raft/
Paxios
ZAB

4. 其他资料

服务端高并发分布式架构演进之路

5. 面向对象

5.1. OOD启思录

5.1.1. 原则

*下面的接口既指类实现的接口,也指类提供的公有可被访问的方法

  1. 所有的数据偶读应该隐藏在它所在类的内部
  2. 类的使用者必须依赖类的公有接口,但类不能依赖它的使用者
    • 使用闹钟的人应该依赖于闹钟,而不是闹钟依赖于使用它的人
  3. 尽量减少类的协议中的消息
    • 减少类中的方法,确保在满足功能的情况下提供最少的方法
  4. 实现所有类都理解的最基本公有接口,如拷贝操作、相等性判断
    • 拷贝操作:Java的clone方法,深拷贝和浅拷贝
    • 相等性判断:Java的equals和hashCode方法
    • 上面两个方法,Java都在语言设计层面由Object类默认实现了这两个操作,所有类都默认继承自Object,感受一下继承的强悍之处
  5. 不要把实现细节(例如放置共用代码的私有方法)放到类的公有接口中
    • 这条原则用于为使用者降低类接口的复杂性,类的使用者不应该在公有接口中看到他们不用的成员(属性、行为)
  6. 不要以用户无法使用或不感兴趣的东西扰乱类的公有接口
    • 是对上一条原则的补充
  7. 类之间应该零耦合,或者只有导出耦合关系。也即,一个类要么同另一个类毫无关系,要么只使用另一个类的公有接口中的操作
    • 零耦合,两个类之间完全没有关系。在一个系统中不可能任意两个类之间都是零耦合,那么就不会有应用系统存在了,除非一个类可以完成所有的功能,这几乎是不可能的。当然类库是可以的
    • 导出耦合,一个类依赖于另一个类的公有接口
    • 授权耦合,一个类经允许使用另一个类的实现细节
    • 暗中耦合,一个类X通过某种方式知道了Y的实现细节,如果类X使用类Y的公有数据成员,那么类X就和类Y暗中耦合
  8. 类应当指标是一个关键抽象
  9. 把相关的数据和行为集中放置
  10. 把不相关的信息放在另一个类中(也即,互不沟通的行为
  11. 确保你为之建模的抽象概念是类,而不只是对象扮演的角色
    • 父亲、母亲应该各自是个类,还是人类这个类的一个具体实例呢?取决于为之建模的领域是什么。如果在给定的领域中,父亲和母亲有不同的行为,那么或许应该各自被建模成一个类。母亲类有一个分娩操作,那么也应该为父亲建模成一个类,因为父亲是无法分娩的。
  12. 在水平方向上尽可能统一地分布系统功能,也即:按照设计,顶层类应当统一地共享工作
  13. 在你的系统中不要创建全能类/对象。
  14. 对公共接口中定义了大量访问方法的类多加小心。大量访问方法意味着先关数据和行为没有集中存放。
  15. 对包含太多互不沟通行为的类多加小心。互不沟通的行为是指在类的数据成员的一个真子集上进行操作的方法。全能类经常有很多互不沟通的行为。
  16. 在同用户界面交互的面向对象模型构成的应用程序中,模型不应该依赖于界面,界面则应当依赖于模型。
  17. 尽可能地按照现实世界建模。(我们常常为了遵守系统功能分布原则、避免全能类原则以及集中放置相关数据和行为的原则而违背这条原则)
  18. 从你的设计中去掉不需要的类
    • 不需要的类就是在系统所在领域中没有意义的行为的类。通常来说,只要看一下类中除了set、get和执行输入任务的方法之外还有没有别的操作就鞥判断这个类是否有用了。当我们讨论去除不需要的类时,我们并不是要去除需要的信息,数据。一般来说,我们会把这个类降级成一个属性。
  19. 去除系统外的类(P39)
  20. 不要把操作变成类
    • 考虑一下那个有意义的行为是否应该迁移到已经存在或者尚未发现的某个类中。
  21. 我们在创建应用程序的分析模型时常常引入代理类。在设计阶段,我们常会发现很多代理是没有用的,应当去除

  22. 5.1.2. 其他杂谈

  23. 在面向对象设计中,只要出现了get和set方法,设计者就有必要问自己:“我到底在对这个数据做什么?为什么不是由类来替我做这件事?”

  24. 虽然通过使用把实体类(模型)同他们的行为(方法)分离的方式能够提高实体类和行为的复用程度,但是这一方式违背了数据和行为作为一个概念快双向联系的原则。
    • 基于Spring框架只能使用贫血模型,因为正常情况下大部分Bean都是单例的,而单例Bean要尽可能地避免竞态变量的存在,这样就需要将数据与行为分离,所以每个类都是贫血模型(大多数情况下只用来封装传递数据而不参与对数据的操作
  25. 对OOD中经验原则5.17的思考
    • 原则:在派生类中空方法(也就是什么也不做的方法)来覆写基类中的方法应当是非法的
      • 实际项目
        • 在一个业务处理中,在这个业务处理流程中,有很多个场景都要走这个流程。每个场景都继承某个基类,利用多态的特性,在运行时会调用实际场景类的对应方法,有一个方法在所有的场景下都需要调用。但是现在随着业务的变化,这个方法在一种特殊场景下不能被调用。
        • 解决:将这个方法迁移至基类中,在这个特殊的场景里重写这个方法,然后提供一个空实现。这样就可以解决这个问题
        • 问题:很明显,这样的操作,违反了上面的5.17原则。
        • 思考:应该在基类和这个派生类之上再进行一个抽象,这个抽象的类中提供这个特殊的方法,但是是空实现或者抽象方法,同时将基类中的通用方法,迁移至新的抽象中,这样即可不破坏这个原则
        • 简单粗暴的方式:在调用这个方法之前判断一下当前对象的类型,如果是这个特殊场景,则跳过,否则调用这个方法。Not gracefully!
        • 原则:在策略模式的使用中,需要考虑这种情况,所以在策略类簇之上,如果有无法预知的扩展情况,那么提供一个对应接口的空实现,然后策略类簇继承自这个空实现应该是很有必要的
  26. 对OOD中经验原则5.19的思考
    • 原则:在创建继承层次时,试着创建可服用的框架,而不是可服用的组件。
      • 组件:同系统/应用相关的类,称为组件
      • 框架:一组系统/应用相关的类,称为框架。框架被定义成一个类,引用包含基类
    • 例子
      • 我们希望给操作系统加一个面向对象包装器,以使得在硬件平台间的转换变得更容易一些。有人会执行系统分析并且判断操作系统需要使用哪个操作来满足特定的应用。这些操作就成了基类OperationSystem的纯多态函数(抽象方法)。派生类DOS、Unix和VMS必须各自实现这些方法,才能供应用程序使用。有了这个层次结构之后,就有可能通过创建一个新的派生类O/S2并在这个新操作系统中实现对应的方法,从而实现把我们的应用程序移植到O/S2上去

image.png

  1. - 领域设计:提出这样的问题:“我们能否找到一个可复用框架,更完整地复用组件DOSVMSUnix和新创建的OS/2建模”?我们认识到,所有的操作系统都有文件系统、进程系统、设备驱动系统,所以我们可以把这些派生类泛化。当然会有不同类型的文件系统、进程系统和设备驱动系统,他们都需要有自己的方法。虽然我们没有得到**_可复用的代码_**,但是我们得到了**_可复用的设计_**

image.png