六大基本原则
为什么要使用设计模式?根本原因还是软件开发要实现可维护、可扩展,就必须尽量复用代码,并且降低代码的耦合度。设计模式主要是基于OOP编程提炼的,它基于以下六大基本原则:
1. 单一职责原则
-
对象不应承担太多功能,正如一心不能二用。比如太多的工作(种类)会使人崩溃。唯有专注才能保证对象的高内聚;唯有唯一,才能保证对象的细粒度。
简单来说,就是职责尽量单一。
-
客户端不应该依赖它不需要的接口
-
类间的依赖关系应该建立在最小的接口上
通俗理解就是,不要再一个接口里放很多的方法,这样会显得这个类很臃肿。接口尽量细化,一个接口对应一个功能模块,同时接口里面的放荡发应该尽可能少,使接口更加灵活轻便。
-
单一职责原则 与 接口隔离原则 的区别:
-
单一职责原则是在业务逻辑上的划分,注重的是职责
-
接口隔离原则是基于接口设计考虑。
例如,一个接口的职责包含 10 个方法,这 10 个方法都放在同一个接口中,并提供给多个模块调用,但不同的模块需要依赖的方法是不一样的,这时模块为了实现自己的功能就不得不实现一些对其没有意义的方法,这样的设计不符合接口隔离原则。接口隔离原则要求 ” 尽量使用多个专门的接口 “ 专门提供给不同的模块
-
3. 依赖倒置原则
-
高层模块不应该依赖底层模块,二者都应该依赖抽象。
-
抽象不应该依赖细节,细节应该依赖抽象。
依赖倒置中心思想是:面向接口编程。
依赖倒置基本原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比细节为基础搭建的架构要稳定的多。
使用接口或抽象类的目的是指定好规范,而不涉及任何具体操作,把展现细节的任务交给他们的实现类来完成。
每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块(一般是接口,抽象类),原子逻辑的组装就是高层模块。在 Java 语言中,抽象就是指接口或抽象类,两者都不能被直接实例化。细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化。下面是依赖倒置原则在 Java 语言中的表现:
模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
接口或抽象类不依赖于实现类
实现类依赖接口或抽象类
案例及优点
场景一:一个爱学习的同学现在正在学习「设计模式」和「Java」的课程
1 public class yilaidaozhi { 2 public static void main(String[] args) { 3 // 上层调用 4 LAOMA_STUDY laoma = new LAOMA_STUDY(); 5 laoma.setName("老马"); 6 laoma.studyJava(); 7 laoma.studyDesignPattern(); 8 9 } 10 } 11 12 class LAOMA_STUDY { 13 private String name; 14 15 public void setName(String name) { 16 this.name = name; 17 } 18 19 public void studyJava() { 20 System.out.println(this.name + "正在学习Java课程"); 21 } 22 public void studyDesignPattern() { 23 System.out.println(this.name + "正在学习设计模式课程"); 24 } 25 }
场景二:继续上面的场景,但是课程更新了,新增了「数据结构」课程
这时,因为业务扩展了,要从底层实现到高层调用依次的修改代码。
1 public class yilaidaozhi { 2 public static void main(String[] args) { 3 // 上层调用 4 LAOMA_STUDY laoma = new LAOMA_STUDY(); 5 laoma.setName("老马"); 6 laoma.studyJava(); 7 laoma.studyDesignPattern(); 8 // 新增 9 laoma.studyDataStructure+(); 10 } 11 } 12 13 class LAOMA_STUDY { 14 private String name; 15 16 public void setName(String name) { 17 this.name = name; 18 } 19 20 public void studyJava() { 21 System.out.println(this.name + "正在学习Java课程"); 22 } 23 public void studyDesignPattern() { 24 System.out.println(this.name + "正在学习设计模式课程"); 25 } 26 // 新增 27 public void studyDataStructure() { 28 System.out.println(this.name + "正在学习数据结构课程"); 29 } 30 31 }
依赖倒置原则的好处
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
-
有效控制影响范围
我们需要在
LAOMA_STUDY
类中加入studyDataStructure
方法,也需要在高层调用中增加调用,这样一来,系统发布后,其实是不稳定的。显然在这个简单的例子中,影响不是很明显,只是新增了几行代码而已,但是在复杂的软件开发中,就不仅仅是几行代码这么简单。最理想的情况就是,我们编写的代码可以“万年不变”,这就意味着已经覆盖的单元测试可以不用修改,已经存在的行为保持不变,即意味着「稳定」。任何代码上的修改带来的影响都是未知的风险,不论看上去有多么的简单。
-
增强代码可读性和可维护性
观察场景二,增加的「数据结构」课程其实和其他两种课程本质上行为都是一样的,如果我们任由这样的行为近乎一样的代码在我们的类里面肆意扩展的话,很快我们的类就会变的臃肿不堪,等到我们意识到不得不重构这个类以缓解这样的情况的时候,或许成本已经变的高的可怕了。
-
降低耦合度
《资本论》中有这样一段描述:
在商品经济的萌芽时期,出现了物物交换。假设你要买一个 iPhone,卖 iPhone 的老板让你拿一头猪跟他换,可是你并没有养猪,你只会编程。所以你找到一位养猪户,说给他做一个养猪的 APP 来换他一头猪,他说换猪可以,但是得用一条金项链来换…
所以这里就出现了一连串的对象依赖,从而造成了严重的耦合灾难。解决这个问题的最好的办法就是,买卖双发都依赖于抽象——也就是货币——来进行交换,这样一来耦合度就大为降低了。
如何修改?
上面我们总结了 依赖倒置原则 的好处,接下来我们要如何修改上面的代码呢?
1 public class yilaidaozhi02 { 2 public static void main(String[] args) { 3 // 4.高层调用 4 LAOMA_STUDY02 laoma02 = new LAOMA_STUDY02(); 5 laoma02.study(new JavaCourse("老马")); 6 laoma02.study(new DesignPatternCourse("老马")); 7 // 6.新增调用「数据结构」课程的方法 8 laoma02.study(new DataStructureCourse("老马")); 9 10 } 11 } 12 13 // 1.定义一个抽象 ICourse 接口 14 interface ICourse { 15 void study(); 16 } 17 18 // 2.分别为 JavaCourse 和 DesignPatternCourse 编写一个实现类 19 class JavaCourse implements ICourse { 20 private String name; 21 // 定义构造方法 22 public JavaCourse(String name) { 23 this.name = name; 24 } 25 @Override 26 public void study() { 27 System.out.println(name + "正在学习Java课程"); 28 } 29 } 30 class DesignPatternCourse implements ICourse { 31 private String name; 32 // 定义构造方法 33 public DesignPatternCourse(String name) { 34 this.name = name; 35 } 36 @Override 37 public void study() { 38 System.out.println(name + "正在学习设计模式课程"); 39 } 40 } 41 42 // 5. 新增「数据结构」课程 43 class DataStructureCourse implements ICourse { 44 private String name; 45 // 定义构造方法 46 public DataStructureCourse(String name) { 47 this.name = name; 48 } 49 @Override 50 public void study() { 51 System.out.println(name + "正在学习数据结构课程"); 52 } 53 } 54 55 // 3.改造 LAOMA_STUDY 类 56 class LAOMA_STUDY02 { 57 public void study(ICourse icourse) { 58 icourse.study(); 59 } 60 }
这时候我们再来看代码,无论课程怎样更新,对于新的课程,都只需要新建一个类,通过参数传递的方式告诉它,而不需要修改底层的代码。实际上这有点像大家熟悉的 依赖注入 的方式了。
总之,切记:以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此在拿到需求后,要面相接口编程,先顶层设计再细节地设计代码结构。
4. 里氏替换原则
里氏替换原则,即子类可以替换父类 。
继承的优缺点
我们知道 Java 的三大特性:封装、继承、多态。
在面向对象语言中,继承是必不可少的语言机制,有很多的优点:
-
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
-
提高代码的重用性;
-
提高代码的可扩展性,实现父类的方法就可以 “为所欲为” 了;
-
提高产品或项目的开放性;
继承是减少重复代码的可靠手段
但同时也存在着一些缺点:
-
类的耦合性增加了。比如:父类更改之后子类也同时需要更改
-
降低代码的灵活性。因为继承时,父类对子类有约束性。
使用 里氏替换原则 可以减少继承带来的问题。
定义
里氏替换原则,英文简称 LSP
,指的是 所有引用基类的地方都可以透明的使用其子类对象 。
简单来说,就是
只要有父类出现的地方,都可以使用子类来代替。而且不会出现任何错误或者异常。
但是反过来却不行。
子类出现的地方,不能使用父类来代替。
规范
里氏替换原则其实就是为 “良好的继承” 制定一些规范。
-
子类必须实现父类的抽象方法,但不得重写父类的非抽象(已实现的)方法。
简单来说,就是如果子类重写了父类的非抽象方法,当使用子类代替父类时,程序行为可能会有所改变。
1 public class lishitihuan01 { 2 public static void main(String[] args) { 3 // Calculator calculator = new Calculator(); 4 // System.out.println( "计算结果为:" + calculator.add(10, 5) ); // 计算结果为:15 5 6 // 子类代替父类 7 MiniCalculator calculator = new MiniCalculator(); 8 System.out.println( "计算结果为:" + calculator.add(10, 5) ); // 计算结果为:5 9 } 10 } 11 12 // 定义一个父类 13 class Calculator { 14 public int add(int num1, int num2) { 15 return num1 + num2; 16 } 17 } 18 19 // 定义子类 20 class MiniCalculator extends Calculator { 21 public int add(int num1, int num2) { 22 return num1 - num2; 23 } 24 }
-
子类可以增加自己特有的方法。(可以随时扩展)
子类一般都会有自己特有的属性或方法,这点是肯定的。
-
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数要更加宽松。否则会调用到父类的方法。
当我们要覆盖重载父类非抽象方法时,为了确保父类的方法能被正确执行,方法的形参应该更抽象化
1 import java.util.HashMap; 2 import java.util.Collection; 3 import java.util.Map; 4 5 public class lishitihuan02 { 6 public static void main(String[] args) { 7 // Father father = new Father(); 8 // HashMap map = new HashMap(); 9 // father.doSomething(map); // 父类被执行 10 11 // 子类代替父类 12 Son son = new Son(); 13 HashMap map = new HashMap(); 14 son.doSomething(map); // 父类被执行 15 } 16 } 17 18 // 定义一个父类 19 class Father { 20 public Collection doSomething(HashMap map) { 21 System.out.println("父类被执行"); 22 return map.values(); 23 } 24 } 25 26 // 定义一个子类 27 class Son extends Father { 28 public Collection doSomething(Map map) { 29 System.out.println("子类被执行"); 30 return map.values(); 31 } 32 }
-
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更加具体化。否则会调用到父类的方法。
父类的一个方法的返回值是一个类型 T,子类的相同方法的返回值为 S,那么里氏替换原则就要求 S 必须小于等于 T。
产生背景
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
概述
迪米特法则,又叫最少知识原则,也就是说,一个对象应当对其他对象尽可能少的了解。
迪米特法则的核心观念:类间解耦,弱耦合,只有弱耦合了以后,类的复用率才能提高 。
意义和作用
迪米特法则的意义在于降低类之间的耦合。有每个对象尽量减少对其他对象的了解,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖 关系。
使用迪米特法则
迪米特法则告诉我们,如果两个类不必彼此通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某一个方法,可以通过第三者转发这个调用。
这样说可能不是很形象,我们来举个栗子,和陌生人说话,甲和朋友认识,朋友和陌生人认识,而甲和陌生人不认识,这时甲可以直接和朋友传递消息 ,由朋友转发给陌生人。
观察如下代码演示:
1 public class dimitefaze01 { 2 public static void main(String[] args) { 3 4 5 } 6 } 7 // 创建主人公甲类 8 class jia { 9 public void play(Friend friend) { 10 friend.play(); 11 } 12 13 public void play(Stranger stranger) { 14 stranger.play(); 15 } 16 } 17 // 甲的朋友类 18 class Friend { 19 public void play() { 20 System.out.println("朋友"); 21 } 22 } 23 24 // 创建陌生人类 25 class Stranger { 26 public void play() { 27 System.out.println("陌生人"); 28 } 29 }
观察如上代码,这种方式显然 是错误的,甲没有通过朋友,直接引用了陌生人的方法,不符合迪米特法则。
1 public class dimitefaze02 { 2 public static void main(String[] args) { 3 jia02 me = new jia02(); 4 me.play(new Friend02()); 5 } 6 } 7 // 创建主人公甲类 8 class jia02 { 9 public void play(Friend02 friend) { 10 friend.play(); 11 Stranger02 stranger = friend.getStranger(); 12 stranger.play(); 13 14 } 15 } 16 // 甲的朋友类 17 class Friend02 { 18 public void play() { 19 System.out.println("朋友"); 20 } 21 22 public Stranger02 getStranger() { 23 return new Stranger02(); 24 } 25 } 26 27 // 创建陌生人类 28 class Stranger02 { 29 public void play() { 30 System.out.println("陌生人"); 31 } 32 } 33 34 /** 35 输出: 36 朋友 37 陌生人 38 */
这种方式,看上去陌生人的 实例是通过朋友来创建的,但还是不行,因为甲中包含陌生人的引用,甲 还是和陌生人直接关联上了,所以,这也不符合迪米特法则,我们要求甲和陌生人一点关系都没有。
1 public class dimitefaze03 { 2 public static void main(String[] args) { 3 jia03 me = new jia03(); 4 me.setFriend(new Friend03()); 5 me.play(); 6 me.getFriend().playWidthStranger(); 7 } 8 } 9 // 创建主人公甲类 10 class jia03 { 11 private Friend03 friend; 12 13 // 创建构造 方法 14 public Friend03 getFriend() { 15 return friend; 16 } 17 18 public void setFriend(Friend03 friend) { 19 this.friend = friend; 20 } 21 public void play() { 22 friend.play(); 23 } 24 } 25 26 // 创建朋友类 27 class Friend03 { 28 public void play() { 29 System.out.println("朋友"); 30 } 31 32 public void playWidthStranger() { 33 Stranger03 stranger = new Stranger03(); 34 stranger.play(); 35 } 36 } 37 38 // 创建陌生人类 39 class Stranger03 { 40 public void play() { 41 System.out.println("陌生人"); 42 } 43 }
这种方式,甲和陌生人之间就没有了任何直接联系,这样就避免了甲和陌生人的耦合度过高。
第四种方式(结合依赖倒置原则)
上面的案例还有一种更好的方式,与依赖倒置原则相结合,为陌生人创建一个接口。
1 public class dimitefaze04 { 2 public static void main(String[] args) { 3 jia04 me = new jia04(); 4 me.setFriend(new Friend04()); 5 me.getFriend().play(); 6 me.setStranger(new StrangeA()); 7 me.getStranger().play(); 8 } 9 } 10 11 class jia04 { 12 private Friend04 friend; 13 private Stranger04 stranger; 14 15 public Friend04 getFriend() { 16 return friend; 17 } 18 19 public void setFriend(Friend04 friend) { 20 this.friend = friend; 21 } 22 23 public Stranger04 getStranger() { 24 return stranger; 25 } 26 27 public void setStranger(Stranger04 stranger) { 28 this.stranger = stranger; 29 } 30 31 public void play() { 32 System.out.println("someone play"); 33 friend.play(); 34 stranger.play(); 35 } 36 } 37 38 class Friend04 { 39 public void play() { 40 System.out.println("朋友"); 41 } 42 } 43 44 // 创建陌生人的抽象类 45 abstract class Stranger04 { 46 public abstract void play(); 47 48 } 49 50 // 抽象具体化 51 class StrangeA extends Stranger04 { 52 public void play() { 53 System.out.println("陌生人"); 54 } 55 }
这样的方式,和甲直接通信的是陌生人的抽象父类,和具体的陌生人没有直接的关系,所以符合迪米特法则。
狭义的迪米特法则的缺点
-
在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。
-
遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
迪米特法则的应用实例
6. 开闭原则
-
软件对象(类、模块、方法等)应该对于扩展是开放的,对修改是关闭的。比如:一个网络模块,原来只有服务端功能,而现在要加入客户端功能,那么应当再不修改服务端功能的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将客户端和服务端分开。公共部分抽象出来。
简单来说,就是在增加新功能的时候,能不修改代码尽量不要修改,如果只增加代码就能完成新功能,那是最好的。
-
当软件需要变化时,尽量通过扩展软件实体的 行为来实现变化,而不是通过修改已有的代码来实现。
-
编程中遵循其他原则,以及使用设计模式的目的 就是遵循开闭原则。
案例
场景:画三角形
方法一(不符合开闭原则)
绘制矩形和圆形
1 public class kaibiyuanze01 { 2 public static void main(String[] args) { 3 GraphicEditor graphicEditor = new GraphicEditor(); 4 graphicEditor.drawShape(new Rectangle()); 5 graphicEditor.drawShape(new Circle()); 6 } 7 } 8 9 // 绘图类 【使用方】 10 class GraphicEditor { 11 // 接收 Shape01 的实例化对象,然后根据 m_type 的值来绘制不同的图形 12 public void drawShape(Shape01 shape) { 13 if (shape.m_type == 1) { 14 drawRectangle(shape); 15 } else { 16 drawCircle(shape); 17 } 18 } 19 20 public void drawRectangle(Shape01 shape) { 21 System.out.println("绘制三角形"); 22 } 23 24 public void drawCircle(Shape01 shape) { 25 System.out.println("绘制圆形"); 26 } 27 } 28 29 // 创建一个图形基类 shape01类 30 class Shape01 { 31 int m_type; 32 } 33 // 三角形类 34 class Rectangle extends Shape01 { 35 Rectangle() { 36 super.m_type = 1; 37 } 38 } 39 // 圆形类 40 class Circle extends Shape01 { 41 Circle() { 42 super.m_type = 2; 43 } 44 } 45 46 /** 47 输出: 48 绘制三角形 49 绘制圆形 50 */
新增绘制三角形
1 public class kaibiyuanze01 { 2 public static void main(String[] args) { 3 GraphicEditor graphicEditor = new GraphicEditor(); 4 graphicEditor.drawShape(new Rectangle()); 5 graphicEditor.drawShape(new Circle()); 6 7 // 新增绘制三角形高层调用 8 graphicEditor.drawShape(new Triangle()); 9 } 10 } 11 12 // 绘图类 【使用方】 13 class GraphicEditor { 14 // 接收 Shape01 的实例化对象,然后根据 m_type 的值来绘制不同的图形 15 public void drawShape(Shape01 shape) { 16 if (shape.m_type == 1) { 17 drawRectangle(shape); 18 } else if(shape.m_type == 3) { 19 // 新增 绘制三角形逻辑判断 20 drawTriangle(shape); 21 } else { 22 drawCircle(shape); 23 } 24 } 25 26 public void drawRectangle(Shape01 shape) { 27 System.out.println("绘制矩形"); 28 } 29 30 public void drawCircle(Shape01 shape) { 31 System.out.println("绘制圆形"); 32 } 33 34 // 新增绘制三角形方法 35 public void drawTriangle(Shape01 shape) { 36 System.out.println("绘制三角形"); 37 } 38 } 39 40 // 创建一个图形基类 shape01类 41 class Shape01 { 42 int m_type; 43 } 44 // 矩形类 45 class Rectangle extends Shape01 { 46 Rectangle() { 47 super.m_type = 1; 48 } 49 } 50 // 圆形类 51 class Circle extends Shape01 { 52 Circle() { 53 super.m_type = 2; 54 } 55 } 56 57 // 新增 绘制三角形 58 class Triangle extends Shape01 { 59 Triangle() { 60 super.m_type = 3; 61 } 62 } 63 64 /** 65 输出: 66 绘制矩形 67 绘制圆形 68 绘制三角形 69 */
方法二(符合开闭原则)
改进思路:将 Shape 类改进为抽象类,并提供一个 draw 方法,让子类去实现即可,这样有新的图形类时,只需要让新的图形类去继承 Shape,并实现 draw 方法即可,使用方的代码不需要修改,满足开闭原则。
绘制矩形和圆形
1 public class kaibiyuanze02 { 2 public static void main(String[] args) { 3 GraphicEditor02 graphicEditor = new GraphicEditor02(); 4 graphicEditor.drawShape(new Rectangle02()); 5 graphicEditor.drawShape(new Circle02()); 6 } 7 } 8 9 // 绘图类 【使用方】 10 class GraphicEditor02 { 11 // 接收 Shape01 的实例化对象,然后根据 m_type 的值来绘制不同的图形 12 public void drawShape(Shape02 shape) { 13 shape.draw(); 14 } 15 } 16 17 // 创建一个图形基类 Shape02 类 18 abstract class Shape02 { 19 int m_type; 20 public abstract void draw(); // 定义一个抽象方法 21 } 22 23 // 矩形类 24 class Rectangle02 extends Shape02 { 25 Rectangle02() { 26 super.m_type = 1; 27 } 28 @Override 29 public void draw() { 30 System.out.println("绘制矩形"); 31 } 32 } 33 // 圆形类 34 class Circle02 extends Shape02 { 35 Circle02() { 36 super.m_type = 2; 37 } 38 39 @Override 40 public void draw() { 41 System.out.println("绘制圆形"); 42 } 43 } 44 45 /** 46 输出: 47 绘制矩形 48 绘制圆形 49 */
新增绘制三角形
public class kaibiyuanze02 { public static void main(String[] args) { GraphicEditor02 graphicEditor = new GraphicEditor02(); graphicEditor.drawShape(new Rectangle02()); graphicEditor.drawShape(new Circle02()); // 新增高层调用 绘制三角形 graphicEditor.drawShape(new Triangle02()); } } // 绘图类 【使用方】 class GraphicEditor02 { // 接收 Shape01 的实例化对象,然后根据 m_type 的值来绘制不同的图形 public void drawShape(Shape02 shape) { shape.draw(); } } // 创建一个图形基类 Shape02 类 abstract class Shape02 { int m_type; public abstract void draw(); // 定义一个抽象方法 } // 矩形类 class Rectangle02 extends Shape02 { Rectangle02() { super.m_type = 1; } @Override public void draw() { System.out.println("绘制矩形"); } } // 圆形类 class Circle02 extends Shape02 { Circle02() { super.m_type = 2; } @Override public void draw() { System.out.println("绘制圆形"); } } // 新增 绘制三角形 class Triangle02 extends Shape02 { Triangle02() { super.m_type = 3; } @Override public void draw() { System.out.println("绘制三角形"); } }