设计模式是对软件设计中普遍存在的问题,所提出的解决方案,而作为一个合格的程序员,设计模式也是必须要掌握的。在学习设计模式之前,肯定是要学习一下设计模式的基础,即设计原则
七大设计原则
首先大概了解一下有哪七大设计原则,如下:
| 设计原则 | 解释 |
|---|---|
| 开闭原则 | 对扩展开放,对修改关闭 |
| 依赖倒置原则 | 通过抽象使各个类或者模块不相互影响,实现松耦合 |
| 单一职责原则 | 一个类、接口、方法只做一件事 |
| 接口隔离原则 | 尽量保证接口的纯洁性,客户端不应该依赖不需要的接口 |
| 迪米特法则 | 又叫最少知道原则,一个类对其所依赖的类知道的越少越好 |
| 里氏替换原则 | 子类可以扩展父类的功能但不能改变父类原有的功能 |
| 合成复用原则 | 尽量使用对象组合、聚合,而不使用继承关系达到代码复用的目的 |
开闭原则(Open-closed Principle, OCP)
开闭原则是面向对象设计汇总最基础的设计原则,指导我们如何建立稳定灵活的系统。
开闭原则是指一个实体如类、模块和函数对扩展开放,对修改关闭。所谓的开闭,正是对扩展和修改两个行为的一个原则。强调的是用抽象构建框架,用实现扩展细节,提高软件的可复用性及可维护性
举个例子,公司实行弹性制工作时间,规定每天工作8小时。意思就是说,对于每天工作8小时的这个规定是关闭的,但是你什么时候来,什么时候走是开放的,早来早走,晚来晚走。
实现开闭原则的核心思想就是面向抽象编程,接下来看下面一段代码:
一个在线教育系统,里面有很多种课程,比如Java、大数据、Python等,现在创建一个课程接口
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
创建一个Java课程
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;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
}
现在要给Java课程做活动,搞优惠。如果我们去修改JavaCourse中的getPrice()方法,则会存在一定的风险,会影响到其他地方调用的结果。如何在不修改原有代码的前提下,实现价格优惠功能呢?那么我们就需要再创建一个处理优惠逻辑的类
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 getPrice() {
return super.getPrice() * 0.6;
}
}
public static void main(String[] args) {
ICourse javaCourse = new JavaCourse(1, "java", 666.66);
javaCourse = new JavaDiscountCourse(1, "Java", 666.66);
System.out.println(javaCourse.getPrice());
}
最终,我们来看一下代码的类图
依赖倒置原则(Dependence Inversion Principle, DIP)
- 高层模块(调用层)不应该依赖底层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒置的中心思想是面向接口编程
- 通过依赖倒置,可以减少类与类之间的耦合性,提高代码的稳定性,可读性以及维护性
- 使用接口或抽象类的目的是指定好规范,而不涉及任何具体的操作,把展现细节的任务交给它们的实现类来完成
看一个案例,还是以课程为例,先创建一个用户
public clss Jack {
public void studyJavaCourse() {
System.out.println("Jack在学习Java");
}
public void studyPythonCourse() {
System.out.println("Jack在学习Python");
}
}
public static void main(String[] args) {
Jack jack = new Jack();
jack.studyJavaCourse();
jack.studyPythonCourse();
}
如果后续要学习新的课程,那么就要从底层到调用层修改代码,如此一来是非常的麻烦,而且有可能在修改代码的同时,带来其他风险,因此要优化一下代码,创建一个课程的抽象接口
public interface ICourse {
void study();
}
创建课程类JavaCourse类
public class JavaCourse implements ICourse{
@Override
public void study() {
System.out.println("Jack在学习Java");
}
}
创建课程类PythonCourse类
public class PythonCourse implements ICourse{
@Override
public void study() {
System.out.println("Jack在学习Python");
}
}
修改Jack类
public clss Jack {
public void study(ICourse course) {
course.study();
}
}
再来调用一下
public static void main(String[] args) {
Jack jack = new Jack();
jack.study(new JavaCourse);
jack.study(new PythonCourse);
}
优化后的代码,对于后续需要学习新的课程,只需要创建一个类,然后通过传参的方式告诉Jack,而不需要修改底层代码。实际上这是就是依赖注入,注入的方式有构造器方式和setter方式
构造器方式
public clss Jack {
private ICourse course;
public Jack(ICourse course) {
this.course = course;
}
public void study() {
course.study();
}
}
根据构造器方式,在调用时,每次都要创建实例,也不是很方便,那就只能选择Setter方式来进行注入
public class Jack {
private ICourse course;
public void setCourse(ICourse course) {
this.course = course;
}
public void study() {
course.study();
}
}
public static void main(String[] args) {
Jack jack = new Jack();
jack.setCourse(new JavaCourse());
jack.study();
jack.setCourse(new PythonCourse());
jack.study();
}
依赖倒置原则的本质就是通过抽象(接口或者抽象类)使各个类或者模块的实现彼此独立,互不影响,实现模块间的松耦合。以后在项目的开发中使用这个原则要遵循一下规则:
- 每个类尽量都有接口或者抽象类
- 变量的表面类型尽量是接口或者抽象类
- 任何类都不应该从具体类派生
- 尽量不要覆写基类的方法
- 如果基类是一个抽象类,而这个方法已经实现,子类尽量不要覆写。类间依赖的是抽象, 覆写了抽象方法,对依赖的稳定性会有一定的影响
最终我们来看下类图:
单一职责原则(Simple Responsibility Principle, SRP)
单一职责原则是指对象不应该承担太多功能,正如一心不能多用,唯有专注才能保证对象的高内聚;唯有唯一,才能保证对象的细粒度。比如有A和B两个类,当A需求发送改变需要修改时,不能导致B类出问题。如何解决这个问题呢?我们就要给两个类分别负责一个职责,进行解耦,后期需求变更维护互不影响
还是以课程为例,课程可以分为直播课和录播课,直播课不能快进快退,录播课可以任意的来回观看,功能职责不一样,先建一个课程类
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("录播课");
}
从上面的代码来看,Course类承担了两种处理逻辑。如果后期要对这两种课程进行一个操作,并且两种课程的操作逻辑不一样,那么就必须要修改代码,那么这样就提高了代码的复杂度,并且可维护性降低了。因此,我们要对其职责进行解耦,分别创建直播课类和录播课类
public class LiveCourse {
public void study(String courseName) {
System.out.println(courseName + "不能快进");
}
}
public class ReplayCourse {
public void study(String courseName) {
System.out.println(courseName + "可以任意的来回播放");
}
}
代码调用一下
public static void main(String[] args) {
LiveCourse liveCourse = new LiveCourse();
liveCourse.study("直播课");
ReplayCourse replayCourse = new ReplayCourse();
replayCourse.study("录播课");
}
这样修改之后,后期维护起来就比较容易了。但是,我们在实际开发中,由于项目依赖,组合等关系,很多类其实都不符合单一职责,我们也要尽可能的让接口和方法保持单一职责,对项目后期的维护是有巨大帮助的。
最后,依然来看一下类图:
接口隔离原则(interface Segregation Principle, ISP)
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
通俗的来讲就是,不要在一个接口里面放太多的方法,这样会显得很臃肿。接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,是接口更加灵活轻便
下面来看一个例子
创建一个动物行为的抽象
public interface IAnimal {
void eat();
void fly();
void swim();
}
Bird类实现
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() {}
}
从上面的代码可以看出,Bird的swim()方法可能会空着,Dog的fly()方法显然是不可能的。因此,我们要针对不同的动物行为来设计不同的接口,看如下代码
public interface IEatAnimal {
void eat();
}
public interface IFlyAnimal {
void fly();
}
public interface ISwimAnimal {
void swim();
}
Bird类实现IEatAnimal、IFlyAnimal
public class Bird implements IEatAnimal, IFlyAnimal{
@Override
public void eat() {}
@Override
public void fly() {}
}
Dog类实现IEatAnimal、ISwimAnimal
public class Dog implements IEatAnimal, ISwimAnimal{
@Override
public void eat() {}
@Override
public void swim() {}
}
通过下面的类图,就能很清晰明了
迪米特法则(Law of Demeter LOD)
迪米特法则是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle, LKP),尽量减少类与类之间的耦合,强调只与你的直接朋友交谈,不与陌生人说话。
迪米特法则中的朋友是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、方法参数等,这些对象存在关联、聚合或者组合关系,可以直接访问这些对象
现在来举一个例子,设计一个权限系统,TeamLeader需要查看目前发布到线上的课程数量。这个时候,TeamLeader要找到员工Employee进去统计,Employee再把统计结果告诉TeamLeader,接下来看代码
Course类
public class Course {
}
Employee类
public class Employee {
public void checkNumberOfCourse(List<Course> courseList) {
System.out.println("已发布的课程" + courseList.size());
}
}
TeamLeader类
public class TeamLeader {
public void commandCheckNumber(Employee employee) {
List<Course> courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
employee.checkNumberOfCourse(courseList);
}
}
测试代码
public static void main(String[] args) {
TeamLeader leader = new TeamLeader();
Employee employee = new Employee();
leader.commandCheckNumber(employee);
}
代码写到这里,其实功能已经完成了,但是根据迪米特原则,TeamLeader只需要知道结果就好,并不需要跟Course产生直接的交流,而Employee统计需要引用Course对象。TeamLeader和Course并不是朋友,从下面的类图就可以看出来
下面来对代码进行改造
Employee类
public class Employee {
public void checkNumberOfCourse() {
List<Course> courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
System.out.println("已发布的课程" + courseList.size());
}
}
TeamLader类
public class TeamLeader {
public void commandCheckNumber(Employee employee) {
employee.checkNumberOfCourse();
}
}
再看下面的类图,Course和TeamLeader已经没有关联了
里氏替换原则(Liskov SubStitution Principle)
里氏替换原则是指对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得T1定义的所有程序P在所有对象o1都替换成o2时,程序p的行为没有发生变化,那么类型T2是类型T1的子类型。
定义看上去比较抽象,我们可以简单的理解为一个软件实体如果适用一个父类的话,那一定是适用于其子类,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,我们总结一下:
子类可以扩展父类的功能,但不能改变父类原有的功能
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象的方法
- 子类中可以增加自己的特有的方法
- 当子类的方法重载父类的方法时,方法的前置添加(即方法的输入/入参)要比父类方法的输入参数更宽松
- 当子类的方法重载父类的方法时(重写/重载或实现抽象方法), 方法的后置条件(即方法的输出/返回值)要比父类更严格或相等
使用里氏替换原则有以下优点
- 约束继承泛滥,开闭原则的一种体现
- 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性。
下面用一个案例来说明里氏替换原则,我们都知道,正方形是一个特殊的长方形,下面创建一个长方形父类
Rectangle类
public class Rectangle {
private Long height;
private Long width;
public Long getHeight() {
return height;
}
public void setHeight(Long height) {
this.height = height;
}
public Long getWidth() {
return width;
}
public void setWidth(Long width) {
this.width = width;
}
}
Square类
public class Square extends Rectangle{
private Long length;
public Long getLength() {
return length;
}
public void setLength(Long length) {
this.length = length;
}
@Override
public void setHeight(Long height) {
setLength(height);
}
@Override
public Long getHeight() {
return getLength();
}
@Override
public void setWidth(Long width) {
setLength(width);
}
@Override
public Long getWidth() {
return getLength();
}
}
在Main类中创建一个resize()方法,如果宽大于或等于高,就让高一直自增
public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(5L);
rectangle.setWidth(10L);
resize(rectangle);
}
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 End, Width:" + rectangle.getWidth() + ", Height:" + rectangle.getHeight());
}
}
打印结果如下
Width:10, Height:6
Width:10, Height:7
Width:10, Height:8
Width:10, Height:9
Width:10, Height:10
Width:10, Height:11
Resize End, Width:10, Height:11
现在将Rectangle替换成Square
Square square = new Square();
square.setLength(10L);
resize(square);
这时我们运行代码就出现了死循环,违背了里氏替换原则,将父类替换成子类后,程序运行的结果没有达到预期。因此,我们的代码设计是存在一定风险的,里氏替换原则只存在父类与子类之间,约束继承泛滥。
合成复用原则(Composite/Aggregate Reuse Principe, CARP)
合成复用原则是指尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的,可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
继承相当于把所有的实现细节暴露给子类,组合/聚合是对类以外的对象无法获取到实现细节
通过一个数据库的例子来讲解一下
DBConnection类
public class DBConnection {
public String getConnection() {
return "MSQL数据库连接";
}
}
ProductDao类
public class ProductDao {
private DBConnection connection;
public void setConnection(DBConnection connection) {
this.connection = connection;
}
public void addProduct() {
String conn = this.connection.getConnection();
System.out.println("使用" + conn + "增加产品");
}
}
从上面代码看,DBConnection还不是一种抽象,不便于扩展。现在只支持MySQL数据库,后续要是业务发生改变,要支持Oracle数据库。我们可以在DBConnection中增加对Oracle数据库连接的方法,但是这样就违背了开闭原则,其实我们只需要把DBConnection修改为abstract就可以了
public abstract class DBConnection {
public abstract String getConnection();
}
将MySQL的逻辑抽离
public class MySQLConnection extends DBConnection{
@Override
public String getConnection() {
return "MySQL数据库连接";
}
}
创建Oracle支持
public class OracleConnection extends DBConnection{
@Override
public String getConnection() {
return "Oracle数据库连接";
}
}
Dao根本就不用改变,怎么选择直接交给应用层来处理
public static void main(String[] args) {
DBConnection connection = new MySQLConnection();
ProductDao dao = new ProductDao();
dao.setConnection(connection);
dao.addProduct();
}
最后来看一下类图
总结
学习设计原则,学习设计模式的基础。在实际开发过程中,并不是一定要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不是刻意追求完美,要在适当的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构