目录
什么是设计模式
- 设计模式(Design Pattern)是一套被反复使用,多数人知晓的,经过分类编目的,代码设计经验的总结。使用设计模式是为了可重用性代码,让代码更容易被他人理解,保证代码可靠性。
概览
- 开闭原则:是指一个软件实体如类、模块和函数应该对扩展开放, 对修改关闭
- 依赖倒置原则:是指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象而不依赖于具体。
- 单一职责原则:是指一 个 Class/Interface/Method 只负责一项职责。
- 接口隔离原则:是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
- 迪米特法原则(最少知道原则):是指一个对象应该对其他对象保持最少的了解。
- 里氏替换原则:是指一个软件实体如果适用一个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变
- 合成复用原则:是指尽量使用对象组合(has-a)或聚合(contanis-a),而不是继承关系达到软件复用的目的
开闭原则
含义:
- 开闭原则(Open-Closed Principle, OCP)是指一个软件实体 如类、模块和函数应该对扩展开放,对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。强调的是用抽象构建框架,用实现扩展细节。开闭原则,是面向对象设计中最基础的设计原则。它指导我们如何建立稳定灵活的系统,例如:我们版本更新,我尽可能不修改源代码,但是可以增加新功能。
核心思想:
- 面向抽象编程。
作用(优点):
- 可以提高代码的可复用性:我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。
- 可以提高软件的可维护性:由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。如:一人模块变化,会对其它的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化。
实现方式:
实现开闭原则的关键就在于“抽象”。把系统/软件的所有可能的行为抽象成一个抽象底层,这个抽象底层规定出所有的具体实现必须提供的方法的特征。作为系统设计的抽象层,要预见所有可能的扩展,从而使得在任何扩展情况下,系统的抽象底层不需修改;同时,由于可以从抽象底层导出一个或多个新的具体实现,可以改变系统的行为,因此系统设计对扩展是开放的。抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:
- 通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。
- 参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求。
- 抽象层尽量保持稳定,一旦确定就不要修改。
举例说明:
以课程打折为例:
先看课程类接口ICourse:
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
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 Integer getId() {
return this.Id;
}
public String getName() {
return this.name;
}
public Double getPrice() {
return this.price;
}
}
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getOriginPrice() {
return super.getPrice();
}
public Double getDiscountPrice() {
return super.getPrice() * 0.61;
}
}
依赖倒置原则
含义:
- 依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层(调用层)模块不应该依赖底层(被调用层)模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。即面向接口编程,不要面向实现编程。依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构要比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类。
核心思想:
- 面向接口编程,不要面向实现编程。
作用(优点):
- 减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险。
实现方式:
- 传递依赖关系有三种方式:接口传递(方法注入)、构造方法传递和setter方法传递
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
- 低层模板尽量都要有抽象类或接口,或者两个都有,程序稳定性更好。
- 变量的声明类型尽量都是抽象类或接口,这样我们的变量变量引用和实际对象间就存在一个缓冲层,有利于程序的扩展和优化。
- 继承时遵循里氏替换原则。
举例说明:
以学习多个学科的课程为例
先看XiaoQiang类:
public class XiaoQiang {
public void studyJavaCourse() {
System.out.println("XiaoQiang 在学习 Java 的课程");
}
public void studyPythonCourse() {
System.out.println("XiaoQiang 在学习 Python 的课程");
}
}
来调用一下:
public static void main(String[] args) {
XiaoQiang xq = new XiaoQiang();
xq.studyJavaCourse();
xq.studyPythonCourse();
}
public interface ICourse {
void study();
}
public class JavaCourse implements ICourse {
@Override
public void study() {
System.out.println("XiaoQiang 在学习 Java 课程");
}
}
public class PythonCourse implements ICourse {
@Override
public void study() {
System.out.println("XiaoQiang在学习Python课程");
}
}
修改XiaoQiang类:
public class XiaoQiang {
public void study(ICourse course){
course.study();
}
}
public static void main(String[] args) {
XiaoQiang xq = new XiaoQiang();
xq.study(new JavaCourse());
xq.study(new PythonCourse());
}
public class XiaoQiang {
private ICourse course;
public XiaoQiang(ICourse course){
this.course = course;
}
public void study(){
course.study();
}
}
public static void main(String[] args) {
XiaoQiang tom = new XiaoQiang(new JavaCourse());
xq.study();
}
public class XiaoQiang {
private ICourse course;
public void setCourse(ICourse course) {
this.course = course;
}
public void study() {
course.study();
}
}
public static void main(String[] args) {
XiaoQiang xq = new XiaoQiang();
xq.setCourse(new JavaCourse());
xq.study();
xq.setCourse(new PythonCourse());
xq.study();
}
单一职责原则
含义:
- 单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。换一种说法,一个类只负责一项职责,应该仅有一个引起它变化的原因。总体来说就是一个Class/Interface/Method 只负责一项职责。
核心思想:
- Class/Interface/Method 只负责一项职责
作用(优点):
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
实现方式:
- 单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
举例说明:
以课程有直播课和录播课。直播课不能快进和快退,录播可以可以任意的反复观看,功能职责不一样为例说明。
public class Course {
public void study(String courseName) {
if ("直播课".equals(courseName)) {
System.out.println("不能快进");
} else {
System.out.println("可以任意的来回播放");
}
}
}
public static void main(String[] args) {
Course course = new Course();
course.study("直播课");
course.study("录播课");
}
public class LiveCourse {
public void study(String courseName) {
System.out.println(courseName + "不能快进看");
}
}
public class ReplayCourse {
public void study(String courseName) {
System.out.println("可以任意的来回播放");
}
}
public static void main(String[] args) {
LiveCourse liveCourse = new LiveCourse();
liveCourse.study("直播课");
ReplayCourse replayCourse = new ReplayCourse();
replayCourse.study("录播课");
}
public interface ICourse {
//获得基本信息
String getCourseName();
//获得视频流
byte[] getCourseVideo();
//学习课程
void studyCourse();
//退款
void refundCourse();
}
public interface ICourseInfo {
String getCourseName();
byte[] getCourseVideo();
}
ICourseManager 接口:
public interface ICourseManager {
void studyCourse();
void refundCourse();
}
来看一下类图:
private void modifyUserInfo(String userName,String address){
userName = "XiaoQiang";
address = "Changsha";
}
private void modifyUserInfo(String userName,String address,boolean bool){
if(bool){
userName = "XiaoQiang";
}else{
address = "Changsha";
}
}
private void modifyUserName(String userName){
userName = "XiaoQiang";
}
private void modifyAddress(String address){
address = "Changsha";
}
这修改之后,开发起来简单,维护起来也容易。所以编写代码的过程,尽可能地让接口和方法保持单一职责,便于我们项目后期的维护
接口隔离原则
含义:
- 接口隔离原则(Interface Segregation Principle, ISP)是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。接口隔离原则符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性
和可维护性
核心思想
- 高内聚低耦合
作用(优点):
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性、可扩展性和可维护性。
接口隔离原则跟单一职责原则区别:
- 单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。
- 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。
实现方式:
- 一个类对一类的依赖应该建立在最小的接口之上。
- 建立单一接口,不要建立庞大臃肿的接口。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
- 尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度,接口过小则会造成接口数量过多,使设计复杂化)。
-
多花时间去思考、要考虑业务模型,包括以后有可能发生变更的地方还要做一些预判。所以对于抽象、对业务模型的理解是非常重要的
举例说明:
public interface IAnimal {
void eat();
void fly();
void swim();
}
public class Bird implements IAnimal {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
Dog 类实现:
public class Dog implements IAnimal {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
public interface IEatAnimal {
void eat();
}
IFlyAnimal 接口:
public interface IFlyAnimal {
void fly();
}
ISwimAnimal 接口:
public interface ISwimAnimal {
void swim();
}
public class Dog implements ISwimAnimal, IEatAnimal {
@Override
public void eat() {
}
@Override
public void swim() {
}
}
这样Dog 就可以依赖它不需要的接口,并且接口单一不会庞大臃肿,有很好的复用性和可维护性
最后来看下两种类图的对比,还是非常清晰明了的:
迪米特原则
含义:
- 迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP)尽量降低类与类之间的耦合。迪米特原则主要强调只和朋友交流,不和陌生人说话(出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的类不属于朋友类。)
核心思想
- 降低类之间的耦合度
作用(优点):
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
举例说明:
public class Course {
}
public class TeamLeader {
public void checkNumberOfCourses(List<Course> courseList) {
System.out.println("目前已发布的课程数量是:" + courseList.size());
}
}
Boss 类:
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader){
//模拟 Boss 一页一页往下翻页,TeamLeader 实时统计
List<Course> courseList = new ArrayList<Course>();
for (int i= 0; i < 20 ;i ++) {
courseList.add(new Course());
}
teamLeader.checkNumberOfCourses(courseList);
}
}
public static void main(String[] args) {
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
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());
}
}
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
}
由此设计Course 和 Boss 已经没有关联了,再来看下面的类图
里氏替换原则
含义:
- 里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为 T1 的对象 o1,都有 类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没 有发生变化,那么类型 T2 是类型 T1 的子类型。换一种理解方式,可以理解为一个软件实体如果适用一个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,我们总结一下:(实现方式:)
- 引申含义:子类可以扩展父类的功能,但不能改变父类原有的功能
- 子类可以实现父类的抽象方法,
- 不能覆盖父类的非抽象方法。
-
子类中可以增加自己特有的方法。
-
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入 参数更宽松。
-
当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输 出/返回值)要比父类更严格或相等。
- 里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。
核心思想:
- 子类中不应该重写父类的方法
作用(优点):
- 约束继承泛滥,里氏替换原则是实现开闭原则的重要方式之一,是开闭原则的一种体现。
- 克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
- 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性。降低需求变更时引入的风险
举例说明:
public class Rectangle {
private long height;
private long width;
@Override
public long getWidth() {
return width;
}
@Override
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public void setWidth(long width) {
this.width = width;
}
}
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
@Override
public long getWidth() {
return getLength();
}
@Override
public long getHeight() {
return getLength();
}
@Override
public void setHeight(long height) {
setLength(height);
}
@Override
public void setWidth(long width) {
setLength(width);
}
}
public static void resize(Rectangle rectangle){
while (rectangle.getWidth() >= rectangle.getHeight()){
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
}
System.out.println("resize 方法结束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(20);
rectangle.setHeight(10);
resize(rectangle);
}
运行结果:
width:20,height:11
width:20,height:12
width:20,height:13
width:20,height:14
width:20,height:15
width:20,height:16
width:20,height:17
width:20,height:18
width:20,height:19
width:20,height:20
width:20,height:21
resize 方法结束
width:20,height:21
public static void main(String[] args) {
Square square = new Square();
square.setLength(10);
resize(rectangle);
}
public interface Quadrangle {
long getWidth();
long getHeight();
}
public class Rectangle implements Quadrangle {
private long height;
private long width;
@Override
public long getWidth() {
return width;
}
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public void setWidth(long width) {
this.width = width;
}
}
public class Square implements Quadrangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
@Override
public long getWidth() {
return length;
}
@Override
public long getHeight() {
return length;
}
}
此时,如果我们把 resize()方法的参数换成四边形 Quadrangle 类,方法内部就会报错。 因为正方形 Square 已经没有了 setWidth()和 setHeight()方法了。因此,为了约束继承泛滥,resize()的方法参数只能用 Rectangle 长方形。
合成复用原则
含义:
- 合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)、聚合(contanis-a),而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的。要根据具体的业务场景来做代码设计,其实也都需要遵循 OOP
核心思想:
- 尽量使用聚合、组合的方式,而不是使用继承。
实现方式:
- 合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
聚合与组合的区别:
- 聚合是用来表示“拥有”关系或者整体与部分的关系。
- 组合则是表示一种强得多的“拥有”关系,在组合里,部分与整体的生命周期是一样的。一个组合的新的对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。一个组合关系中的成分对象是不能与另一个组合关系共享的。一个组成部分在同一个时间内只能属于一个组合关系。
复用的基本方式有两种:
- 组合/聚合
- 继承
复用两种方式的区别:
- 组合/聚合是将已有的对象纳入到新对象中,使之成为新对象的一部分,
- 优点:
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 每个新的类可以将焦点集中在一个任务上。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
- 缺点:
- 通过使用这种方式复用建造的系统会有较多的对象需要管理。
- 继承是面向对象特有的复用工具,而且也最容易被滥用。继承复用通过扩展一个已有对象的实现来得到新的功能。
- 优点:
- 较为容易,因为父类的大部分功能都可以通过继承关系自动进入子类。
- 修改或扩展继承而来的实现较为容易。
- 缺点:
- 破坏了类的封装性,因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,修改父类就会形成链锁反映,这不利于类的扩展与维护。
- 限制了复用的灵活性,从父类继承而来的实现是静态的,在编译时已经定义,所以不可能在运行时间内发生改变,因此也没有足够的灵活性。
举例说明:
以数据库操作为例,先来创建 DBConnection 类:
public class DBConnection {
public String getConnection() {
return "MySQL 数据库连接";
}
}
public class ProductDao {
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct() {
String conn = dbConnection.getConnection();
System.out.println("使用" + conn + "增加产品");
}
}
public abstract class DBConnection {
public abstract String getConnection();
}
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL 数据库连接";
}
}
再创建 Oracle 支持的逻辑:
public class OracleConnection extends DBConnection {
@Override
public String getConnection() {
return "Oracle 数据库连接";
}
}
设计原则总结
这7种设计原则是软件设计模式必须尽量遵循的原则,各种原则要求的侧重点不同:
- 开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭。
- 依赖倒置原则告诉我们要面向接口编程。
- 单一职责原则告诉我们实现类要职责单一。
- 接口隔离原则告诉我们在设计接口的时候要精简单一。
- 迪米特法则告诉我们要降低耦合度。
- 里氏替换原则告诉我们不要破坏继承体系。
- 合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。