Java设计模式学习——设计原则
第一章 设计原则
1.开闭原则
一个软件实体,像类,模块,函数应该对扩展开放,对修改关闭
在设计的时候,要时刻考虑,让这个类尽量的好,写好了就不要去修改。如果有新的需求来,在增加一个类就完事了,原来的代码能不动就不动。这个原则的特性一个是对扩展开发,一个是对修改关闭。即面对需求,我们要做的是通过增加代码来完成,而不是更改现有的代码。这也是设计原则的基础,精神所在
代码解释:
创建一个ICourse接口,用javaCourse类去实现它。
/** * @program: designModel * @description: 课程接口 * @author: YuKai Fan * @create: 2018-11-13 10:36 **/ public interface ICourse { Integer getId(); String getName(); Double getPrice(); }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 10:37 **/ public class JavaCourse implements ICourse { private Integer Id; private String name; private Double price; public JavaCourse(Integer id, String name, Double price) { this.Id = id; this.name = name; this.price = price; } public JavaCourse() { } public Integer getId() { return this.Id; } public String getName() { return this.name; } public Double getPrice() { return this.price; } }
创建一个测试类
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 10:38 **/ public class Test { public static void main(String[] args) { ICourse javaCourse = new JavaCourse(96, "Java设计原则", 998d); System.out.println("课程Id:" + javaCourse.getId() + "课程名称:" + javaCourse.getName() + "课程价格:" + javaCourse.getPrice()); } }
现在因为有活动,需要对java有关课程进行8折活动,需要修改代码,但是对于其他类型的课程不一定打8折,而且如果直接在接口中改动,则太麻烦,其实现类也需要改动,如果有多个类实现接口,则大大降低了开发效率,甚至会使项目崩溃,所以要遵循开闭原则。
不能改动原有的代码。添加一个新的类,来继承javaCourse,重写price方法,即可完成需求
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 10:45 **/ public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getOriginPrice() { return super.getPrice(); } @Override public Double getPrice() { return super.getPrice() * 0.8; } }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 10:38 **/ public class Test { public static void main(String[] args) { ICourse iCourse = new JavaDiscountCourse(96, "Java设计原则", 998d); JavaDiscountCourse javaCourse = (JavaDiscountCourse) iCourse; System.out.println("课程Id:" + javaCourse.getId() + "课程名称:" + javaCourse.getName() + "课程价格:" + javaCourse.getPrice() + "课程原价:" + javaCourse.getOriginPrice()); } }
根据面向对象的堕胎性质,JavaDiscountCourse,JavaCourse,ICourse都是同一类型,可以直接强转。
UML类图:
2.里氏替换原则
如果一个类型为T1的对象o1,都有类型为T2的对象o2,使得在程序P中将所有的o1替换成o2时,程序的行为P没有发生变化,那么T2就是T1的子类型。
子类型必须能够替换调它们的父类型。如果一个软件实体中使用的是父类,那么一定适用于子类,而且程序察觉不出它们之间的区别。比如,我们说企鹅是一种鸟,但是在编程中,不能用企鹅来继承鸟。因为在面向对象设计中,子类应该拥有父类所有非private的属性和行为,因为鸟会飞,而企鹅不会飞,所以企鹅不能继承鸟。
当子类可以替换掉父类,软件单位的功能不受影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为,正是有里氏代换原则,使得继承复用成为了可能。正是由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展,不然还谈什么扩展开放,修改关闭呢。
比较通俗的说,里氏替换原则就是子类可以扩展父类的功能,但是不能改变父类原有的功能:
1.子类可以实现父类的抽象方法,但是不能覆盖父类的抽象方法
2.子类可以增加自己特有的方法
3.当子类的方法重载父类的方法时,方法的前置条件(方法的形参)要比父类的方法更加宽松
4.当子类实现父类的抽象方法时,方法的后置条件(方法的返回值)要比父类更严格
2.依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象,即针对接口编程,而不是针对实现编程。
依赖倒置可以说是面向对象的设计的标志原则,无论用哪种语言编写程序,如果编写的都是如何针对抽象而不是针对细节,即程序中的依赖关系都终止于接口或者抽象类,那就是面向接口设计,返回就是面向过程了。这样的好处就是实现了低耦合,高扩展,易维护的特性。通过代码来看问题会更加明显。现在有一个学生,他想学习java和前端,可以直接在这个类中实现这两个方法:
/** * @program: designModel * @description: 学生类 * @author: YuKai Fan * @create: 2018-11-13 11:16 **/ public class Student { public void studyJava() { System.out.println("学习java"); } public void studyFE() { System.out.println("学习前端"); } }
/** * @program: designModel * @description: test属于应用层,不应该依赖student低层次模块 ,学生想学习什么都需要在Student中添加方法,然后在Test使用,扩展性比较差。 * @author: YuKai Fan * @create: 2018-11-13 11:17 **/ public class Test { public static void main(String[] args) { Student student = new Student(); student.studyFE(); student.studyJava(); } }
但是就如同代码上的注释一样,如果现在学生又想学习linux课程,那就必须要在Student中添加方法,在Test应用层中实现。这样高层依赖低层,耦合度很高,扩展性差。所以现在进行改动,编写一个接口,让对应的课程去实现接口完成学习任务
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 11:20 **/ public interface ICourse { void studyCourse(); }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 11:20 **/ public class JavaCourse implements ICourse { public void studyCourse() { System.out.println("学生学习java课程"); } }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 11:21 **/ public class FECourse implements ICourse { public void studyCourse() { System.out.println("学生学习前端课程"); } }
而在Student中,就不需要这两个方法,写一个公共的方法,使用接口作为入参。Test直接调用。这种注入的方法,称为接口注入
/** * @program: designModel * @description: test属于应用层,不应该依赖student低层次模块 ,学生想学习什么都需要在Student中添加方法,然后在Test使用,扩展性比较差。 * @author: YuKai Fan * @create: 2018-11-13 11:17 **/ public class Test { // public static void main(String[] args) { // Student student = new Student(); // student.studyFE(); // student.studyJava(); // } //接口注入 public static void main(String[] args) { Student student = new Student(); student.studyStudentCourse(new JavaCourse()); student.studyStudentCourse(new FECourse()); } }
还有另一种方式,在Student中定义一个有参构造器,将接口传入
/** * @program: designModel * @description: 学生类 * @author: YuKai Fan * @create: 2018-11-13 11:16 **/ public class Student { /*public void studyJava() { System.out.println("学习java"); } public void studyFE() { System.out.println("学习前端"); }*/ private ICourse iCourse; public Student(ICourse iCourse) { this.iCourse = iCourse; } public void studyStudentCourse() { iCourse.studyCourse(); } }
Test类中,在new student时直接可以传入接口实现类对象,这种称为构造器注入
/** * @program: designModel * @description: test属于应用层,不应该依赖student低层次模块 ,学生想学习什么都需要在Student中添加方法,然后在Test使用,扩展性比较差。 * @author: YuKai Fan * @create: 2018-11-13 11:17 **/ public class Test { // public static void main(String[] args) { // Student student = new Student(); // student.studyFE(); // student.studyJava(); // } //接口注入 // public static void main(String[] args) { // Student student = new Student(); // student.studyStudentCourse(new JavaCourse()); // student.studyStudentCourse(new FECourse()); // } //构造器注入 public static void main(String[] args) { Student student = new Student(new JavaCourse()); student.studyStudentCourse(); Student student1 = new Student(new FECourse()); student1.studyStudentCourse(); } }
第三种方法,也是在实际应用中使用最多的方法settr方法注入
/** * @program: designModel * @description: 学生类 * @author: YuKai Fan * @create: 2018-11-13 11:16 **/ public class Student { /*public void studyJava() { System.out.println("学习java"); } public void studyFE() { System.out.println("学习前端"); }*/ /*public Student(ICourse iCourse) { this.iCourse = iCourse; }*/ private ICourse iCourse; public void studyStudentCourse() { iCourse.studyCourse(); } public void setiCourse(ICourse iCourse) { this.iCourse = iCourse; } }
Test应用类,通过接口的set方法,将对象注入
/** * @program: designModel * @description: test属于应用层,不应该依赖student低层次模块 ,学生想学习什么都需要在Student中添加方法,然后在Test使用,扩展性比较差。 * @author: YuKai Fan * @create: 2018-11-13 11:17 **/ public class Test { // public static void main(String[] args) { // Student student = new Student(); // student.studyFE(); // student.studyJava(); // } //接口注入 // public static void main(String[] args) { // Student student = new Student(); // student.studyStudentCourse(new JavaCourse()); // student.studyStudentCourse(new FECourse()); // } //构造器注入 // public static void main(String[] args) { // Student student = new Student(new JavaCourse()); // student.studyStudentCourse(); // // Student student1 = new Student(new FECourse()); // student1.studyStudentCourse(); // } public static void main(String[] args) { Student student = new Student(); student.setiCourse(new JavaCourse()); student.studyStudentCourse(); student.setiCourse(new FECourse()); student.studyStudentCourse(); } }
UML类图:
依赖倒置原则的中心思想就是面向接口编程,依赖传递方式有三种:接口注入,构造器注入,setter方法注入。这三种方法,在学习Spring的时候,都会用到依赖传递。
相对于细节的东西,抽象的东西要稳定的多。以抽象基础搭起来的架构要比细节搭出来的架构要稳定的多。在java中,抽象指的是抽象类和接口,细节就是具体的实现类,使用接口和抽象类的目的就是为了制定规范和约束,而不涉及任何的就操作。
在实际编程中,一般要做到:低层模块都要有抽象类或者接口,变量的声明类型尽量是抽象类或者接口,使用继承是遵循里氏替换原则
3.单一职责原则
不要存在多于一个导致类变更的原因。直白的说就是一个类只负责一项职责,应该只有一个引起他变化的原因
说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
遵循单一职责原则的优点:
1.可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多
2.提高类的可读性,提高系统的可维护性
3.变更引起的风险降低,变更是必然的,但是如果遵循单一职责原则,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都需要遵循这一重要原则。
4.接口隔离原则
用多个专门的接口,而不是用单一的总接口,客户端不应该依赖它不需要的接口。我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
我最初感觉单一职责原则与接口隔离原则很相似,但是后来发现两者不一样。单一职责原则注重的职责,主要约束的是类,其次才是接口和方法,针对的是程序中的实现和细节。而接口隔离原则,注重接口依赖的隔离,主要约束的是接口,针对抽象,程序整体架构的构建。但是使用接口隔离原则需要注意几点:
1.接口尽量小,但是要有限度;虽然对接口进行细化会提高程序设计的灵活性,但是如果过小,会造成接口数量过多,使设计复杂化。
2.为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
3.提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
比如下面的UML类图
狗可以吃,也可以游泳,但是不会飞。鸟可以吃,可以飞,但是不会游泳。这样就会造成多余的空方法。
改造后UML类图:
5.迪米特原则(最少知道原则)
一个对象应该对其他对象保持最少的了解,又叫最少知道原则,尽量降低类与类之间的耦合。强调只和朋友交流,不和陌生人说话。
朋友:出现在成员变量,方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
迪米特法则的的根本思想是,强调类之间的松耦合,类之间耦合越弱,越有利于复用。一个处于弱耦合的类被修改时,不会对其有关系的类造成影响,换句话说,也就是信息的隐藏促进了软件的复用。
我们都知道软件编程的总原则是,高内聚,低耦合,无论是面向对象编程还是面向过程,只有各个模块的耦合度尽量低,才能提高代码的复用率。
最少知道原则:顾名思义,一个类对自己依赖的类知道的越少越好,即对于被依赖的类来说,无论逻辑多么复杂,都尽量的将逻辑封装在类的内部,对外除了提供public的方法,不对外泄露任何信息。
只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
现在有一个老板Boss,他想指令一个团队领导Teamleader统计课程Course的数量,并打印出来。可以如下设计
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 15:29 **/ public class Boss { public void commandCheckNumber(TeamLeader teamLeader) { List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 15:30 **/ public class TeamLeader { public void checkNumberOfCourses(List<Course> courseList) { System.out.println("在线课程的数量是:" + courseList.size()); } }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 15:33 **/ public class Test { public static void main(String[] args) { Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); } }
根据UML类图可知:
其中Boss与课程Course是没有关系的,所以Boss不应该知道有关Course的内容,而是Teamleader去了解Course,所以作如下改动
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 15:29 **/ public class Boss { public void commandCheckNumber(TeamLeader teamLeader) { /*List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); }*/ teamLeader.checkNumberOfCourses(); } }
/** * @program: designModel * @description: * @author: YuKai Fan * @create: 2018-11-13 15:30 **/ public class TeamLeader { public void checkNumberOfCourses() { List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } System.out.println("在线课程的数量是:" + courseList.size()); } }
UML类图为: