设计模式——七大原则
更多内容,前往 IT-BLOG
设计模式的目的是为了让程序,具有更好的代码重用性、可读性(编程规范性,便于后期维护和理解)、可扩展性(当需要增加新需求时,非常方便)、可靠性(增加新功能后,对原功能么有影响)、使程序呈现高内聚,低耦合的特性。设计模式包含了面向对象的精髓,“懂了设计模式,就懂得了面向对象分析和设计(OOA/D)的核心”
一、单一职责原则
单一职责原则(SRP:Single responsibility principle)又称为单一功能原则:它规定一个类应该只负责一项职责。单一职责原则注意事项和细节:
1)、降低类的复杂度,一个类只负责一项职责。
2)、提高类的可读性,可维护性。
3)、降低变更引起的风险。
4)、通常情况下,我们应当遵守单一职责原则,只有当逻辑足够简单时,才可以在代码级别违反单一职责原则;只有类中方法足够少,可以在方法级别保持单一职责原则。
二、接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP)的定义:客户端不应该依赖它不需要的接口类,类之间的依赖关系应该建立在最小的接口上。一句话,就是实现接口的类中,有多余的方法时,需要将接口进行拆分。接口隔离原则的规范:
1)、使用接口隔离原则前首先需要满足单一职责原则。
2)、接口需要高内聚,也就是提高接口、类、模块的处理能力,少对外发布public的方法。
3)、定制服务,就是单独为一个个体提供优良的服务,简单来说就是拆分接口,对特定接口进行定制。
4)、接口设计是有限度的,接口的设计粒度越小,系统越灵活,但是值得注意不能过小,否则变成"字节码编程"。
接口隔离解决的问题如下(实现类实现了接口中不需要的抽象方法):
1 //接口 2 interface Interface1 { 3 void operation1(); 4 void operation2(); 5 void operation3(); 6 } 7 8 class B implements Interface1 { 9 public void operation1() { 10 System.out.println("B 实现了 operation1"); 11 } 12 public void operation2() { 13 System.out.println("B 实现了 operation2"); 14 } 15 public void operation3() { 16 System.out.println("B 实现了 operation3"); 17 } 18 } 19 //问题所在:A类只用到了B类的 1,2 方法,但B类却要实现方法3,造成代码的冗余。 20 class A { //A 类通过接口Interface1 依赖(使用) B类,但是只会用到1,2方法 21 public void depend1(Interface1 i) { 22 i.operation1(); 23 } 24 public void depend2(Interface1 i) { 25 i.operation2(); 26 } 27 }
遵循接口隔离原则后(将抽象方法进行隔离,当需要时实现多个接口即可) :
1 // 接口1 2 interface Interface1 { 3 void operation1(); 4 void operation2(); 5 } 6 7 // 接口2 8 interface Interface2 { 9 void operation3(); 10 } 11 12 class B implements Interface1 { 13 public void operation1() { 14 System.out.println("B 实现了 operation1"); 15 } 16 17 public void operation2() { 18 System.out.println("B 实现了 operation2"); 19 } 20 21 } 22 23 class A { // A 类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法 24 public void depend1(Interface1 i) { 25 i.operation1(); 26 } 27 28 public void depend2(Interface2 i) { 29 i.operation2(); 30 } 31 }
三、依赖倒置原则
依赖倒转原则(Dependency Inversion Principle,DIP)的定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。依赖倒置的原则:
1)、高层模块不应该依赖底层模块,二者都应该依赖其抽象。
2)、抽象不应该依赖细节(实现类),细节应该依赖抽象。
3)、依赖倒置的中心思想是面向接口编程。
4)、依赖倒置的的设计理念是:相对于细节的多样性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳的多。在 Java 中,抽象指的是接口和抽象类,细节就是具体的实现类。
5)、使用接口或抽象类的目的是定制好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒置解决的问题如下(方法中传入的参数为类,而不是接口):
1 //完成 Person 接收消息的功能:这里receive方法中直接传入的对象是 类 也是依赖倒置重要强调的问题所在。 2 /**1. 如果我们获取的对象是 微信,短信等等,则新增类,同时Perons也要增加相应的接收方法getInfo() 3 * 2. 解决思路:引入一个抽象的接口IReceiver, 表示接收者, 这样Person类与接口IReceiver发生依赖 4 * 因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就ok, 这样我们就符号依赖倒转原则 5 */ 6 class Person { 7 public void receive(Email email ) { 8 System.out.println(email.getInfo()); 9 } 10 } 11 12 class Email { 13 public String getInfo() { 14 return "电子邮件信息: hello,world"; 15 } 16 } 17 点击并拖拽以移动 18 遵循依赖倒置原则后(方法中传入的参数修改为接口) : 19 20 public class DependecyInversion { 21 22 public static void main(String[] args) { 23 Person person = new Person(); 24 //当为电子邮件时,传入邮件对象 25 person.receive(new Email()); 26 //当为微信时,传入微信对象 27 person.receive(new WeiXin()); 28 } 29 30 } 31 32 //定义接口 33 interface IReceiver { 34 public String getInfo(); 35 } 36 //原电子邮件类,实现接口 37 class Email implements IReceiver { 38 public String getInfo() { 39 return "电子邮件信息: hello,world"; 40 } 41 } 42 43 //增加微信 44 class WeiXin implements IReceiver { 45 public String getInfo() { 46 return "微信信息: hello,ok"; 47 } 48 } 49 50 //方法中传入接口 51 class Person { 52 //这里我们是对接口的依赖 53 public void receive(IReceiver receiver ) { 54 System.out.println(receiver.getInfo()); 55 } 56 }
依赖传递的三种方式和案例: 【1】接口传递:就是上面举例的方式。
【2】构造方法传递:
1 //方式2: 通过构造方法依赖传递 2 //接口1 3 interface IOpenAndClose { 4 public void open(); //抽象方法 5 } 6 //接口2 7 interface ITV { 8 public void play(); 9 } 10 //接口1的方法实现调用接口2,接口2的实现通过构造器传入 11 class OpenAndClose implements IOpenAndClose{ 12 //成员 13 public ITV tv; 14 //构造器 15 public OpenAndClose(ITV tv){ 16 //将传入的对象值复制给自己的成员变量 17 this.tv = tv; 18 } 19 //调用的方法 20 public void open(){ 21 this.tv.play(); 22 } 23 } 24 25 //--------------测试类------------------- 26 public class DependencyPass { 27 public static void main(String[] args) { 28 openAndClose.open(changHong); 29 //通过构造器进行依赖传递 30 OpenAndClose openAndClose = new OpenAndClose(changHong); 31 openAndClose.open(); 32 } 33 }
【3】setter 方法传递:(将实现类通过 set 方法传入到目标对象中):
1 // 方式3 , 通过setter方法传递 2 interface IOpenAndClose { 3 public void open(); // 抽象方法 4 //相当于多添加了一个方法,只用来获取实现 ITV 接口的实现类,并赋值给自己的属性对象。 5 public void setTv(ITV tv); 6 } 7 // ITV接口 8 interface ITV { 9 public void play(); 10 } 11 //目标类(逻辑处理类)当tv属性多次使用到时,可以用此方法实现 12 class OpenAndClose implements IOpenAndClose { 13 private ITV tv; 14 15 public void setTv(ITV tv) { 16 this.tv = tv; 17 } 18 19 public void open() { 20 this.tv.play(); 21 } 22 } 23 //接口 ITV 的实现类 24 class ChangHong implements ITV { 25 @Override 26 public void play() { 27 System.out.println("长虹电视机,打开"); 28 } 29 30 } 31 //--------------------测试----------------- 32 public class DependencyPass { 33 public static void main(String[] args) { 34 OpenAndClose openAndClose = new OpenAndClose(); 35 //通过setter方法进行依赖传递 36 openAndClose.setTv(changHong); 37 openAndClose.open(); 38 } 39 }
依赖倒置原则的注意事项和细节:
1)、低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
2)、变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲区层,对于程序扩展和优化。
3)、继承时遵循理氏替换原则
四、里氏替换原则
里氏代换原则(Liskov Substitution Principle,LSP)的定义:所有引用基类的地方必须能透明地使用其子类的对象,子类可以扩展父类的功能,但不能改变父类原有的功能。面向对象(Object Oriented,OO)继承性的思考和说明:
1)、继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有子类都必须遵循这种契约,但是如果子类对这些已经实现的方法任意修改,就会对这个继承体系造成破坏。
2)、继承在给程序设计带来方便的同时,也带来了弊端。比如使用继承给程序带来侵入性,程序可移植性降低,增加对对象间的耦合性。如果一个类被其他的类所继承,则当此类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。
3)、问题提出:在编程中如何正确的使用继承,答案是:遵循里氏替换原则
里氏替换原则基本介绍:
1)、里氏替换原则是在1988年,有麻省理工学院的一名姓里的女士提出的。
2)、如果对类型为T1的对象o1,对有类型为T2的对象o2,使得以T1定义的所有程序P中对象o1可以代替成o2,程序 P 的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方能透明地使用其子类的对象。
3)、在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
4)、里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。
里氏替换解决的问题如下:(子类重写了父类的方法)
1 // A类 2 class A { 3 // 返回两个数的差 4 public int func1(int num1, int num2) { 5 return num1 - num2; 6 } 7 } 8 9 // B类继承了A 10 // 增加了一个新功能:完成两个数相加,然后和9求和 11 class B extends A { 12 //这里,重写了A类的方法, 可能是无意识 13 public int func1(int a, int b) { 14 return a + b; 15 } 16 17 public int func2(int a, int b) { 18 return func1(a, b) + 9; 19 } 20 }
遵循里氏替换原则后(提取一个公共的类,将A类与B类进行组合) :
1 //创建一个更加基础的基类 2 class Base { 3 //把更加基础的方法和成员写到Base类 4 } 5 6 // A类 7 class A extends Base { 8 // 返回两个数的差 9 public int func1(int num1, int num2) { 10 return num1 - num2; 11 } 12 } 13 14 // B类继承了A 15 // 增加了一个新功能:完成两个数相加,然后和9求和 16 class B extends Base { 17 //如果B需要使用A类的方法,使用组合关系 18 private A a = new A(); 19 20 //这里,重写了A类的方法, 可能是无意识 21 public int func1(int a, int b) { 22 return a + b; 23 } 24 25 public int func2(int a, int b) { 26 return func1(a, b) + 9; 27 } 28 29 //我们仍然想使用A的方法 30 public int func3(int a, int b) { 31 return this.a.func1(a, b); 32 } 33 }
五、开闭原则
开闭原则(Open Closed Principle,OCP)的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原代码的情况下进行扩展。开闭原则的基本介绍:
1)、开闭原则是编程中最基础、最重要的设计原则。
2)、一个软件实体,如类,模块和函数应该对扩展开发(提供方),对修改关闭(使用方)。用抽象构建框架,用实现扩展细节。
3)、当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是修改已有的代码来实现变化。
4)、编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则
开闭原则解决的问题如下:(在使用方进行了代码修改)
1 //这是一个用于绘图的类 [使用方] 2 class GraphicEditor { 3 //接收Shape对象,然后根据type,来绘制不同的图形 4 public void drawShape(Shape s) { 5 //**问题所在:此类属于使用方,但当我们需要扩展新的图形时,却要修改使用方,就不符合OCP原则 6 if (s.m_type == 1) { 7 drawRectangle(s); 8 }else if (s.m_type == 2) { 9 drawCircle(s); 10 } 11 } 12 13 //绘制矩形 14 public void drawRectangle(Shape r) { 15 System.out.println(" 绘制矩形 "); 16 } 17 //绘制圆形 18 public void drawCircle(Shape r) { 19 System.out.println(" 绘制圆形 "); 20 } 21 } 22 23 //Shape类,基类 24 class Shape { 25 int m_type; 26 } 27 28 class Rectangle extends Shape { 29 Rectangle() { 30 super.m_type = 1; 31 } 32 } 33 class Circle extends Shape { 34 Circle() { 35 super.m_type = 2; 36 } 37 }
遵循开闭替换原则(将公共方法提取到抽象类,在实现类中实现需要调用的方法,使用类中直接调用公共方法即可) :
1 //这是一个用于绘图的类 [使用方] 2 class GraphicEditor { 3 //接收Shape对象,调用draw方法 4 public void drawShape(Shape s) { 5 //直接调用公共方法即可,就算增加新的图形也无需修改此处, 6 //当多个地方调用时,更能体现OCP的重要性,这里只是简单举例 7 s.draw(); 8 } 9 } 10 11 //Shape类,基类 12 abstract class Shape { 13 //抽象方法 14 public abstract void draw(); 15 } 16 //[提供方] 17 class Rectangle extends Shape { 18 @Override 19 public void draw() { 20 // TODO Auto-generated method stub 21 System.out.println(" 绘制矩形 "); 22 } 23 } 24 25 class Circle extends Shape { 26 @Override 27 public void draw() { 28 // TODO Auto-generated method stub 29 System.out.println(" 绘制圆形 "); 30 } 31 }
六、迪米特法则
迪米特法则(Law of Demeter,LOD),有时候也叫做最少知识原则(Least Knowledge Principle,LKP)定义是:一个软件实体应尽可能少地与其他实体发生相互作用。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块独立,相互之间不存在(或很少有)依赖关系。迪米特法则则不希望类之间建立直接的关系。如果真的有需要建立联系,也希望能通过它的友元类(中间类或者跳转类)来转达。迪米特法则的规则:
1)、Only talk to your immediate friends(只与直接的朋友通讯):每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友(例如,在一个方法中new了一个类,那么此类就不属于直接朋友)。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
2)、一个对象应该对其他对象保持最少的了解。
3)、类与类关系越密切,耦合度越大。
4)、迪米特法则指一个类对自己依赖的类知道的 越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。
5)、 迪米特法则还有个更简单的定义:只与直接的朋友通信。
迪米特法则解决的问题如下:(方法中出现了局部变量)
1 //通过查看如下代码会发现,CollegeEmployee 以局部变量的形式出现在方法 printAllEmployee 中,违反了迪米特法则 2 public class Demeter1 { 3 //该方法完成输出学校总部和学院员工信息(id) 4 void printAllEmployee(CollegeManager sub) { 5 6 //分析问题 7 //1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友 8 //2. CollegeEmployee 是以局部变量方式出现在 SchoolManager 9 //3. 违反了 迪米特法则 10 11 //获取到学院员工 12 List<CollegeEmployee> list1 = sub.getAllEmployee(); 13 System.out.println("------------学院员工------------"); 14 for (CollegeEmployee e : list1) { 15 System.out.println(e.getId()); 16 } 17 }
遵循迪米特法则后(将局部变量部分,提取到自己的类中):
1 public class DemeterUpdate { 2 void printAllEmployee(CollegeManager sub) { 3 //分析问题 4 //1. 将输出学院的员工方法,封装到CollegeManager 5 sub.printEmployee(); 6 } 7 } 8 9 //管理学院员工的管理类 10 class CollegeManager { 11 //输出学院员工的信息 12 public void printEmployee() { 13 //获取到学院员工 14 List<CollegeEmployee> list1 = getAllEmployee(); 15 System.out.println("------------学院员工------------"); 16 for (CollegeEmployee e : list1) { 17 System.out.println(e.getId()); 18 } 19 } 20 21 //返回学院的所有员工 22 public List<CollegeEmployee> getAllEmployee() { 23 List<CollegeEmployee> list = new ArrayList<CollegeEmployee>(); 24 for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list 25 CollegeEmployee emp = new CollegeEmployee(); 26 emp.setId("学院员工id= " + i); 27 list.add(emp); 28 } 29 return list; 30 } 31 }
迪米特法则注意事项和细节:
1)、迪米特法则的核心是降低类之间的耦合。
2)、但是注意:由于每个类之间都减少了不必要的依赖,因此迪米特法则只是要求降低类之间(对象间)耦合关系,并不是要求完全没有依赖关系。
七、合成复用原则
合成复用原则的定义是:原则是尽量使用合成/聚合的方法,而不是使用继承。
聚合用来表示“拥有”关系或者整体与部分的关系:代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,班级和学生,当班级删除后,学生还能存在,学生可以被培训机构引用。
1 class OpenAndClose implements IOpenAndClose { 2 private ITV tv; 3 //通过set方法将ITV对象聚合到OpenAndClose对象中 4 public void setTv(ITV tv) { 5 this.tv = tv; 6 } 7 8 public void open() { 9 this.tv.play(); 10 } 11 }
合成用来表示一种强得多的“拥有”关系:在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成的新对象对组成部分的内存分配、内存释放有绝对的责任。例如,一个人由头、四肢和各种器官组成,人与这些具有相同的生命周期,人死了,这些器官也就挂了。房子和房间的关系,当房子没了,房间也不可能独立存在。
1 class OpenAndClose implements IOpenAndClose { 2 //将 ITV 对象组合到 OpenAndClose 对象中 3 ITV tv = new ITV(); 4 }
【总结】设计原则的核心思想:
【1】找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
【2】 针对接口编程,而不是针对实现编程。
【3】为了交互对象之间的松耦合设计而努力。
简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系;依赖倒置原则指导我们要面向接口编程;接口隔离原则指导我们在设计接口的时候要精简单一;迪米特法则指导我们要降低耦合。
设计模式就是通过这七个原则,来指导我们如何做一个好的设计。但是设计模式不是一套“奇技淫巧”,它是一套方法论,一种高内聚、低耦合的设计思想。我们可以在此基础上自由的发挥,甚至设计出自己的一套设计模式。
当然,学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。如果脱离具体的业务逻辑去学习或者使用设计模式,那是极其空洞的。