在进行软件架构工作时,需要遵循面向对象的设计原则,注意体会这些原则在解决“变化”和“依赖”问题中的效果。前人共总结出7条通用的原则(降低耦合性,提高扩展性):

  • 单一职责原则(SRP);
  • 里氏替换原则(LSP);
  • 依赖注入原则(DIP);
  • 接口分离原则(ISP);
  • 迪米特原则(LOD);
  • 开闭原则(OCP);
  • 优先使用组合而不是继承原则;

1. 单一职责原则(Single Responsibilty Principle,SRP)

系统中的每一个对象只用于完成一种职责,即“高内聚,低耦合”;对外只提供一种功能,引起类变化的原因应该只有一个(并不是说只有一个变量,而是只有一个标准);

一般情况下,我们在设计一个类的时候会把与该类有关的操作(属性和方法)都组合到这个类中。这样做的后果就是讲这个类中的某些职责会“耦合”在一起,在想要改变一个职责时,难免其他的职责不会受到影响,造成了程序的“僵硬”;

但是,如果每一个职责都单独写出一个类,无疑效率会大大降低,因此,运用单一职责原则是为了将能够“引起类变化的职责”提取出来封装,做到各司其职,不会因为一个类中的某一个职责发生了变化,要将整个类重写;

例如,在设计接口时,将属性和行为定义在一起,会造成业务对象和业务逻辑这两种职责的混合,违背了单一职责原则(SRP)

public interface Tank{
    void setSpeed(int speed);//属性
    void shoot(Bullet bullet);//行为
}

在这种情况下,如果setSpeed()实现方法需要改变,而接口中的多个职责之间存在“耦合”,可能变化的不仅仅是接口中setSpeed()方法,shoot()行为也会变化。这在逻辑简单的时候还可以解决,如果这两个方法很复杂,几百行代码,还有多线程,修改起来会非常麻烦。因为需要使用单一职责原则“解耦”

public interface TankSpeed{
    void setSpeed(int speed);
}

public interface TankShoot{
    void shoot(Bullet bullet);
}

总结:
1. 一个合理的类,应该只有一个引起它变化的原因,即单一职责;
2. 在没有变化征兆的情况下去使用SRP是不明智的;
3. 在需求发生变化时,应该应用SRP原则重构代码;

2. 里氏替换原则(Liskov Substitution Principle,LSP)

用于规范使用继承:在任何父类出现的地方都可以用它的子类替代。也就是说:父类替换为子类不会出现错误或者异常,但是子类替换为父类就会出现问题。要达到这个目标,需要遵从以下4点:
1. 子类必须完全实现父类的方法;
2. 子类可以扩展自己的特性;
3. 覆盖或者实现父类的方法时的形参范围不能缩小;
4. 覆盖或者实现父类的方法时的输出结果可以被缩小;

其实LSP的意思就是使用继承时,子类需要更具体,同时需要更宽容(可以接受的形参范围更大);

3. 依赖反转原则(Dependence Inversion Principle,DIP)也叫依赖注入原则

用于规范依赖关系:要依赖抽象类,不要依赖于具体的实现。因为抽象的类具有更高的可复用性和可维护性;也就是要正对接口编程,不要针对实现编程;

抽象指的是抽象类或者接口,不能被实例化;
1. 高层模块不应该依赖低层模块,两者都应该依赖于抽象;
2. 抽象不应该依赖于具体实现;
3. 具体实现应该依赖于抽象;

目的是为了实现模块间的松耦合,这个原则也是几个设计原则中最难实现的,例如开闭原则(对扩展开放,对修改关闭)就是建立在这个原则基础上的;那么如何实现注入反转原则呢?
1. 通过构造函数传递依赖对象(抽象类或者接口);
2. 通过setXXX方法传递依赖对象;
3. 通过声明接口,实现依赖对象;

例如需求是实现一辆坦克发射出一定速度的子弹:

public class Tank{
    void shoot(Bullet bullet){
        bullet.speed();
    }
}

public class Bullet{
    void speed(){
        System.out.println("Shoot bullet in Speed");
    }
}

//场景类
public class Battle{
    public static void main(String[] args){
        Tank myTank = new Tank();
        Bullet myBullet = new Bullet();
        myTank.shoot(myBullet);
    }
}

这里,Tank和Bullet两个类之间的耦合性就太高了,Tank只能发射Bullet,以后需求多了,不能喷火,不能加导弹。。。因此,需要降低两者之间的耦合性:

public interface Tank{
    void shoot(Weapon wp);//只要是个weapon都能使用。。。太强大了
}

public Interface Weapon{
    void speed();//可以任意设定速度,甚至可以加入变化的速度
}

//下面就是通过这两个借口去实现具体的MyTank类和MyWeapon类,满足需求了。。。。省略
  • 当实现需求时,通过实现接口就可以获得更多的功能;同时,两个类之间的耦合性也大大降低,因为高层模块(Tank)不依赖低层模块(Weapon)了!高层模块根本不care低层模块是什么,怎么实现(实际上,也不可能实现,因为此时的低层模块是个借口);低层模块也不需要知道高层模块干了什么。
  • 也就是说,通过”注入”了中间层接口,使得模块之间有了缓冲,耦合性大大降低;

总结:依赖注入原则就是通过在实现类之前注入抽象类或者接口,将接口作为实现类之间的”解耦”桥梁。

4. 接口分离原则(Interface Segregation Principle,ISP)

这里的借口不仅包括类接口(通过interface关键字定义的接口),类也被当做是一种借口—相对于实例对象来说的一个借口;

ISP原则和SRP原则有些类似,不同之处在于:
单一职责原则要求的是类和接口的职责单一,是从业务逻辑上的划分;而接口分离原则要求的是接口中的方法尽量少,从而避免强迫实现接口中不需要使用的方法。

  • 接口尽量小;但是也有限度,不然系统复杂度会剧增;
  • 接口高内聚,即接口内部的方法之间都与某一个子模块相关;对外则尽可能隔离;

5. 迪米特原则(Law of Demeter,LOD)

一个对象应该对其他对象尽可能少地了解;也就是说,当第一个类希望去访问第三个类中的数据时,如果存在第二个类,既与第一个类关系紧密,又和第三个类关系紧密,那么没有必要第一个类和第三个类之间再相互了解,而是应该通过第二个类。

  1. 在类的划分上,应该创建有弱耦合的类;
  2. 在类的结构设计上,应该尽量降低成员的访问权限,尽量不让别的类了解你,提供访问器来访问;同时尽量降低类的访问权限,一个道理;
  3. 只要有可能,一个类应该设计成不变类—没有依赖关系;
  4. 尽量少地引用其他对象;

6. 开闭原则(Open for Extension,Close for Modification,OCP)

对类的改动是通过增加代码进行的,而不是通过修改原有的代码进行的—借助于抽象,继承和多态

其实开闭原则是前面5种原则的一个抽象总结,要想实现开闭原则,还需要靠前面5个原则去完成。