面向对象设计原则

我第一次学习到这里是也很吃惊: OOP竟然还有设计原则吗? 老师没有讲过呀. 然后很认真地学习了相关的知识, 可以说这些设计原则对我有很大的启发. 所以特地把学习笔记再总结一遍, 希望自己能牢记这几条原则.

开闭原则

开闭原则指的是类应该对扩展开放, 对修改关闭, 这条原则是面向对象设计是否正确最基本的原理之一. 这个原则可能过于抽象, 不好理解. 在我的理解下, 翻译成大白话就是: 在合理地设计下尽可能以继承的方式实现新功能, 旧类本身的代码不需要变动. 比如下面的例子:

Example1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void EatApple(){
System.out.println("吃苹果");
}

public void EatBanana(){
System.out.println("吃香蕉");
}

public void Eat(List<String> fruits){
for(int i=0; i<fruits.size(); i++){
if(fruits.get(i) == "apple"){
EatApple();
}
else if(fruits.get(i) == "banana"){
EatBanana();
}
}
}

现在要添加吃橘子, 那不仅要写EatOrange()这个函数, 还要在Eat()函数里添加新的if分支. 这就违反了开闭原则. 而如果写成下面这样:

Example2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Fruit{
void Eat();
}

class Apple extends Fruit{
void Eat(){
System.out.println("吃苹果");
}
}

class Banana extends Fruit{
void Eat(){
System.out.println("吃苹果");
}
}

public void EatFruit(List<Fruit> fruits){
for(int i=0; i<fruits.size(); i++){
fruits.get(i).Eat();
}
}

添加吃橘子时, 只需要重写Eat函数即可, 不需要对原有的EatFruit()函数进行改动.

里氏替换原则

里氏替换规则指所有引用基类的地方必须能透明地使用其派生类的对象. 也就是说, 如果一个类有子类, 那么将这个类替换为它的子类后, 程序也应该正常工作.

这个原则可以换句话来理解: 子类可以扩展父类的功能, 但不能改变父类原有的功能. 实际上, 这条规则和开闭原则有些相似, 但开闭原则又比它更严格, 所以可以说替换规则是开闭规则的前提.

其实我觉得不大有人会违反这条规则……不过笔记还是要记. 既然子类可以扩展父类的功能, 也就是说子类不仅可以有自己独特的函数, 在重载或重写父类的函数时, 参数的要求应该比父类更宽松;但是相对地, 返回值要比父类更严格.

单一职责原则

单一职责原则指的是, 一个类应该只有一个引起它变化的原因. 用大白话说就是, 一个类中的成员变量和方法尽量只服务于同一个功能. 这个原则的意义在于, 它可以让类的代码尽可能的简单, 不会产生代码耦合的问题. 我对这一点体会特别深. 在写我的第一个真正意义上的app的时候, 我就经常把一个类写的很臃肿, 结果就是每次再改代码的时候都要理很长时间的逻辑, 然后还得在代码堆中找到要改的那部分, 真的很痛苦.

其实我觉得, 单一职责原则不只用于类的设计, 也用于方法的设计. 当一个方法内容太多太复杂的时候, 这个方法也应该拆分.

依赖倒置原则

依赖倒置原则有三个要求:

  1. 高层模块不应该依赖底层模块, 二者都应该依赖其抽象

  2. 抽象不应该依赖具体

  3. 具体应该依赖抽象

我觉得像Java和C#这样面向对象的语言是最能体现这条原则的. 在Java中, 抽象就是抽象类和接口, 具体就是各种实例类. 它告诉我们在编程时要面向接口、面向抽象类变成, 而不是说直接把两个实体类联系在一起.

大家可以再回味一下上面吃水果的代码, 那就是一个依赖倒置原则的实现例子. 我们面向Fruit这个抽象类进行编程, 结果就是我们程序的扩展性增加了, 但代码维护的工作量又没怎么变.

接口隔离原则

接口隔离原则指的是, 每一个类不因该依赖它不需要的接口. 听起来像是废话, 但是再翻译成大白话就很有道理了: 当一个类实现一个接口的时候, 有一个实现的方法为什么都不做, 那就违反了接口隔离原则.

比如说我们继续吃水果. 设计一个如下的接口:

Example3
1
2
3
4
5
6
public interface WayOfEat{

void OpenMouth();

void Chew();
}

我们吃水果都得张嘴和咀嚼对吧? 但是问题来了, 香蕉在张嘴前还得剥皮, 苹果在张嘴前还得洗干净, 我们要把剥皮和洗干净的函数写进接口里吗? 不能, 因为苹果不用剥皮, 香蕉不用洗. 如果写进去, 那苹果在实现这个接口的时候就用不到剥皮这个函数, 同样香蕉也不会用到洗这个函数, 这就违反了接口隔离原则. 正确的做法是写两个接口, 一个接口专门管不用剥皮吃的水果, 另外一个接口专门管必须剥皮吃的水果.

最少知识原则

最少知识原则指的是, 一个类应该尽量少地接触其它类, 这样就可以最大程度地减少类之间的耦合. 也就是说, 当我们修改一个类时, 受到影响的其它类的数量最少, 自然我们维护代码的工作量最最小化了.

这里我们还以吃水果作为例子. 吃水果之前肯定要买水果, 于是我们来到了水果摊前. 假设我们只买苹果, 并且我们只关心苹果的味道和价钱, 那么可以写出下面的代码:

Example4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Apple{
public float price;
public int flavor;
}

public class SaleMan{
public List<Apple> apples;

public List<Apple> getApples{
return apples;
}
}

public class Customer{
private float ideal_price;
private int ideal_flavor;

public void buy(){
List<Apple> apples = saleman.getApples();
for(Apple apple : apples){
if(apple.price <= ideal_price && apple.flavor >= ideal_flavor){
System.out.println("买到苹果了!");
break
}
}
System.out.println("没买到苹果!");
}
}

上面的代码简单易懂. 我从水果商贩那里拿到一堆苹果, 然后开始挑, 挑到好的我就买. 虽然很符合我们日常买东西的场景, 但是其实并不符合我们最大的利益. 作为买家, 我们肯定希望当一个“甩手掌柜”, 只要告诉老板我们要买苹果, 老板就会主动给我们挑水果, 我们只需要掏钱就可以了. 这就是最少知识原则: 作为买家, 挑苹果的事儿不是我们应该做的, 是老板应该做的. 所以应该写成下面的样子:

Example5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Apple{
public float price;
public int flavor;
}

public class SaleMan{
public List<Apple> apples;

public boolean Pick(float ideal_price, int ideal_flavor){
for(Apple apple : apples){
if(apple.price <= ideal_price && apple.flavor >= ideal_flavor){
return true;
}
}
return false;
}
}

public class Customer{
private float ideal_price;
private int ideal_flavor;

public void buy(){
saleman.Pick(ideal_price, idea_flacor) ?
System.out.println("买到苹果了!") : System.out.println("没买到苹果!");
}
}

像上面这样, 我们只需要告诉老板我们的要求, 别的就不用管了. Apple类再变化和我也没有关系, 只有必须和它有关系的老板受到影响, 这就是虽少知识原则.