六大设计原则SOLID

一、SOLID

设计模式的六大原则

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则

把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。下面我们来分别看一下这六大设计原则。

二、单一职责原则 (Single Responsibility Principle)

1. 单一职责原则定义

一个类应该只有一个发生变化的原因。

There should never be more than one reason for a class to change.

2. 问题由来

类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

3. 解决方案

遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)

4. 单一职责原则适用于类、接口、方法

单一职责原则简称SRP。 顾名思义,就是一个类只负责一个职责。 那么这个原则有什么用呢, 它让类的职责更加单一。 这样的话, 每个类只需要负责自己的那部分, 类的复杂度就会降低。 如果职责划分的很清楚, 那么代码维护起来也更加容易。 试想如果所有的功能都放在一个类中, 那么这个类就会变得非常臃肿, 而且一旦出现bug, 要在所有代码中去寻找; 更改某一个地方, 可能要改变整个代码的结构, 想想非常可怕。 当然一般时候,没人会去这么写的。

当然,这个原则不仅仅使用于类,对于接口和方法也适用,即一个接口/方法, 只负责一件事,这样的话, 接口就会变得简单,方法中的代码会更少,易读,便于维护。

事实上, 由于一些其他因素的影响, 类的单一职责在项目中是很难保证的。通常,接口和方法的单一职责更容易实现。

5. 单一职责原则的好处

  • 代码的颗粒度降低了, 类的复杂度降低了。
  • 可读性提高了, 每个类的职责都很明确, 可读性自然更好。
  • 可维护性提高了, 可读性提高了。 一旦出现bug, 自然更容易找到问题所在。
  • 改动源码消耗的资源降低了, 更改的风险也降低了。

三、 开闭原则(Open Closed Principle)

1 . 开闭原则定义

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

Software entities like classes, modules and functions should be open for extension but closed for modification.

2. 变化带来的问题

在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改, 可能会给旧代码引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

3. 用开闭原则改善因变化带来的问题

当软件需要变化时, 尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。 其实只要遵循SOLID中的另外5个原则, 设计出来的软件就是符合开闭原则的。

4. 用抽象构建架构, 用实现扩展细节

用抽象构建架构, 用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展, 当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了, 当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。

四、 里氏替换原则

1. 里氏替换原则定义

所有引用基类的地方必须透明地使用其子类的对象

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

定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

定义2:所有引用基类的地方必须能透明地使用其子类的对象。

2. 问题由来

有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

3. 解决方案

里氏替换原则弥补继承的缺陷

里氏替换原则的意思是, 所有基类在的地方, 都可以换成子类, 程序还可以正常的运行。这个原则是与面向对象语言的继承特性密切相关的。
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

在学习java类的继承时,我么知道继承有一些优点:

  • 子类拥有父类的所有属性和方法,从而可以减少创建类的工作量。
  • 提高了代码的重用性。
  • 提高了代码的扩展性, 子类不但拥有了父类的所有功能,还可以添加自己的功能。

但有优点也同样存在缺点

  • 继承是侵入性的。只要继承,就必须拥有父类的方法和属性。
  • 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
  • 增强了耦合性。 当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。

如何扬长避短呢?方法是引入里氏替换原则。

4.里氏替换原则对继承进行了规则上的约束

里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:

  • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(即只能重载不能重写)
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

下面对以上四个含义进行详细的阐述

4.1 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法

在我们做系统设计时, 经常会设计接口或抽象类,然后由子类来实现抽象方法, 这里使用的其实就是里氏替换原则。 若子类不完全对父类的方法进行实例化,那么子类就不能被实例化, 那么这个接口或抽象类就毫无存在的意义。
里氏替换原则规定, 子类不能重写父类已实现的方法。 父类中已实现的方法其实是一种已定好的规范和契约, 如果我们随意的修改了它,那么可能会带来意想不到的错误。 下面举例说明一下子类重写父类方法带来的后果。

public class Father {

    public void fun(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
    }
}

public class Son extends Father {

    @Override
    public void fun(int a, int b) {
        System.out.println(a + "-" + b + "=" + (a - b));
    }
}


public class Client {

    public static void main(String[] args) {
        Father father = new Father();
        father.fun(1, 2);

        // 父类存在的地方,可以用子类替代
        // 子类Son替代父类Father
        System.out.println("子类替代父类后的运行结果");
                Son son = new Son();
                son.fun(1, 2);
    }
}

运行结果

1+2=3
子类替代父类后的运行结果
1-2=-1

我们想要的结果是“1+2=3”。 可以看到,方法重写后结果就不是我们想要的结果了。 也就是这个程序中子类B不能代替父类A。 这违反了里氏替换原则, 从而给程序造成了错误。

我们可以给父类的非抽象(已实现)方法加final修饰, 这样就在语法层面控制了父类的非抽象方法被子类重写而违反里氏替换原则

有时候父类有多个子类(son1,son2),但在这些子类中有一个特例(son2)。 要想满足里氏替换原则,又想满足这个子类的功能时, 有的伙伴可能会修改父类(Father)的方法。 但是, 修改了父类的方法优惠对其他的子类造成影响,产生更多的错误。 这怎么办呢? 我们可以为这个特例(son2)重建一个新的父类(Father2),这个新的父类拥有原来父类的部分功能(Father2并不继承Father, 而是持有Father的引用, 组合Father, 调用Father里的功能), 又有不同的功能。 这样既满足了里氏替换原则, 又满足了这个特例的需求。

4.2 子类中可以增加自己特有的方法

这个很容易理解,子类继承了父类, 拥有了父类和方法, 同时还可以定义自己有, 而父类没有的方法。 这是在继承父类方法的基础上进行功能的扩展, 符合里氏替换原则。

4.3 当子类覆盖或者实现父类的方法时, 方法的重置条件(即方法的形参)要比父类方法的输入参数更宽松

先看一段代码:

public class Father {
    public void fun(HashMap map){
        System.out.println("父类被执行...");
    }
}

public class Son extends Father {
    public void fun(Map map){
        System.out.println("子类被执行...");
    }
}

public class Client {

    public static void main(String[] args) {
        System.out.print("父类的运行结果:");
        Father father=new Father();
        HashMap map=new HashMap();
        father.fun(map);
        
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.print("子类替代父类后的运行结果:");
        Son sun=new Son();
        son.fun(map);
    }
}

运行结果:

父类的运行结果:父类被执行...
子类替代父类后的运行结果:父类被执行...

我们应当注意, 子类并非重写了父类的方法,而是重载了父类的方法。 因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大, 所以当参数输入为HashMap时,只会执行父类的方法,不会执行子类的重载方法。 这符合里氏替换原则。

但当我们将子类方法的参数范围缩小会怎样? 看代码:

public class Father {
    public void fun(Map map){
        System.out.println("父类被执行...");
    }
}

public class Son extends Father {
    public void fun(HashMap map){
        System.out.println("子类被执行...");
    }
}


public class Client {

    public static void main(String[] args) {
        System.out.print("父类的运行结果:");
        Father father=new Father();
        HashMap map=new HashMap();
        father.fun(map);
        
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.print("子类替代父类后的运行结果:");
        Son son=new Son();
        son.fun(map);
    }
}

运行结果:

父类的运行结果:父类被执行...
子类替代父类后的运行结果:子类被执行...

在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。 所以子类中方法的前置条件必须与父类中被重写的方法的前置条件相同或者更宽松。

4.4 当子类的方法实现父类的(抽象)方法时, 方法的后置条件(即方法的返回值)要比父类更加严格

public abstract class Father {
    public abstract Map fun();
}

public class Son extends Father {
    @Override
    public HashMap fun() {
        System.out.println("子类被执行...");
        return null;
    }
}

public class Client {

    public static void main(String[] args) {
        Father father=new Son();
        father.fun();
    }
}

运行结果:

子类被执行...

注意, 是实现父类的抽象方法, 而不是父类的非抽象(已实现)方法, 不然就违反了第一条。

五、迪米特法则(Law of Demeter)

1. 迪米特法则定义

只与你的朋友交谈, 不跟“陌生人”说话

Talk only to your immediate friends and not to strangers.

其含义是: 如果两个软件实体无须直接通信, 那么就不应该发生直接的相互调用。可以通过第三方转发该调用。其目的是降低类直接的耦合度,提高模块的相对独立性。

2. 问题由来

类与类之间的关系越密切,耦合度越大, 当类发生改变时, 对另一个类的影响也越大。

3. 解决方案

迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则会有一下两个优点:

  • 降低了类之间的耦合度,提高了模块的相对独立性。

  • 由于亲和度降低, 从而提高了类的可复用率和系统的扩展性。

自从我们接触编程开始,就知道了软件编程的总的原则:低耦合、高内聚。无论面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量降低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎样编程才能做到低耦合呢?那正是迪米特法则要去完成的。
迪米特法则又叫最少知道原则。最早是在1987年由美国的Northeastern University的lan holland提出。通俗来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。 首先来解释下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式有很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说, 陌生的类最后不要作为局部变量的形式出现在类的内部。

举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。 先来看一个违反迪米特法则的设计。

//总公司员工
class Employee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}
 
//分公司员工
class SubEmployee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}
 
class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//为分公司人员按顺序分配一个ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
}
 
class CompanyManager{
 
	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//为总公司人员按顺序分配一个ID
			emp.setId("总公司"+i);
			list.add(emp);
		}
		return list;
	}
	
	public void printAllEmployee(SubCompanyManager sub){
		List<SubEmployee> list1 = sub.getAllEmployee();
		for(SubEmployee e:list1){
			System.out.println(e.getId());
		}
 
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}
}
 
public class Client{
	public static void main(String[] args){
		CompanyManager e = new CompanyManager();
		e.printAllEmployee(new SubCompanyManager());
	}
}

现在这个设计的主要问题出在CompanyManager中, 根据迪米特法则, 只与直接的朋友发生通信, 而SubEmployee类并不是CompanyManager类的直接朋友,(以局部变量出现的耦合不属于直接朋友), 从逻辑上将总公司只与它的分公司耦合就行了, 与分公司的员工并没有任何关系, 这样的设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现非直接朋友关系的耦合。修改后的代码如下:

class SubCompanyManager{
	private List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//为分公司人员按顺序分配一个ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
	public void printEmployee(){
		List<SubEmployee> list = this.getAllEmployee();
		for(SubEmployee e:list){
			System.out.println(e.getId());
		}
	}
}

 
class CompanyManager{
	private List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//为总公司人员按顺序分配一个ID
			emp.setId("总公司"+i);
			list.add(emp);
		}
		return list;
	}
	
	public void printAllEmployee(SubCompanyManager sub){
		sub.printEmployee();
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}
}
 
public class Client{
	public static void main(String[] args){
		CompanyManager e = new CompanyManager();
		e.printAllEmployee(new SubCompanyManager());
	}
}

修改后, 为分公司增加了打印人员ID的方法, 总公司直接调用来打印,从而避免了与分公司的员工发生耦合。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都要有度,虽然可以闭眼与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生的联系。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

4. 实现方法

从迪米特法则的定义和特点可知,它强调一下两点:

  • 从依赖者的角度来说,只依赖应该依赖的对象。
  • 从被依赖者的角度来说,只暴露应该暴露的方法。

六、接口隔离原则(Interface Segregation Principle)

1. 接口隔离原则定义

  1. 客户端不应该依赖它不需要的接口。
  2. 类之间的依赖关系应该建立在最小的接口上。

Client should not be forced to depend upon interfaces that they don’t use.
The dependency of one class to another one should depend on the smallest possible.

注: 该原则中的接口, 是一个泛泛而言的接口, 不仅仅指java中的接口,还包括其中的抽象类。

其实通俗来理解就是, 不要在一个接口里面放很多的方法,这样会显得这个类很臃肿不堪。接口应该尽量的细化,一个接口对应一个功能模块,同时接口里面的方法应该尽量的少,使接口尽可能的轻便灵活。

接口隔离原则和单一原则都是为了提高类的内聚性、降低他们的耦合性,体现了封装的思想,但两者是不同的:

  • 单一职责原则注重的是职责, 而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要是约束接口, 主要针对抽象和程序整体框架的构建。

2. 问题由来

在接口I中定义多个方法(假如A,B,C,D),类C1通过实现接口I来扩展自己需要的方法A、B;类C2通过实现接口I来扩展自己需要的方法C、D;那么接口I对于类C1和类C2来说不是最小接口,则类C1和类C2必须去实现他们不需要的方法。

2. 解决方案

采用接口隔离原则。将臃肿的接口I拆分为独立的几个接口,类C1和类C2分别与他们需要的接口建立依赖关系。

接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是解耦,从而容易重构。

根据上面的场景,用代码来讲解接口隔离原则的使用。假如有这么一个需求,老师有两个职责,备课和讲课;学生也有两个职责,听课和复习。如果不考虑接口隔离原则,可以这样来实现:

先定义一个接口,用来定义所有的职责:

/**
 * 定义接口I
 */
public interface ILearning {
 
    //备课
    String prepareLessons();
 
    //讲课
    String teach();
 
    //听课
    String study();
 
    //复习
    String review();
}

定义一个教师类,实现上面的接口:

/**
 * 老师类
 */
public class Teacher implements ILearning {
 
    @Override
    public String prepareLessons() {
        return "老师正在备课.......";
    }
 
    @Override
    public String teach() {
        return "老师正在讲课.......";
    }
 
    @Override
    public String study() {
        return null;
    }
 
    @Override
    public String review() {
        return null;
    }
}

定义一个学生类, 实现上面的接口:

/**
 * 学生类
 */
public class Students implements ILearning {
 
    @Override
    public String prepareLessons() {
        return null;
    }
 
    @Override
    public String teach() {
        return null;
    }
 
    @Override
    public String study() {
        return "学生正在听课......";
    }
 
    @Override
    public String review() {
        return "学生正在复习......";
    }
}

运行结果:

老师正在备课.......

老师正在讲课.......

学生正在听课......

学生正在复习......

从结果来看,是没有问题的,但这时老师有意见了:我备好课、讲完课就完事了,你怎么还给我戴两个听课、复习的空帽子???学生也开始喃喃自语了:谁去鸟你备课、讲课!!!

之所以出现这种问题,分析上面的代码,是因为接口定义的太大,不满足客户端依赖的接口尽可能地小这一原则,导致了代码的臃肿。这时我们可以遵循接口隔离原则对上面的代码进行重构,将上面的接口拆分成为更小的和更具体的接口,这样老师和学生只需要知道他们感兴趣的方法。

/**
 * 教师类接口
 */
public interface ITeacher {
 
    //备课
    String prepareLessons();
 
    //讲课
    String teach();
}
 
/**
 * 学生类接口
 */
public interface IStudents {
 
    //听课
    String study();
 
    //复习
    String review();
}

老师和学生分别实现自己感兴趣的接口:

/**
 * 老师类
 */
public class Teacher implements ITeacher {
 
    @Override
    public String prepareLessons() {
        return "老师正在备课.......";
    }
 
    @Override
    public String teach() {
        return "老师正在讲课.......";
    }
}
 
 
/**
 * 学生类
 */
public class Students implements IStudents {
 
    @Override
    public String study() {
        return "学生正在听课......";
    }
 
    @Override
    public String review() {
        return "学生正在复习......";
    }
}

通过对接口这么一分解,老师和学生便可各司其职,愉快的工作了。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。但是,采用接口隔离原则对接口进行约束时要注意,接口尽量小,但要把握好限度。对接口进行细化可以减少代码的臃肿,提供程序的灵活性是不争事实,但是如果接口过小,则会造成接口数量过多,使程序难以维护,反而适得其反。所以一定要适可而止。

七、依赖倒置原则(Dependence Inversion Principle)

1. 依赖倒置原则定义

  1. 上层模块不应该依赖底层模块, 他们都应该依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

High level modules should not depend upon low level modules. Both should depend upon abstraction.
Abstractions should not depend upon details. Details should depend up abstractions.

2. 问题由来

类A直接依赖与类B, A改为依赖类C,则必须通过修改类A的代码来完成。 这种场景下, 类A一般是上层模块, 负责复杂的业务逻辑,类B和类C是低层模块,负责基本的原子操作;假如修改类A, 会给程序带来不必要的风险。

3. 解决方案

将类A改为依赖接口I, 类B和类C各自实现接口I,类A通过接口I间接的与类B和类C发生联系, 则会大大降低修改类A的几率。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。 以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。 在java中, 抽象指的契约,是接口或者抽象类, 细节就是具体的实现类, 使用接口或者抽象类的目的是制定好的规范和而不去设计任何具体的操作, 把展示细节的任务交给他们的实现类去完成。
依赖倒置原则的核心思想是面向接口编程。 我们依旧用给一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。 场景是这样的,母亲给孩子讲故事,只要给她一本书, 她就可以照着书给孩子讲故事了。代码如下:

/* java版本 */
class Book{
	public String getContent(){
		return "很久很久以前有一个阿拉伯的故事……";
	}
}
 
class Mother{
	public void narrate(Book book){
		System.out.println("妈妈开始讲故事");
		System.out.println(book.getContent());
	}
}
 
public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
	}
}
/* C++ 版本*/
class Book{
public:
	string getContent() {
		return "很久很久以前有一个阿拉伯的故事……";
    };
};

class Mather {
public:
    void narrate(Book book){
		cout<< "妈妈开始讲故事" << endl;
		cout<< book.getContent()<< endl;
	};
};

int main ( int argc, char *argv[] )
{
    Mather *mather = new Mather();
    Book book;
    mather->narrate(book);
    return 0;
}				

运行结果:

妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……

运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

    class Newspaper{
    	public String getContent(){
    		return "林书豪38+7领导尼克斯击败湖人……";
    	}
    }
/*c++*/
class Newspaper{
public:
	string getContent() {
		return "林书豪38+7领导尼克斯击败湖人……";
    };
};

这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。

我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

interface IReader{
	public String getContent();
}

Mother类与接口IReader发生了依赖关系, 而Book与Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:

class Newspaper implements IReader {
	public String getContent(){
		return "林书豪17+9助尼克斯击败老鹰……";
	}
}
class Book implements IReader{
	public String getContent(){
		return "很久很久以前有一个阿拉伯的故事……";
	}
}
 
class Mother{
	public void narrate(IReader reader){
		System.out.println("妈妈开始讲故事");
		System.out.println(reader.getContent());
	}
}
 
public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
		mother.narrate(new Newspaper());
	}
}
/* c++版本 */
class IReader{
public :
	virtual string getContent() = 0;
};

class Book : public IReader{
public:
	string getContent() {
		return "很久很久以前有一个阿拉伯的故事……";
    };
};

class Newspaper : public IReader{
public:
	string getContent() {
		return "林书豪38+7领导尼克斯击败湖人……";
    };
};

class Mather {
public:
    void narrate(IReader &ireader){
		cout<< "妈妈开始讲故事" << endl;
		cout<< ireader.getContent()<< endl;
	};
};

int main ( int argc, char *argv[] )
{
    Mather *mather = new Mather();
    Book book;
    mather->narrate(book);
    Newspaper newspaper;
    mather->narrate(newspaper);
    return 0;
}		

运行结果:

妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
妈妈开始讲故事
林书豪17+9助尼克斯击败老鹰……

这样修改后, 无论以后怎么修改Client类, 都不需要再修改Mother类了。 这只是一个简单的例子, 实际情况中, 代表上层模块的Mother类讲负责完成主要的业务逻辑, 一旦需要对它进行修改, 引入错误的风险极大。 所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性, 降低修改程序造成的风险。

采用依赖倒置原则给多人并行开发带来了极大的便利, 比如上例中, 原本Mother类与Book类直接耦合时, Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后则可以同时开工,互不影响, 因为Mother类与Book类一点关系都没有。 参与协作开发的人越多、项目越庞大,采用依赖倒置原则的意义就越重大。
传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递set方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生。
在实际编程中,我们要一般要做到一线三点:

  • 低层模块尽量都要有抽象类或者接口, 或者两者都有。
  • 变量的声明类型尽量是抽象类或接口。
  • 使用继承时遵循里氏替换原则。

依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程, 也就理解了依赖倒置。

附录

本文自以下链接复制、提取并再次加工:
https://blog.csdn.net/ljw124213/article/details/82106840
https://blog.csdn.net/zhengzhb/article/details/7278174
https://blog.csdn.net/zhengzhb/article/details/7281833
https://blog.csdn.net/zhengzhb/article/details/7289269
https://blog.csdn.net/zhengzhb/article/details/7296921
https://blog.csdn.net/zhengzhb/article/details/7296930
https://blog.csdn.net/zhengzhb/article/details/7296944
https://www.jianshu.com/p/3268264ae581
https://www.jianshu.com/p/526a70f24ac5
https://www.jianshu.com/p/55c3482d6e00
https://www.jianshu.com/p/dfcdcd5d9ece
https://www.jianshu.com/p/98761ad06de6
https://www.jianshu.com/p/3232c9891403
https://www.jianshu.com/p/c3ce6762257c

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
面向对象八大设计原则是一组指导面向对象软件设计的原则,也被称为SOLID原则。这些原则旨在提高软件的可维护性、可扩展性和可重用性。下面是对这八大设计原则的介绍: 1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。 2. 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,可以通过添加新的代码来扩展功能。 3. 里氏替换原则(Liskov Substitution Principle,LSP):子类型必须能够替换掉它们的父类型。也就是说,如果一个类是父类的子类,那么它可以在任何使用父类的地方替代父类而不会引发错误。 4. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖它不需要的接口。一个类不应该强迫它的客户端依赖于它们不使用的方法。 5. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。 6. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解。一个类应该只与它的直接朋友进行通信,而不是与陌生的类进行通信。 7. 合成复用原则(Composite Reuse Principle,CRP):尽量使用对象组合而不是继承来达到复用的目的。通过将对象组合在一起,可以灵活地增加新的行为,而不需要修改现有的代码。 8. 优先使用组合而不是继承原则(Prefer Composition Over Inheritance):在设计时,应优先考虑使用对象组合来实现代码的复用和扩展性,而不是过度使用继承。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值