面向对象设计原则

面向对象设计原则

flavor wheel

高内聚是指相近的功能、行为应该放到同一个组件中。

设计原则

高內聚、低耦合

什么是“高内聚”

高内聚是指相近的功能、行为应该放到同一个组件中。

什么是“低耦合”

组件间的依赖关系清晰,交互不复杂。

面向对象编程、面向对象分析

面向对象编程不是使用面向对象的编程语言进行编程,而是利用多态特性进行编程,面向对象语言真正区别于其他高级语言的地方是多态

框架(frameworks)

框架是用来实现某一类应用的结构性程序,是对某一类架构方案可复用的设计与实现。

架构师通过开发或者维护框架来把控系统的质量。

面向对象设计的基本原则

OOD 原则一:开/闭原则(OCP)

OCP - Open/Closed Principle

  • 对扩展是开放的(Open for extension)
  • 对更改是封闭的(Closed for modification)

不需要修改现有软件实体,就能实现功能扩展

实现不修改而扩展的关键是抽象

DEMO

设计一个控制电话拨号的软件。
“拨打电话”的 Use Case描述:

  • 我们按下数字按钮,屏幕上显示号码,扬声器发出按键音
  • 我们按下Send按钮,系统接通无线网络,同时屏幕上显示正在拨号。

不太优雅的设计

类图

Class Diagram

Demo Code
public class Button {
    public final static int SEND_BUTTON = -99;
    private Dialer dialer;
    private int token;

    public Button(int token, Dialer dialer) {
        this.token = token;
        this.dialer = dialer;
    }

    public void press() {
        switch (token) {
        case 0:
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
        case 6:
        case 7:
        case 8:
        case 9:
            dialer.enterDigit(token);
            break;
        case SEND_BUTTON:
            dialer.dial();
            break;
        default:
            throw new UnsupportedOperationException("unknown button pressed: token=" + token);
        }
    }
}
坏味道
  • 僵硬 - 不易增加、修改:

    • 增加一种Butn类型,就需要对 Button类进行修改;
    • 修改 Dialer,可能会影响 Button。
  • 脆弱- switch case/if elsei语句是相当脆弱的。

    • 当我想修改Send按钮的功能时,有可能不小心破坏数字按钮
    • 当这种函数很多时,我很有可能会漏掉某个函数,或其中的某个条件分支。
  • 不可移植-设想我们要设计密码锁的按钮,它只需要数字按键,但 Button的设计使它必须附带”send”类型的按钮。

改进 Button:方法一

抽象 Button 接口,分别实现 DigitButton 和 SendButton 实现类,分别调用 Dialer 业务方法。Dialer 类内部通过 if-else 判断决定,分别执行 digit/send。

method1

改进 Button:策略模式

抽象 ButtonServer 接口,声明 ButtonServer#buttonPressed(int token) 方法,在 Dialer 中实现该方法,供 Button#process 调用。

stratery

Button 类伪代码

public class Button {
    ...
    public void process(int token) {
        buttonServer.buttonPressed(token);
    }
    ...
}

改进 Button:适配器模式

定义两个 Adepter 类实现 ButtonServer 接口,分别调用 Dialer 类的 digit/send 方法。Dialer 和 ButtonServer 解耦,去除掉了 if-else 逻辑。

adepter

DigitButtonDailerAdepter 类伪代码:

public class DigitbuttonDilerAdepter implements ButtonServer {
    ...

    @Override
    public void buttonPressed(int token) {
        dailer.enterDigit(token);
    }

    ...
}

SendButtonDailerAdepter 类伪代码:

public class SendButtonDailerAdepter implements ButtonServer {
    ...

    @Override
    public void buttonPressed(int token) {
        dailer.dail(token);
    }
    ...
}

在此基础上新的需求:
同一 Button 同时需要触发多个功能。e.g. 点亮电话的灯。

可以通过增加新的 Adepter 来聚合不同的方法。

LampAndDigitButtonDailerAdepter 类伪代码:

public class LampAndDigitButtonDailerAdepter implements ButtonServer {
    ...
    // enterDigit()
    // lamp()
    ...
}

改进 Button:观察者模式

应对一个 button 按钮需要触发多个功能需求。

接口 ButtonServer 修改为 ButtonListener。

Button 类添加 List buttonListeners 字段,添加 addButtonListener(ButtonListener buttonListener) 方法。

Button#process 方法调整为迭代执行 buttonListeners 内所有对象。

Observable

phone 类组装

满足开闭原则,对功能扩展有较好的支持。

Phone 类伪代码:

public class Phone {
    private Dialer dialer;
    private Button[] digitButtons;
    private Button sendButton;

    public Phone() {
        dialer = new Dialer();
        digitButtons = new Button[10];
        for (int i = 0; i < digitButtons.length; i++) {
            digitButtons[i] = new Button();
            final int digit = i;
            digitButtons[i].addListener(new ButtonListener() {
                public void buttonPressed() {
                    dialer.enterDigit(digit);
                }
            });
        }
        sendButton = new Button();
        // 匿名函数定义 Adepter。
        sendButton.addListener(new ButtonListener() {

            public void buttonPressed() {
                dialer.dial();
            }

        });
    }

    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.digitButtons[9].press();
        phone.digitButtons[1].press();
        phone.digitButtons[1].press();
        phone.sendButton.press();
    }

}

OOD原则ニ:依赖倒置原则(DIP)

DIP - Dependency Inversion Principle

高层模块不依赖低层模块,而是依赖抽象。

抽象不依赖实现,而是实现依赖抽象。

DIP 倒置了什么?

  • 模块或者包的依赖关系
  • 开发顺序和职责(高层来定义、来调用)

软件的层次化

  • 高层决定低层
  • 高层被重用

e.g. Controller 层定义接口,自己调用, Service 层负责实现这个接口。

DIP

反例:

unDIP

修改:

DIP-demo

框架的核心

好莱坞原则:

  • Don't call me, I'll call you.

倒转的层次依赖关系。

应用不要调用框架,框架会来调用应用层实现。依赖是反过来的。

ooD原则三: Liskova替换原则(LSP)

子类必须能替换掉基类。

凡是使用基类的地方,一定也适用于其子类。

e.g. 人不能骑马,但是不能骑小马,违反原则。

子类抛出来的异常是父类抛出来的异常的子类,否则基类 catch 不到子类异常。

继承违反原则的时候使用组合(适配器模式)。

combination

如果设计目的不是为了其他类继承的类,最好不要去继承它。

子类不应该比基类语义上更严格。

OOD原则四:单一职责原则(SRP)

SRP - Single Responsibility Principle

一个类只有一个引起它改变的原因。

单个类的职责少一些。

e.g.

x

改进

x

区分类的方法:分清职责

职责: 变化的原因

如果实现类不可拆分,采用接口隔离,分离职责。

OOD原则五:接口分离原则(ISP)

ISP - Interface Segregation Principle

不应该强迫客户程序依赖它们不需要的方法。

客户端看不到不需要的方法。

不暴露给客户端不需要的方法。

不要为了复用方法继承基类。

ISP和SRP的关系

  • ISP和SRP是相关的,都和“内聚性”有关。
  • SRP指出应该如何设计ー一个类ーー只能有一种原因才能促使类发生改变。
  • ISP指出应该如何设计一个接口ーー从客户的需要出发,强调不要让客户看到他们不需要的方法。

总结

OOD 原则

  • 开/闭原则(OCP)
  • 依赖倒置原则(DIP)
  • Liskova替换原则(LSP)
  • 单一职责原则(SRP)
  • 接口分离原则(ISP)