设计模式即一种解决问题的最佳实践。

0、UML图

Unified Modeling Language,UML,统一建模语言。图形化表达软件设计的动态与静态信息。下面用UML中的类图来表示类的静态信息:

在这里插入图片描述
以上的等价代码:

public class Employee{

	private String name;
	private int age;
	private String address;
	public void work(){
		//...
	}
}

用UML的类图表示类与类之间的关系:

关联关系

  • 单向关联:如老师类中有个属性类型为学生类,顾客有个地址属性:

在这里插入图片描述

  • 双向关联:两个类各自持有对方类型的成员变量

在这里插入图片描述

  • 自关联,如Node类包含Node类型的成员变量

在这里插入图片描述

聚合关系

强关联关系,成员对象是整体对象的一部分,但成员对象可以脱离整体对象独立存在。如大学类和老师类,大学没了,老师可再找工作。空心菱形表示,且空心菱形一侧是整体的类

在这里插入图片描述

组合关系

更强烈的一种聚合关系,强烈体现在:整体对象不存在的化,部分对象也将不存在,如Head类和Mouth类。用实心菱形表示:

在这里插入图片描述

依赖关系

一个类的方法通过局部变量或者形参来访问另一个类的某个方法。如Driver类有形参Car car,并调用了car类的move方法

在这里插入图片描述

继承关系

实线 + 实线箭头从子类指向父类:

在这里插入图片描述

实现关系

虚线+虚线箭头在这里插入图片描述

1、开闭原则

  • 对扩展开放,对修改关闭
  • 需求拓展时,尽量不改旧代码,热插拔的效果
  • 实现思路:使用抽象类或接口进行构建,使用实现类扩展细节
  • 面向抽象编程
  • 提高了可扩展性和可维护性

以搜狗输入法支持切换各种主题皮肤为例,皮肤对象就是背景图片、颜色、字体等属性的组合。思路是:定义个抽象类,具体皮肤是其子类。搜狗输入法注入这个抽象类的对象来进行展示。类图:

在这里插入图片描述

皮肤的抽象类:

public abstract class AbstractSkin {
    /**
     * 展示输入法皮肤
     */
    public abstract void display();
}

实现类:

/**
 * 默认皮肤类
 */
public class DefaultSkin extends AbstractSkin{

    @Override
    public void display() {
        System.out.println("默认皮肤");
    }
}

/**
 * 自定义皮肤一
 */
public class AnotherSkin extends AbstractSkin{
    @Override
    public void display() {
        System.out.println("自定义皮肤一");
    }
}

搜狗输入法,聚合抽象类,展示皮肤:

/**
 * 搜狗输入法
 * 通过成员变量,聚合抽象类
 */
public class SouGouInput {

    private AbstractSkin skin;  //聚合抽象类

    /**
     * 提供set,方便注入和初始化赋值,当然用构造方法来初始抽象类对象也可
     */
    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }

    public void display(){
        skin.display();
    }
}

客户端模拟:

/**
 * 客户端
 */
public class ClintTest {
    public static void main(String[] args) {
        //搜狗输入法对象
        SouGouInput souGouInput = new SouGouInput();
        //创建皮肤对象
        DefaultSkin defaultSkin = new DefaultSkin();
        //将皮肤设置到输入法中
        souGouInput.setSkin(defaultSkin);
        //展示
        souGouInput.display();
    }
}

在这里插入图片描述

此后,再扩展新皮肤,写实现类即可。

2、里氏替换原则

是继承复用的基石(对子类的继承关系进行约束),是对开闭原则的补充。里氏替换原则即:任何父类可以出现的地方,子类一定可以出现。比如某个方法的形参类型是父类A,那传个A的子类B也要行得通。为此,开发时,建议:

  • 子类可以扩展父类的功能,但尽量不要重写父类原有的功能
  • 重继承和复用,轻覆盖
  • 子类可以实现父类的抽象方法,但是不能覆盖父类原有的方法
  • 子类中可以增加自己特有的方法

以长方形和正方形为例:正方形属于长方形的一种,因此首先想到继承:

/**
 * 长方形
 */
public class Rectangle {

    private double length;
    private double width;

    public double getLength() {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }
}

注意这里正方形做为子类,重写了父类的set方法

/**
 * 正方形
 */
public class Square extends Rectangle{

    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setLength(width);
    }
}

写测试类,定义一个扩宽的方法:

public class RectangleDemo {
    public static void main(String[] args) {
        //长方形
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        resize(r);
        printLengthAndWidth(r);
        //正方形
        System.out.println("=====");
        Rectangle r2 = new Square();
        r2.setWidth(10);
        resize(r2);
        printLengthAndWidth(r2);

    }
    /**
     * 扩宽方法
     * 当宽小于等于长时,宽+1
     */
    public static void resize(Rectangle rectangle){
        while(rectangle.getWidth() <= rectangle.getLength()){
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    /**
     * 打印长方形的长和宽
     */
    public static void printLengthAndWidth(Rectangle rectangle){
        System.out.println(rectangle.getLength());
        System.out.println(rectangle.getWidth());
    }
}

类图:

在这里插入图片描述

在这里插入图片描述

发现传入长方形正常,传入正方形则死循环,因为正方形这个子类重,set被重写,宽 <= 长始终满足。不满足里氏替换了。关于这个问题的改进:引入一个抽象的四边形接口Quadrilateral:

在这里插入图片描述

/**
 * 四边形
 */
public interface Quadrilateral {

    public double getLength();
    public double getWidth();
}

正方形和长方形做为四边形的子类:

/**
 * 长方形
 */
public class Rectangle implements Quadrilateral{

    private double length;
    private double width;

    @Override
    public double getLength() {
        return this.length;
    }

    @Override
    public double getWidth() {
        return this.width;
    }

    public void setLength(double length) {
        this.length = length;
    }

    public void setWidth(double width) {
        this.width = width;
    }
}

四边形只有get方法,正方形的set方法属于是新增的方法

/**
 * 正方形
 */
public class Square implements Quadrilateral{

    private double side;

    @Override
    public double getLength() {
        return this.side;
    }

    @Override
    public double getWidth() {
        return this.side;
    }

    public void setSide(double side) {
        this.side = side;
    }
}

测试类:

public class RectangleTest {
    public static void main(String[] args) {
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        resize(r);
        printLengthAndWidth(r);

        Square square = new Square();
        square.setSide(10);
        printLengthAndWidth(square);
    }
    /**
     * 扩宽方法
     * 当宽小于等于长时,宽+1
     */
    public static void resize(Rectangle rectangle){
        while(rectangle.getWidth() <= rectangle.getLength()){
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    /**
     * 打印长方形的长和宽
     */
    public static void printLengthAndWidth(Quadrilateral quadrilateral){
        System.out.println(quadrilateral.getLength());
        System.out.println(quadrilateral.getWidth());
    }
}

对于resize方法,形参为长方形,正方形不再满足,当然也可instance of + 形参类型是四边形。总之:里氏替换要求父类可满足的,子类也可满足,为了达到这一点,coding时,对于父类已经实现的方法,尽量避免重写,以防违背里氏替换原则,带来Bug。

3、依赖倒转原则

高层模块不应该直接依赖底层模块,而是应该依赖其抽象。

在这里插入图片描述

  • 是开闭原则的一种落地
  • 面向接口进行编程
  • 依赖抽象,而不是具体的实现
  • 减少类与类之间的耦合性,提高系统稳定性,降低修改程序所造成的风险

以组装电脑为例,配件有cpu、硬盘、内存条。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。能想到的最直接的实现的类图:

在这里插入图片描述

类图翻译代码:

在这里插入图片描述

定义电脑类,其聚合硬盘、内存、CPU等类:

@Data
public class Computer{

	private XiJieHardDisk hardDisk;
	private IntelCpu cpu;
	private KingstonMemory memory;

	//组装运行电脑
	public void run(){
		System.out.println("计算机工作");
		cpu.run();
		memory.save();
		String data = hardDisk.get();
		System.out.println("从硬盘获取的数据为:" + data);
	}
}

测试类:

在这里插入图片描述

很明显,这么写,是直接依赖了其底层模块,而不是依赖抽象。当要换个厂商的CPU或者硬盘,就得大改Computer代码。改进方案的类图(对下游类抽取成一个接口):

在这里插入图片描述

部分代码:

//硬盘、内存以此类推
public interface Cpu{

	public void run();
}

后续换不同的厂商,就写不同的实现类,而Computer自然要去依赖下游类的抽象(即Cpu接口,而非IntelCpu类):

@Data
public class Computer{

	private HardDisk hardDisk;
	private Cpu cpu;
	private Memory memory;

	//组装运行电脑
	public void run(){
		System.out.println("计算机工作");
		cpu.run();
		memory.save();
		String data = hardDisk.get();
		System.out.println("从硬盘获取的数据为:" + data);
	}
}

测试类:

public class Test{

	//创建计算机组件对象
	HardDisk hardDisk = new SiJieHardDisk();
	Cpu cpu = new IntelCpu();
	Memory memory = new KingstonMemory();
	//创建计算机对象
	Computer c = new Computer();
	//组装计算机
	c.setCpu(cpu);
	c.setHardDisk(hardDisk);
	c.setMemory(memory);
	//运行计算机
	c.run();
}

4、接口隔离原则

客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。

在这里插入图片描述

因此:

  • 用多个接口,而不是使用单一的接口
  • 尽量细化接口,接口中的方法尽量少
  • 符合低耦合的设计思想,提高了可扩展性和可维护性

举个实际例子:安全门具有防火、防水、防盗功能,如果将三个功能对应的方法都放在一个接口,写的实现类如下:

在这里插入图片描述

此时,有个品牌只支持防火和防水,直接继承SafetyDoor就会出问题。根据接口隔离原则,可拆成三个接口,类图如下:

在这里插入图片描述

5、迪米特原则

如果两个对象或者软件实体无需直接通信,那就不应当发生直接的掉哦那个,可以通过第三方转发该调用,已达到解耦的目的。如需要做软件的公司应该去找开发软件的公司,而不是工程师个人。

在这里插入图片描述

  • 一个对象应该对其他对象保持最少的了解(最少知道原则)
  • 为了降低类与类之间的耦合
  • 强调只和朋友交流,不和陌生人说话
  • 这里的朋友指的是当前对象的成员变量对象、当前对象的方法的输入或输出参数,它们与当前对象存在关联、聚合、组合关系,可直接访问这些对象的方法。

案例一:开发者 --> 领导 --> 老板

实现思路:

  • 需要和老板类的对象直接通信的是领导类的对象,这里以形参的方式
  • 需要和领导类的对象直接通信的是开发者类的对象,这里以成员变量的形式
//人
public interface Human {
}
//代码开发者
public abstract class Programmer implements Human {

	public abstract void work();
	
}
public class JavaProgrammer  extends Programmer {

	@Override
	public void work() {
		System.out.println("Java程序员在做开发工作");
	}
}
public class PythonProgrammer  extends Programmer {

	@Override
	public void work() {
		System.out.println("Python程序员在做开发工作");
	}
}
//管理者
public interface Manager extends Human{
	
}

老板类:

public class Boss implements Manager{
	
	public void meet(TeamLeader teamLeader){      //需要和老板类的对象直接通信的是领导类的对象,这里以形参的方式
		System.out.println("老板开会,分布任务");
		//teamLeader.assginTasks();
	}
}

项目经理类:

public class TeamLeader implements Manager{
	
	private Programmer programmer;     //需要和领导类的对象直接通信的是开发者类的对象,这里以成员变量的形式。
	
	public void setProgrammer(Programmer programmer){   
		this.programmer = programmer;
	}
	public void assginTasks(){
		System.out.println("给下属分配任务");
		if(this.programmer instanceof JavaProgrammer){
			//是Java开发者
			(JavaProgrammer)programmer.work();
		}else if(this.programmer instanceof PythonProgrammer){
			//是Python开发者
			(PythonProgrammer)programmer.work();
		}
		
		
	}
}
public class Test{

	public static void main(String[] args){
		Boss boss = new Boss();
		TeamLeader teamLeader = new TeamLeader();
		JavaProgrammer javaProgrammer = new JavaProgrammer();
		PythonProgrammer pythonProgrammer = new PythonProgrammer();

		//老板只负责通知项目经理开会
		boss.meet(teamLeader);
		//项目经理根据会议内容进行任务分配
		teamLeader.setProgrammer(javaProgrammer);
		teamLeader.assginTasks();
		teamLeader.setProgrammer(pythonProgrammer);
		teamLeader.assginTasks();
	}
}

Boss只与TeamLeader有关,TeamLeader则只与Programmer有关

在这里插入图片描述

案例二:明星 --> 经纪人 --> 粉丝

明星许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的"朋友",而粉丝和媒体公司是"陌生人",所以适合使用迪米特法则

在这里插入图片描述

明星类:

public class Star {

	private String name;
	
	public Star(String name) {
		this.name=name;
	}
	public String getName() {
		return name;
	}
}

粉丝类:

public class Fans {

	private String name;
	
	public Fans(String name) {
		this.name=name;
	}
	
	public String getName() {
		return name;
	}
}

媒体公司类:

public class Company {

	private String name;
	
	public Company(String name) {
		this.name=name;
	}
	
	public String getName() {
		return name;
	}
}

经纪人类,是粉丝、明星的"朋友",在这里进行聚合:

public class Agent {

	private Star star;
	
	private Fans fans;
	
	private Company company;
	
	public void setStar(Star star) {
		this.star = star;
	}
	
	public void setFans(Fans fans) {
		this.fans = fans;
	}
	
	public void setCompany(Company company) {
		this.company = company;
	}
	
	public void meeting() {
		System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。");
	}
	
	public void business() {
		System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
	}
}

6、合成复用原则

  • 尽量使用对象组合,而不是使用继承关系,以达到代码复用的目的
  • 如此就降低了类与类之间的耦合,一个类的变化对其他类造成的影响较小

这里的对象组合,指的是前面类图重的组合和聚合关系,通过成员变量或者形参等等,将已有的对象纳入新对象重,新对象通过已有对象类型的属性也可以调用已有类的方法。这比继承好,依赖继承破坏看封装性,二来有耦合,父类变,子类也得变。举个例子,车按动力分为汽油、电动。按颜色分为红黑白等。如果用继承来实现就是:

在这里插入图片描述

可以看到继承复用会产生很多子类,考虑用聚合复用,把颜色类做为属性(成员变量),明显更优:

在这里插入图片描述

如果现在需求变更,有个光能汽车,对继承复用就得新增三个类:

在这里插入图片描述

对聚合复用则只需增加一个类即可,因此日常开发优先考虑聚合复用。

在这里插入图片描述

7、单一原则

  • 一个类、一个接口、一个方法,只去负责一项职责
  • 降低程序复杂度以及代码变更带来的风险

更多推荐