面向对象设计原则

1.0 Design Smells

糟糕设计的症状【Symptoms of Poor Design】

  1. Rigidity – The design is hard to change

    • 僵化性:难于修改
    • 一个改动导致系统其他地方被迫做出修改
  2. Fragility – The design is easy to break

    • 脆弱性:易于遭到破坏
    • 对系统的改动会导致系统中与被改动部分无概念关联的地方出问题
  3. Immobility – The design is hard to reuse

    • 固定性:难以重用
    • 很难将系统分成为若干个组件,以便在其他系统中重用
  4. Viscosity – It is hard to do the right thing

    • 粘滞性:难以做正确的事情
    • 容易出错,不容易正确
  5. Needless Complexity – Overdesign

    • 不必要的复杂:过度设计
    • 设计中包含不具有直接益处的基础结构
    • 设计要基于业务,不是所有的系统都要三层,也不是都要七层
  6. Needless Repetition – Mouse abuse

    • 不必要的重复:滥用复制粘贴
    • 同样的代码出现两次就值得注意,出现三次就必须考虑抽离
  7. Opacity – Disorganized expression

    • 晦涩性:混乱的表达
    • 难于阅读和理解,没有将本来的意图表达清楚
    • 代码即文档,代码混乱导致系统难以维护,高级的语言特性可能导致理解困难

2.0 单一职责原则(Single Responsibility Principle)

单一制职责

A class should have only one reason to change

就一个类而言,应该仅有一个引起它(状态)变化的原因

  • Responsibility = Reason to change
    • 什么是职责?(职责 = 变化的原因)
  • Separate coupled responsibilities into separate classes
    • 一个类只负责一项职责
  • 面相对象的关注点:类的职责

Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class

一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中

2.1 分析:

  • 一个类(或者大到模块,小到方法)承担的职责越多,被复用的可能性越小

  • 在一个类中耦合过多的职责,一个职责改变可能影响其他职责

  • 类的职责:

    • 数据职责:属性
    • 行为职责:方法
  • 单一职责原则是实现高内聚、低耦合的指导方针

2.2 遵循单一职责原则的好处:

  • 可以降低类的复杂度
  • 提高类的可读性
  • 提高类的重用性
  • 提高系统的可维护性
  • 变更引起的风险降低

3.0 开闭原则(Open-Closed Principle)

开闭原则

Software entities (classes, modules, functions, etc.,) should be open for extension, but closed for modification

一个软件实体(如类、模块和函数...)应该对扩展开放,对修改封闭

**
也就是说在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展
**

3.1 分析

  • 软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类
  • Abstraction is the Key(采用抽象化):在不被修改的前提下被扩展
  • 对可变性封装原则(Principle of Encapsulation of Variation, EVP)
    • 要求找到系统的可变因素并将其封装起来
  • In many ways, the OCP is at the heart of object-oriented design

    • OCP是面向对象设计的核心原则
  • $OCP=abstraction+polymorphism+inheritance$

    • OCP原则背后的机制是抽象(abstraction)多态(polymorphism),通过继承(inheritance)方式实现
  • 遵循OCP原则可获得面向对象技术的最大好处

4.0 Liskov替换原则(Liskov Substitution Principle)

Liskov替换原则

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it

4.1 Barbara Liskov 's word

**
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T
**

class T{
    public void run(){
        ...
    }
}

class S extends T{
    @Override
    public void run(){
        ...
    }
}

public class P{
    public static void main(String args[]){
        S o1=new S();
        T o2=new T();

        //原来
        // o1.run();

        //替换为o2,P::main 的功能不发生变化
        o2.run();
    }
}

**
Subtypes must be substitutable for their base types
**

可以用子类型替换基类型,并且不会改变系统的功能

4.2 分析

  • 可以使用子类替换基类,不改变程序的功能

  • 不一定可以使用基类替换子类

  • 里氏代换原则可以通俗表述为:在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类

  • 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中 尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父
    类对象

4.3 实例:


/**
 * 正方形
 * 设置的width or height 会保持一致
 */
public class Square extends Rectangle {
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
} // 破坏了Rectangle的width-height独立性




/**
 * 客户端代码
 */
public class TestLSP {
    public static void main(String[] args) {
        
        // Rectangle rec = new Rectangle();
        //这里用子类替换基类,并不是透明的,会导致系统功能改变
        Rectangle rec = new Square();
        clientOfRectangle(rec); // rec是子类Square的对象
    }
    private static void clientOfRectangle(Rectangle rec) {
        rec.setWidth(4);
        rec.setHeight(5);
        System.out.println(rec.area());
        }
    }
}

/* 解决方案:
 *      让Rectangle 和 Square 都实现Shape接口
 *      在客户端,如果是Shape,则是通用接口
 *              如果是Rectangle,也不会导致出错
 * 
解决方案

当使用继承时,要遵循Liskov替换原则

  • 类B继承类A时,除添加新的方法完成新增功能外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法

  • 父类中已经实现好的方法,实际上是在设定一系列的规范和契约(contract),虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏

  • 继承在给程序设计带来巨大便利的同时,也带来了弊端

    • 父类和子类紧耦合
    • 有时候考虑聚合/组合会更好

4.4 违反LSP原则的一些线索

  1. 派生类中把基类的某些功能去掉了,即派生类能做的事情比基类还少,说明有违反LSP原则的可能
  2. 如果派生类的方法中增加了异常的抛出,也可能会导致派生类对象变得不可替换,从而违反LSP原则

5.0 接口隔离原则(Interface Segregation Principle)

Clients should not be forced to depend upon interfaces that they do not use.

客户程序不应该被强制依赖它不使用的接口

  • When we bundle functions for different clients into one interface/class, we create unnecessary coupling among the clients.

    • 将不同客户所使用的函数捆绑在一个接口/类中,相当于在这些客户间增加了不必要的耦合

    • When one client causes the interface to change, all other clients are forced to recompile.

      • 当一个客户端需要修改接口时,所有其他客户端被迫修改接口
  • Once an interface has gotten too 'fat' it needs to be split into smaller and more specific interfaces so that any clients of the interface will only know about the methods that pertain to them.

    • 一旦一个接口太复杂,则需要将它分割成一些更细小的接口,使用该接口的客户程序仅需知道与之相关的方法即可

5.1 分析

  • 接口隔离原则是指使用多个专门的接口,而不使用单一的总接口

    • 每一个接口承担一种相对独立的角色,角色隔离
    • 接口仅仅提供客户程序需要的行为,即所需的方法
      • 客户程序不需要的行为则隐藏起来,
      • 应当为客户程序提供尽可能小的单独的接口,而不要提供大的总接口
  • 使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好

  • 可以在进行系统设计时采用定制服务的方式,即为不同的客户程序提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为

5.2 ISP的策略:

  • 建立职责单一的接口,不要建立庞大臃肿的接口,接口
    中的方法尽量少
  • 接口的大小要适度,设计得过大或过小都不好

5.3 遵循ISP的好处

  • 通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性

6.0 依赖倒置原则(Dependency Inversion Principle)

依赖倒置原则

三种表述方式:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

高层模块不应该依赖于低层模块,二者都应该 依赖于抽象

Abstractions should not depend upon details. Details should depend upon abstractions.

抽象不应该依赖细节; 细节应该依赖抽象

Program to an interface, not an implementation

针对接口编程,不针对实现编程

6.1 分层

“…all well-structured object-oriented architectures have clearly defined layers, with each layer providing some coherent[1] set of services through a well-defined and controlled interface…”

面向对象结构定义了层的概念, 每个层通过定义良好和可控的接口对外提供服务

  • Dependency Inversion: Lower-level layers is dependent upon upper-level layers.

    • 依赖的倒置:低层依赖上层
  • Ownership Inversion: The client (upper-level layer) owns the interface, not the lower-level layers

    • 所有权的倒置:上层拥有接口,而不是低层
    • 上层拥有关于下层的接口,也就是下层的服务描述
    • ssm开发中,service 层接口其实属于controller层,serviceImpl 的实现依赖于高层的实现,也就是serviceImpl 依赖于service接口

生活中,对于一条电线(高层),我们只需要知道是三角还是两级的,然后找到对应的插座,插上去就好了,插座内部的实现就是底层。这里三角插口就是一种规范(接口),是对插座提供电源(底层服务)的描述。这时候电线和三角规范是耦合的,三角规范和三角插座是耦合的,电线和三角插孔是通过三角规范耦合在一起的,拿掉这个三角规范,电线和三角插座就毫无关系。

6.2 遵循DIP原则的启发式(heuristic)做法:

  • Depend on abstractions
    • 依赖抽象
    • Do not depend on a concrete class – that all relationships in a program should terminate on an abstract class or an interface
      • No variable should hold a pointer or reference to a concrete class
        • 任何变量都不应该直接引用具体的类
      • No class should derive from a concrete class.
        • 没有任何类派生自一个具体类
      • No method should override an implemented method of any of its base classes.
        • 没有任何方法重写在基类中已实现的方法

6.3 分析

  • 依赖于抽象,而不依赖于具体的类;针对接口或抽象类编程,而不是针对具体类编程

  • 实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说 开闭原则是面向对象设计的目标 的话,那么 依赖倒置原则就是面向对象设计的主要手段

  • 依赖倒置原则的常用实现方式之一是在代码中使用抽象类,而将具体类的连接放在配置文件中

  • 依赖倒置原则要求客户类依赖于抽象耦合,以抽象方式耦合是依赖倒置原则的关键

    • 类之间的耦合有:
      • 零耦合关系
      • 具体耦合关系
      • 抽象耦合关系

6.4 面相过程vs面相对象

Traditional structural programming creates adependency structure in which policies[2] depend on details.

  • 传统的面相结构编程,策略依赖于细节
  • Policies become vulnerable[3] to changes in the details
    • 直接修改细节,策略将变得非常脆弱

Object-orientation enables to invert the dependency:

  • Policy and details depend on abstractions.
    • 策略和细节依赖于抽象
  • Service interfaces are owned by their clients.
    • 客户端拥有服务接口
  • Inversion of dependency is the hallmark of good object-oriented design.
    • 依赖倒置是面向对象设计的标志

6.5 DIP原则背后的原理:

Good software designs are structured into modules

好的软件设计是模块化的

  • High-level modules contain the important policy decisions and business models of an application – The identity of the application.
    • 高层模块包含应用的重要策略决策和业务模型 — 该应用的定义
  • Low-level modules contain detailed implementations of individual mechanisms needed to realize the policy.
    • 低层模块包含为实现这些策略所需要的个体机制的具体实现
  • High-level Policy
    • 高层的策略(可理解为业务逻辑,Business Logic)
    • The abstraction that underlies the application;
    • The truth that does not vary when details are changed;
    • The system inside the system;
    • The metaphor[4]

7.0 合成复用原则(Composite Reuse Principle)

Favor composition of objects over inheritance as a reuse mechanism

尽量使用对象组合,而不是继承来达到复用的目的

  • 在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分
  • 新对象通过委派调用已有对象的方法达到复用其已有功能的目的

简言之:要尽量使用组合/聚合关系,少用继承

也称为:组合/聚合复用原则:

Composition / Aggregate Reuse Principle, CARP

分析

面向对象设计-复用

  • 继承
    • 实现简单,易于扩展。
    • 破坏系统的封装性,父类对子类是可见的
    • 从基类继承而来的实现是静态的,不可能在运行时发生改变
    • 没有足够的灵活性;只能在有限的环境中使用。(白箱复用[5]
  • 组合/聚合
    • 耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(黑箱复用 )
    • 组合/聚合可以使系统更加灵活,类之间低耦合度

选择:

  • 首选组合/聚合,其次才考虑继承
  • 在使用继承时,需要严格遵循Liskov替换原则,有效使用继承会有助于对问题的理解,降低复杂度,
  • 滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用

8.0 Demeter法则(Law of Demeter)

Don't talk to strangers

不要和“陌生人”说话

Talk only to your immediate friends

只与你的直接朋友通信

Each unit should have only limited knowledge about other units: only units "closely" related to the current unit

每一个软件单元对其他的单元都只有最少的知识,而且局限于那些与本单元密切相关的软件单元

also called: 最少知识原则(Least Knowledge Principle)

Demeter法则来自于1987年秋美国东北大学(Northeastern University)一个名为“Demeter” 的研究项目

简单地说,Demeter法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用
这样,当一个模块修改时,就会尽量少地影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求 限制软件实体之间通信的宽度和深度

分析

在Demeter法则中,朋友包括:

  • 对象本身(this)
  • 以参数形式传入到当前对象方法中的对象
  • 当前对象的成员对象
  • 如果当前对象的成员对象是一个集合,那么集合中的元素也都朋友
  • 当前对象所创建的对象

Demeter法则可分为 狭义法则和广义法则

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

  • 可以降低类之间的耦合
  • 使一个系统的局部设计简化,每一个局部都不会和远距离的对象有直接的关联
  • 但是会在系统中增加大量的小方法并散落在系统的各个角落
  • 造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调

广义的Demeter法则:指对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制

  • 信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用和修改,同时可以促进软件的复用,
  • 由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。信息不会流出本模块
  • 一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显

Demeter法则的主要用途在于控制信息的过载

  • 类的划分:尽量创建松耦合的类
    • 类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及
  • 类的结构设计:尽量降低其成员变量和成员函数的访问权限
  • 类的设计:尽量设计成不变类,则信息不会导致状态的改变
  • 类的引用:尽量减少对其他对象的引用

总结

对于面向对象的软件系统设计来说,在支持可维护性的同时,需要提高系统的可复用性

软件的复用可提高软件开发效率,提高软件质量,节约开发成本,恰当的复用还可改善系统的可维护性

  • 单一职责原则:一个类只负责一个功能领域中的相应职责
  • 开闭原则对扩展开放,对修改关闭
  • Liskov替换原则如果能够使用基类对象,那么一定能够使用其子类对象
  • 接口隔离原则接口按照职能细分,客户端只使用最小接口,客户端不需要的功能对客户端隔离
  • 依赖倒置原则依赖于抽象;针对接口编程
  • 合成复用原则复用时尽量使用组合,慎用继承
  • Demeter法则一个软件实体应当尽可能少的与其他实体发生相互作用

参考:敏捷软件开发:原则、模式与实践


  1. coherent:adj. 一致的,条理清晰的

  2. policies: n. 策略

  3. vulnerable: adj. 脆弱

  4. metaphor:n.隐喻

  5. 白箱复用:B extends A,B可以看到A中的细节,一般是public 继承

    黑箱复用:B 通过组合或聚合复用A功能

    两者的区别在于复用是否了解被复用者的内部细节

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容