Java 设计模式基础

171 阅读15分钟

设计模式基础

1. 设计模式的定义

在软件工程中,设计模式是对软件中普遍存在(反复出现)的各种问题,所提出的解决方案

2. 设计模式的目的

我们在编写程序的过程中,会面临着来自耦合性、内聚性、以及可维护性、可拓展性、可重用性、灵活性等多方面的挑战,设计模式就是为了让程序拥有更好的:

  1. 代码重用性(即:相同功能的代码,不用多次编写)
  2. 可读性(即:编程规范性,便于其他程序员阅读和理解)
  3. 可扩展性(即:当需要增加新的功能时,非常方便,并且方便维护)
  4. 可靠性(即:代码需要有很好的鲁棒性,比如我们添加新功能后,对原有的代码不会产生影响)
  5. 使程序具有高内聚,低耦合的特性

3. 设计模式的七大原则

设计模式原则,其实就是程序员在编程时应该遵守的原则,也是各种设计模式的基础(即:设计模式为什么这么设计的依据)

1. 单一职责原则

1. 基本介绍

对类来说的,即一个类应该只负责一项原则。如类A负责两个不同的职责:职责1、职责2。当职责1需求变更而改变类A时,可能造成职责2执行错误,所以需要将类A按照职责分解为A1类和A2类,分别负责职责1和职责2。

2. 代码说明

//调用
public class MainClass{
    public static void main(String[] args){
        //对a的调用
        //此时对Vehicle来说,它既负责汽车的职责,也负责飞机和轮船的职责
        //这样就会导致飞机在公路上跑,轮船在公路上跑的错误
        Vehicle vehicle = new Vehicle();
        vehicle.run("汽车");
        vehicle.run("飞机");
        vehicle.run("轮船");
        
        //对b的调用
        RoadVehicle rv = new RoadVehicle();//只负责路上跑的
        rv.run("汽车");
        AirVehicle av = new AirVehicle();//只负责天上飞的
        av.run("飞机");
        BoatVehicle bv = new BoatVehicle();//只负责水里游的
        bv.run("轮船");
        
        //对c的调用
        Vehicle vehicle = new Vehicle();
        vehicle.roadRun("汽车");//该方法负责路上跑的
        vehicle.airRun("飞机");//该方法负责天上飞的
        vehicle.boatRun("轮船");//该方法负责水里游的
    }
}


//a. 违反单一职责原则的类
class Vehicle{
    public void run(String vehicle){
        System.out.println(vehicle + "在公路上跑");
    }
}

//b. 将Vehicle类改造成单一职责原则的类
//可以根据不同的职责将Vehicle类分解成不同的类,让每个类负责一项交通工具的职责,如下:
class RoadVehicle{
    public void run(String vehicle){
        System.out.println(vehicle + "在公路上跑");
    }
}

class AirVehicle{
    public void run(String vehicle){
        System.out.println(vehicle + "在天上飞");
    }
}

class BoatVehicle{
    public void run(String vehicle){
        System.out.println(vehicle + "在水上游");
    }
}

//c. 但上面这种分解类的方法改动较大
//改进:可以直接修改Vehicle类,让其保证方法级别上的单一职责而不是类级别上的单一职责,这样改动的代码较小,如下:
class Vehicle{
    public void roadRun(String vehicle){
        System.out.println(vehicle + "在公路上跑");
    }
    
    public void airRun(String vehicle){
        System.out.println(vehicle + "在天上飞");
    }
    
    public void boatRun(String vehicle){
        System.out.println(vehicle + "在水上游");
    }
}

3. 单一职责原则补充

  1. 降低类的复杂度,一个类只负责一项职责
  2. 提高类的可读性、可维护性
  3. 降低变更引起的风险
  4. 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中的方法数量足够少,才可以在方法级别保持单一职责原则。

2. 接口隔离原则

1. 基本介绍

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上

2. 代码说明

//假设:类A通过接口Interface依赖类B,类C通过接口Interface依赖类D。(暗示B和D是接口Interface的实现类)
//假设:Interface中有5个方法fun1,fun2,fun3,fun4,fun5。
//假设:类A需要其中的fun1,fun2,fun3,类C需要其中的fun1,fun4,fun5
//那么接口Interface对于类A和类C来说都不是他们的最小接口
//那么很显然,类A对类B的依赖不是建立在最小的接口上,类C对类D的依赖同理
//即使接口Interface对于类A和类C来说不是最小接口,类B和类D也必须实现接口Interface中类A和类C不需要的方法

//a. 违反接口隔离原则的代码
interface Interface{
    void fun1();
    void fun2();
    void fun3();
    void fun4();
    void fun5();
}

class B implements Interface{//需要实现所有方法
    public void fun1(){
        System.out.println("我是B的fun1");
    }
    public void fun2(){
        System.out.println("我是B的fun2");
    }
    public void fun3(){
        System.out.println("我是B的fun3");
    }
    public void fun4(){
        System.out.println("我是B的fun4");
    }
    public void fun5(){
        System.out.println("我是B的fun5");
    }
}

class D implements Interface{//需要实现所有方法
    public void fun1(){
        System.out.println("我是D的fun1");
    }
    public void fun2(){
        System.out.println("我是D的fun2");
    }
    public void fun3(){
        System.out.println("我是D的fun3");
    }
    public void fun4(){
        System.out.println("我是D的fun4");
    }
    public void fun5(){
        System.out.println("我是D的fun5");
    }
}

class A{
    public void dependB1(Interface i){
        i.fun1();
    }
     public void dependB2(Interface i){
        i.fun2();
    }
     public void dependB3(Interface i){
        i.fun3();
    }
}

class C{
    public void dependD1(Interface i){
        i.fun1();
    }
     public void dependD2(Interface i){
        i.fun4();
    }
     public void dependD3(Interface i){
        i.fun5();
    }
}

//对上面进行修改,使之遵从接口隔离原则
//可以对接口进行拆分,然后让类A和类C分别与他们需要的接口建立依赖关系。
//这些接口的实现类只需要分别实现类A和类C需要的方法
//也就是说此时类A对类B、类C对类D的依赖都是建立在最小接口上

//接口拆分
interface Interface1{
    void fun1();
}
interface Interface2{
    void fun2();
    void fun3();
}
interface Interface3{
    void fun4();
    void fun5();
}

class B implements Interface1,Interface2{//类B实现接口1和接口2,只需要实现这两个接口中的所有方法
    public void fun1(){
        System.out.println("我是B的fun1");
    }
    public void fun2(){
        System.out.println("我是B的fun2");
    }
    public void fun3(){
        System.out.println("我是B的fun3");
    }
}

class D implements Interface1,Interface3{//类D实现接口1和接口3,只需要实现这两个接口中的所有方法
    public void fun1(){
        System.out.println("我是D的fun1");
    }
    public void fun4(){
        System.out.println("我是D的fun4");
    }
    public void fun5(){
        System.out.println("我是D的fun5");
    }
}

class A{
    public void dependB1(Interface1 i){//通过接口1依赖B,比如这里可以传入new B()
        i.fun1();
    }
     public void dependB2(Interface2 i){//通过接口2依赖B,比如这里可以传入new B()
        i.fun2();
    }
     public void dependB3(Interface2 i){
        i.fun3();
    }
}

class C{
    public void dependD1(Interface1 i){//通过接口1依赖D,比如这里可以传入new D()
        i.fun1();
    }
     public void dependD2(Interface3 i){//通过接口3依赖D,比如这里可以传入new D()
        i.fun4();
    }
     public void dependD3(Interface3 i){
        i.fun5();
    }
}

3. 依赖倒转原则

1. 基本介绍

依赖倒转原则是指:

  1. 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象
  3. 依赖倒转的中心思想是面向接口编程
  4. 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
  5. 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成

2. 代码说明

//比如一个Person类实现打印消息功能
//未遵守依赖倒转原则的代码
class Email{
    public String getMsg(){
        return "email 消息";
    }
}

class Person{
    //很明显,如果我们这样写的话,Person直接依赖Email类,那么以后的拓展就会大受限制
    //比如我们还需要Person打印qq消息,短信消息等等,那么Person类的这个方法也需要进行修改
    public void printMsg(Email email){
        System.out.println(email.getMsg());
    }
}

//对上面改进使之遵守依赖倒转原则
//也就是依赖抽象而不是依赖细节
interface Msg{
    String getMsg();
}

class Email implements Msg{
    public String getMsg(){
        System.out.println("Email消息");
    }
}

class QQ implements Msg{
    public String getMsg(){
        System.out.println("QQ消息");
    }
}

class Person{
    //依赖接口,这样只需要我们传入不同的实现类对象,就可以打印不同信息
    public void printMsg(Msg msg){
        System.out.println(msg.getMsg());
    }
}

3. 依赖关系传递的三种方式以及代码说明

  1. 接口传递
  2. 构造方法传递
  3. setter方法传递
//1. 通过接口传递
public interface ITV {
    void play();
}

public interface IOpenAndClose {
    void open(ITV itv);
}

public class ChangHongTV implements ITV {
    @Override
    public void play() {
        System.out.println("长虹电视打开了");
    }
}

public class OpenAndClose implements IOpenAndClose {
    @Override
    public void open(ITV itv) {
        itv.play();
    }
}

//2. 通过构造方法传递依赖关系
public interface ITV {
    void play();
}

public interface IOpenAndClose {
    void open();
}

public class ChangHongTV implements ITV {
    @Override
    public void play() {
        System.out.println("长虹电视打开了");
    }
}

public class OpenAndClose implements IOpenAndClose{
    private ITV itv;
    public OpenAndClose(ITV itv){
        this.itv = itv;
    }
    
    public void open(){
        itv.play();
    }
}

//2. 通过setter方法传递依赖关系
public interface ITV {
    void play();
}

public interface IOpenAndClose {
    void open();
    void setITV(ITV itv);
}

public class ChangHongTV implements ITV {
    @Override
    public void play() {
        System.out.println("长虹电视打开了");
    }
}

public class OpenAndClose implements IOpenAndClose{
    private ITV itv;
    
    public void setITV(ITV itv){
        this.itv = itv;
    }
    
    public void open(){
        itv.play();
    }
}

//测试
public class MainTest {
    @Test
    public void t1() {//测试通过接口传递依赖关系
        ITV changhong = new ChangHongTV();
        OpenAndClose oc = new OpenAndClose();
        oc.open(changhong);
    }
    
    @Test
    public void t2() {//测试通过构造方法传递依赖关系
        ITV changhong = new ChangHongTV();
        OpenAndClose oc = new OpenAndClose(changhong);
        oc.open();
    }
    
    @Test
    public void t2() {//测试通过setter方法传递依赖关系
        ITV changhong = new ChangHongTV();
        OpenAndClose oc = new OpenAndClose();
        oc.setITV(changhong);
        oc.open();
    }
}

4. 依赖倒转原则的补充

  1. 低层模块尽量都要有抽象类或接口,或者二者都有,程序稳定性更好。
  2. 变量的声明类型尽量是接口或抽象类,这样我们的变量引用和实际对象之间,就存在一个缓冲层,利于程序拓展和优化
  3. 继承时遵循里氏替换原则

4. 里氏替换原则

1. OO中的继承性的思考和说明

  1. 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵守这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
  2. 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他类继承,则当这个类修改时,必须考虑到所有的子类,并且父类修改之后,所有涉及到子类的功能都有可能产生故障。
  3. 问题提出:如何在编程中正确使用继承?–>里氏替换原则

2. 介绍与理解

  1. 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明的使用其子类的对象。
  2. 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
  3. 里氏替换原则告诉我们,继承实际上使两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题
  4. 里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事
  5. 如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
  6. 不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。
  7. 如何符合LSP?总结一句话 ——就是尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。

5. 开闭原则

1. 基本介绍

  1. 开闭原则是编程中最基础、最重要的设计原则
  2. 一个软件实体如类,模块和函数应该对扩展开放(提供方),对修改关闭(使用方)也就是说尽量在不修改原有代码的基础上实现扩展。用抽象构建框架,用实现拓展细节
  3. 当软件需要变化时,尽量通过拓展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
  4. 编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则

2. 代码说明

//1. 违反开闭原则的代码

//基类
class Shape{
    int type;
}

//用于绘图的类【使用方】
class GraphEditor{
    //接受Shape对象,然后根据对象的type来绘制不同的图形
    public void drawShape(Shape s){
	    if(s.type == 1) drawRec(s);
        else if(s.type == 2) drawCir(s);
    }
    
    //绘制矩形
    public void drawRec(Shape s){
        System.out.println("绘制矩形");
    }
    
    //绘制圆形
    public void drawCir(Shape s){
        System.out.println("绘制圆形");
    }
    
}

//矩形类【提供方】
class Rec extends Shape{
    Rec(){
        super.type = 1;
    }
}

//圆形类【提供方】
class Cir extends Shape{
    Cir(){
        super.type = 2;
    }
}

//假如我们现在需要实现绘制三角形,通过上面的代码1,需要:
//1.添加三角形类
//2.修改GraphEditor,添加绘制三角形方法和修改drawShape,很明显,修改了使用方,违背了ocp原则

//2. 修改上面代码使之遵循ocp原则
//基类
abstract class Shape{
    int type;
    
    public abstract void draw();
}

//用于绘图的类【使用方】
class GraphEditor{
    //接受Shape对象,根据不同的对象调用其draw方法
    public void drawShape(Shape s){
	    s.draw();
    } 
}

//矩形类
class Rec extends Shape{
    Rec(){
        super.type = 1;
    }
    
    public void draw(){
        System.out.println("绘制矩形")
    }
}

//圆形类
class Cir extends Shape{
    Cir(){
        super.type = 2;
    }
    
    public void draw(){
        System.out.println("绘制圆形")
    }
}

//很明显,上面代码2如果想增加绘制三角形功能,只需要添加三角形类,即只需要拓展提供方
//不需要修改GraphEditor类,即不需要修改使用方,符合ocp原则

6. 迪米特法则

1. 基本介绍

  1. 一个对象应该对其他对象保持最少的了解
  2. 类与类关系越密切,耦合度越大
  3. 迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息。
  4. 迪米特法则还有个更简单的定义:只与直接的朋友通信
  5. 直接的朋友:每个对象都会与其他对象有耦合关系,只要俩个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友,也就是说,陌生的类最好不要以局部变量的形式出现在当前类中

2. 代码说明

//学校总部员工类
class Employee{
    private int id;
    
    public void setId(int id){
        this.id = id;
    }
    public int getId(){
        return this.id;
    }
}

//学院的员工类
class CollegeEmployee{
    private int id;
    
     public void setId(int id){
        this.id = id;
    }
    public int getId(){
        return this.id;
    }
}

//管理学院员工的管理类
class CollegeManager{
    //返回学院所有员工的
    public List<CollegeEmployee> getAll(){
        List<CollegeEmployee> res = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            CollegeEmployee c = new CollegeEmployee();
            c.setId(i);
            res.add(c);
        }
        return res;
    }
}

//学校管理类
class SchoolManager{
    //返回学校总部的所有员工
    public List<Employee> getAll(){
        List<Employee> res = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            Employee c = new Employee();
            c.setId(i);
            res.add(c);
        }
        return res;
    }
    
    //打印
    public void print(CollegeManager cm){
        //打印学院员工
        List<CollegeEmployee> list1 = cm.getAll();
        //打印逻辑
        
        //打印学校总部所有员工
        List<Employee> list2 = this.getAll();
        //打印逻辑
    }
}

//分析:SchoolManager类的直接朋友类有Employee,CollegeManager
//而CollegeEmployee对SchoolManager来说是陌生类,因为它是以局部变量的形式出现在SchoolManager中的
//违反了迪米特法则


//改进:将不是直接朋友的CollegeEmployee放到CollegeManager中。
//即List<CollegeEmployee> list1 = cm.getAll();这个逻辑封装到CollegeManager类中

//管理学院员工的管理类
class CollegeManager{
    //返回学院所有员工的id
    public List<CollegeEmployee> getAll(){
        List<CollegeEmployee> res = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            CollegeEmployee c = new CollegeEmployee();
            c.setId(i);
            res.add(c);
        }
        return res;
    }
    
    //打印学院员工信息
    
    public void print(){
        List<CollegeEmployee> res = getAll();
        //打印逻辑
    }
}

//然后在学校管理类中打印方法调用CollegeManager的打印方法即可
//学校管理类
class SchoolManager{
    //返回学校总部的所有员工
    public List<Employee> getAll(){
        List<Employee> res = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            Employee c = new Employee();
            c.setId(i);
            res.add(c);
        }
        return res;
    }
    
    //打印
    public void print(CollegeManager cm){
        //打印学院员工
        cm.print();
               
        //打印学校总部所有员工
        List<Employee> list2 = this.getAll();
        //打印逻辑
    }
}

3. 米特法则补充

  1. 迪米特法则的核心是降低类之间的耦合度
  2. 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系

7. 合成复用原则

尽量使用组合、聚合和依赖关系,少用继承关系

8. 七大设计原则总结

  1. 找出应用中可能需要变化之处,将它们独立出来,不要和那些不需要的变化的代码混在一起
  2. 针对接口编程,而不是针对实现编程
  3. 为了交互对象之间的低耦合设计而努力