设计模式--7大原则

1_1.设计模式

讲师:Tom

时长:1h8min

Date:4/10/2020 8:52-9:30

前提知识:

       设计模式,spring相关

适合人群:

             

1.为什么要从设计模式学起

为了解耦,总结前人的经验、方便阅读代码。

7大设计原则:

       》开闭原则---对扩展开放,对修改关闭

       》单一职责原则---一个方法只做一件事

       》依赖倒置原则---

       》接口隔离原则---尽量保证接口纯洁性

       》迪米特法则---最少依赖原则----封装与隐藏

       》里氏替换原则

       》合成复用原则----多用聚合,少用继承  

学习它,可以更好的优化,重构代码:

如:多分支if结构-----》提取类的方式,

好处:

       》写出更加优雅的代码

       》帮助重构代码

       》经典框架中大量使用设计模式   

Spring用到的设计模式:

》工厂模式

》装饰者模式

》代理模式—aop

>单例模式

》委派模式 DispatcherServlet

>策略模式 HandlerMapping

>适配器模式 handlerAdapter

>模板方法模式 jdbcTemplate

>观察者模式ContextLoaderListener

推荐学习资料:

》《软件架构设计七大原则》课件

》《大话设计模式》

》《Head First设计模式》

》《设计模式-可复用面向对象软件的基础》

》《Effective Java》

2.工厂模式

基于spring

Spring Ioc 工厂、单例

Spring aop 代理、观察者

Spring mvc 适配器

Spring jdbc 模板方法

2.1.什么是工厂模式?

2.1.1简单工厂模式Simple Factory Pattern

由工厂对象决定创建出哪一种产品类的实例。【工厂创建对象,对象创建过程封装,隐藏起来】

属于创建型模式,不属于GOF,23种设计模式。

2.1.1.1.代码实现

 1.1.1.jdbc连接数据库

步骤如下:

1.注册驱动,获致连接Connection

2.创建Statement对象

3.execute()执行sql

4.把结果集转换成pojo对象

5.关闭资源

思考:

当项目较大,直接使用原生api会带来什么问题?

》代码大量重复

》结果集处理太复杂,麻烦

》连接管理不方便

》sql语句硬编码

       我需要关闭资源,如果忘记关闭,可能造成数据库服务异常。

       我们对数据库的业务逻辑,与资源管理是耦合在一起的,对开发非常不利。

       为了解决这些问题,出现一框架,如:

Mybatis,hibernate,spring-jdbc,

Apache DbUtils

       >QueryRunner

       >ResultSetHandler

它主要解决了结果集封装问题。它支持不同数据源:c3p0, jdbc,druid,hikari

Spring-jdbc:

>实现RowMapper接口,mapRow()方法

》转换结果集Object

方法的封装,是通过jdbcTemplate完成。

资源管理 ----注入数据源dataSource

结果集处理---RowMapper---重写mapRow

总结:

       以上工具,解决了一些问题:

》方法封装

》支持数据源

》映射结果集

       没解决的问题:

1.sql语句硬编码

2.参数只能顺序传入【占位符的方式】

3.没有实现实体类到数据库记录的映射

4.没有提供缓存等功能

2.体系结构与工作原理

3.插件原理及spring集成

4.手写mybatis

1.1.2.Orm框架

/**=======================================================================华丽的分隔线================================================*/

 1_1.7大软件设计原则

1.1.1.开闭原则

  Open-Closed Principle【OCP原则】。

定义

  一个软件实体,如:类,模块和函数应该对扩展开放对修改关闭

  用抽象构建框架,用实现扩展细节。

优点:

  提高软件系统的可复用性可维护性

它的核心思想:

  面向抽象编程,面向接口编程。

生活实例体现:

  实行弹性工作制。8小时工作时间是固定的【对修改关闭】,而上下班时间灵活调整【对扩展开放】。

1.1.2.依赖倒转原则

  Dependence Inversion Principle【DIP原则】。

定义:

  高层模块不应该依赖低层模块,二者都应该依赖其抽象。而不应该依赖细节,细节应该依赖抽象。

核心思想:

  面向接口编程,不要面向实现编程。

优点:

  可以减少类间耦合性,提高系统稳定性,提高代码可读性和可维护性。

  可降低程序所造成的风险。

 1.1.3.单一职责原则

  Simple Responsibility Principle【SRP原则】

定义:

  不要定义多于一个导致类变更的原因。

  一个类、接口、方法只负责一项职责。

优点:

  降低类和复杂度

  提高类的可读性

  提高系统的可维护性

  降低变更引起的风险

1.1.4.接口隔离原则

  Interface Segregation principle[ISp原则]

定义:

  用多个专门的接口,而不是使用一个单一的总接口。

  客户端不应该依赖它不需要的接口。

注意:  

  一个类对应一个类的依赖,应该建立在最小的接口上

  建立单一接口,不要建立庞大臃肿的解耦

  尽量细化接口,接口中方法尽量少

注意适度原则,一定要适度

优点:  

  符合我们常说的高内聚,低耦合的设计思想。

  从而使得类具有良好的可读性,可扩展性和可维护性。

 1.1.5.迪米特法则

  Law of Demeter.

 

定义:

  一个对象应该对其他对象保持最少的了解。又称最少知道原则

  尽量降低类与类之间的耦合度。【优点】

理解:

  强调只和朋友交流,不和陌生人说话。

  

朋友:

  出现在成员变量、方法的输入、输出参数中的类成为成员朋友类,而出现在方法体内部的类不属于朋友类。

 1.代码示例

 A.先写一个违背最少知道原则的小型系统

【1】业务类---course课程信息
public class Course {
}
【2】核心类--TeamLeader领导----给员工下命令
public class TeamLeader {
    /**
     * 给员工发命令,检查课程总数
     */
    public void commandCheckNumber(Employee employee){
        List<Course> courses = new ArrayList<>();
        for(int i=0;i<20;i++){
            courses.add(new Course());
        }
        //员工检查课程数量
        employee.checkNumberOfCourses(courses);
    }
}
【3】业务类---Employee员工类
public class Employee {
    //检查课程总数
    public void checkNumberOfCourses(List<Course> courseList){
        System.out.println("目前发布的课程数量为:"+courseList.size());
    }
}

[4]测试--领导安排的事情

public class LodTest {
    public static void main(String[] args) {
        TeamLeader leader = new TeamLeader();
        Employee employee = new Employee();
        leader.commandCheckNumber(employee);
        //目前发布的课程数量为:20
    }
}

B.系统类图

 

分析类图:

  TeamLeader,通过发布命令,让员工来检查课程数量,似乎是没有什么问题。

  TeamLeader其实想要的是一个结果【数量是多少】,并不关心课程Course有哪些?所以,TeamLeader不应该和Course有关联关系。

  这里违反了最少依赖原则。

  怎么来解决这个问题呢?

  我们发现,Employee与Course明显存在依赖关系。所以,可以把Leader中与Course相关代码移到Employee中。

【1】改造代码Employee和TeamLeader

移除TeamLeader类中Course相关代码。

public class TeamLeader {
    /**
     * 给员工发命令,检查课程总数
     */
    public void commandCheckNumber(Employee employee){
       /* List<Course> courses = new ArrayList<>();
        for(int i=0;i<20;i++){
            courses.add(new Course());
        }*/
        //员工检查课程数量
        //employee.checkNumberOfCourses(courses);
        employee.checkNumberOfCourses();
    }
}

Course相关代码迁移到Employee中:

public class Employee {
    //检查课程总数
    public void checkNumberOfCourses(){
        List<Course> courses = new ArrayList<>();
        for(int i=0;i<20;i++){
            courses.add(new Course());
        }
        System.out.println("目前发布的课程数量为:"+courses.size());
    }
}
【2】再次查看类图

 显然,现在的系统严格遵守最少知道原则。

 1.1.6.里氏替换原则

  Liskov Substitution Principle【LSP】。

 定义

  如果对每一个类型为T1的对象obj1,都有类型T2的对象obj2,使得以T1定义的所有程序P在所有对象obj1都替换成obj2时,

程序P的行为没有发生变化 ,那么类型T2是类型T1的子类型。

 定义扩展:

  一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,

子类对象能够替换父类对象,而程序逻辑不变。

 

引申定义

  子类可以扩展父类的功能,但是不能改变父类的原有功能【就是多态的应用】

含义1:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

含义2:子类中可以增加自己特有的方法【父类中没有的方法】

含义3:当子类的方法重载父类的方法时,方法的前置条件【当方法的输入,入参】要比父类方法的输入参数更宽松。

含义4:当子类的方法实现父类的方法时【重写,重载或实现抽象方法】,方法的后置条件【方法返回值】,要比父类更严格

或相等。

  

 优点:

  1.约束继承泛滥,是开闭原则的一种体现。

  2.加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的可维护性,扩展性

  降低需求变更时引入风险。

1.代码示例

A.先写一个违反里氏替换原则的小型系统

这里以正方形和长方形的关系,进行举例:

【1】定义一个长方形类Rectangle
package com.wf.design_principle.liskovsubstitution.simple;

/**
 * @ClassName Rectangle
 * @Description 长方形
 * @Author wf
 * @Date 2020/4/26 13:04
 * @Version 1.0
 */
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;
    }
}
[2]定义一个正方形Square

因为正方形,是特殊的长方形。这里继承Retangle

package com.wf.design_principle.liskovsubstitution.simple;

/**
 * @ClassName Square
 * @Description TODO
 * @Author wf
 * @Date 2020/4/26 13:06
 * @Version 1.0
 */
public class Square extends Rectangle {
    /** 边长 */
    private Long length;

    public Long getLength() {
        return length;
    }

    public void setLength(Long length) {
        this.length = length;
    }
    //覆盖父类的方法

    @Override
    public Long getHeight() {
        return this.getLength();
    }

    @Override
    public void setHeight(Long height) {
        this.setLength(height);
    }

    @Override
    public long getWidth() {
        return this.getLength();
    }

    @Override
    public void setWidth(long width) {
        this.setLength(width);
    }
}
【3】测试类
package com.wf.design_principle.liskovsubstitution.simple;

/**
 * @ClassName SimpleTest
 * @Description TODO
 * @Author wf
 * @Date 2020/4/26 13:13
 * @Version 1.0
 */
public class SimpleTest {
    //测试长方形
   /* public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(20L);
        rectangle.setHeight(10L);
        resize(rectangle);
    }*/
    //测试正方形
    public static void main(String[] args) {
        Square square = new Square();
        square.setLength(10L);
        resize(square);
    }

    public static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() >= rectangle.getHeight()){
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println(""+rectangle.getWidth() +",Height:"+rectangle.getHeight());
    }
        System.out.println("Resize End ,"+rectangle.getWidth() +",Height:"+rectangle.getHeight());
    }
}

测试结果说明:

  正方形Square作为子类,当它同样调用父类方法时,由于while条件恒为true,导致代码陷入死循环。

  显然,这里违反里氏替换原则。    

那么,如何解决这个问题呢?

  正方形Square和长方形Rectangle,虽然存在一定特殊关系,但它们定义为父子关系,有些不太合适。

  反而,正方形和长方形都是四边形。 这里把它们处理为兄弟关系

  

 B.改造代码
[1].定义四边形---处理为接口
package com.wf.design_principle.liskovsubstitution;

/**
 * @ClassName QuadRectangle
 * @Description 四边形
 * @Author wf
 * @Date 2020/4/26 14:10
 * @Version 1.0
 */
public interface QuadRectangle {
    public Long getWidth();
    public Long getHeight();
}
【2】定义长方形,实现四边形
package com.wf.design_principle.liskovsubstitution;

/**
 * @ClassName Rectangle
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 10:33
 * @Version 1.0
 */
public class Rectangle implements QuadRectangle {
    private Long height;
    private Long width;

    public Long getHeight() {
        return height;
    }

    public void setHeight(Long height) {
        this.height = height;
    }

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

    public Long getWidth() {
        return width;
    }
}
[3]定义正方形,也实现四边形接口
package com.wf.design_principle.liskovsubstitution;

/**
 * @ClassName Square
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 10:34
 * @Version 1.0
 */
public class Square implements QuadRectangle {
    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;
    }
}
【4】测试类

修改原有测试逻辑。

revise方法,现在希望修改入参为QuadRangle[四边形]。方法直接报错了,如下所示:

为什么会这样呢?

  因为四边形只有get方法,没有setHeight方法。  

  这样的好处是,产生编译错误,让错误提前到编译期,可以杜绝继承泛滥的作用。

  也就是说,revise方法入参不能使用四边形,只能传长方形。【因为这里本身就是长方形的特有逻辑】

  此外,正方形是不能调用revise方法的。

最终的代码如下:

package com.wf.design_principle.liskovsubstitution;

/**
 * @ClassName LspTest
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 10:36
 * @Version 1.0
 */
public class LspTest {
    //测试长方形
   /* public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(20L);
        rectangle.setHeight(10L);
        resize(rectangle);
    }*/
    //测试正方形
    public static void main(String[] args) {
//        Square square = new Square();
//        square.setLength(10L);
//        resize(square);
    }

    public static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() >= rectangle.getHeight()){
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println(""+rectangle.getWidth() +",Height:"+rectangle.getHeight());
        }
        System.out.println("Resize End ,"+rectangle.getWidth() +",Height:"+rectangle.getHeight());
    }
}

  这里说明的是类的继承关系,与里氏替换原则的应用。

C.里氏替换原则应用--方法入参限定

预期:

  子类重载父类的方法,子类方法的入参要比父类更宽松。

  如,父类方法的参数是hashMap,子类方法的入参可以是hashMap或Map

【1】定义父类
package com.wf.design_principle.liskovsubstitution.methodparam;

import java.util.HashMap;

/**
 * @ClassName Base
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 11:07
 * @Version 1.0
 */
public class Base {
    public void method(HashMap map){
        System.out.println("父类执行");
    }
}
【2】定义子类

并重载父类中方法,如下所示:

package com.wf.design_principle.liskovsubstitution.methodparam;

import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName Child
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 11:10
 * @Version 1.0
 */
public class Child extends Base {
    @Override
    public void method(HashMap map) {
        System.out.println("子类hashMap入参方法执行");
    }
    
    //重载方法
    public void method(Map map) {
        System.out.println("子类Map入参方法执行");
    }
}
【3】测试类
package com.wf.design_principle.liskovsubstitution.methodparam;

import java.util.HashMap;

/**
 * @ClassName MethodParamTest
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 11:13
 * @Version 1.0
 */
public class MethodParamTest {
    public static void main(String[] args) {
        Child child = new Child();
        HashMap hashMap = new HashMap();
        child.method(hashMap);//子类hashMap入参方法执行
        //子类实例,执行重写方法,会执行重写方法逻辑
        
        //当子类,未重写父类方法时,会执行父类方法的逻辑
        
    }
}

测试说明:

  父类方法传参hashMap,子类重写父类方法,并重载一个传参Map的方法。

  子类实例,传参hashMap,调用方法会执行子类重视方法的逻辑。

  

  然后,注释掉子类重写方法,如下:

public class Child extends Base {
//    @Override
//    public void method(HashMap map) {
//        System.out.println("子类hashMap入参方法执行");
//    }

    //重载方法
    public void method(Map map) {
        System.out.println("子类Map入参方法执行");
    }
}

再执行测试方法,结果如下:

显然,是执行父类方法的逻辑。

现在,我们修改方法入参:

  修改目的---父类使用Map入参,子类重载时使用HashMap入参,如下所示:

public class Base {
//    public void method(HashMap map){
//        System.out.println("父类执行");
//    }
    public void method(Map map){
        System.out.println("父类执行");
    }
}
public class Child extends Base {
//    @Override
//    public void method(HashMap map) {
//        System.out.println("子类hashMap入参方法执行");
//    }

    //重载方法
    public void method(HashMap map) {
        System.out.println("子类Map入参方法执行");
    }
}

然后,执行测试:

public class MethodParamTest {
    public static void main(String[] args) {
        Child child = new Child();
        HashMap hashMap = new HashMap();
        child.method(hashMap);//子类Map入参方法执行
    }
}

测试说明:

  这里执行了,子类重载方法的逻辑。而不是父类方法的逻辑。

测试场景3:

  还是原来的方法定义,父类方法入参HashMap,子类方法重载入参Map。【注:重载方法注释掉】

  然后,修改测试类实例创建,如下所示:

public class MethodParamTest {
    public static void main(String[] args) {
        Base child = new Child();       //声明父类,new 子类
        HashMap hashMap = new HashMap();
        child.method(hashMap);//父类执行
    }
}

测试结果说明:

  无论创建实例时,声明父类new子类,还是创建子类补全,都是执行父类方法的逻辑。

  这里说明的是,改变实例声明类型,不会影响最终执行结果。【这也是里氏替换原则的体现】

D.里氏替换原则应用--方法返回值限定

预期:

  子类重载父类的方法,子类方法的返回值类型要相比父类更严格或相等。

  如,父类方法的返回是Map,子类方法的返回值可以是hashMap或Map或LinkedMap

【1】定义父类方法
public abstract class Base {
    public abstract Map method();
}

方法返回Map类型。

[2]定义子类
package com.wf.design_principle.methodreturn;

import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName Child
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 11:50
 * @Version 1.0
 */
public class Child extends Base {
    @Override
    public Map method() {
        HashMap hashMap = new HashMap();
        System.out.println("执行子类的method");
        hashMap.put("msg","子类method");
        return hashMap;
    }
}
【3】测试类
package com.wf.design_principle.methodreturn;

/**
 * @ClassName MethodReturnTest
 * @Description TODO
 * @Author wf
 * @Date 2020/4/27 13:49
 * @Version 1.0
 */
public class MethodReturnTest {
    public static void main(String[] args) {
        Base child = new Child();
        System.out.println(child.method());
    }
}

假如我们想改变,子类方法的返回值,以更为宽松的类型进行返回,结果如下:

因为,父类方法中返回值为Map类型。现在想修改为返回Object类型,明显,代码会提示编译报错。

显然,这里也体现的里氏替换原则。

 1.1.7.合成复用原则

  Composite & Aggregate Reuse Principle【C&ARP】

  也叫组合复用原则

定义

  尽量使用对象组合、聚合,而不是继承关系来达到软件复用的目的。

聚合has-a和组合contains-a

优点:  

  可以使系统更加灵活,降低类与类之间的耦合度

  一类的变化对其他类造成影响相对较小。

何时使用合成/聚合,何时使用继承?

聚合has-a,组合contains-a,继承is-a

  继承关系,关联性很强,如:狗是一种动物,使用狗---继承---动物

  组合关系,整体与部分的关系。 如:人的身体构成----头,手,脚

1.1.7.1.代码示例

这里以数据库连接,Connection与Dao之间的关系,来说明组合复用原则。

1.定义Connection类
package com.wf.design_principle.compositereuse;

/**
 * @ClassName DBConnection
 * @Description 数据库连接对象
 * @Author wf
 * @Date 2020/4/27 14:15
 * @Version 1.0
 */
public class DBConnection {
    public String getConnection(){
        return "获取Mysql数据库连接";
    }
}

注意:在实战编程中,需要满足开闭原则,面向接口编程。【这里为简单,定义为实现类】

2.dao接口定义
package com.wf.design_principle.compositereuse;

/**
 * @ClassName ProductDao
 * @Description 数据库表操作
 * @Author wf
 * @Date 2020/4/27 14:16
 * @Version 1.0
 */
public class ProductDao {
    private DBConnection dbConnection;
    public void setConnection(DBConnection dbConnection){
        this.dbConnection = dbConnection;
    }

    public void addProduct(){
        String conn = dbConnection.getConnection();
        System.out.println("获得数据库连接");
    }
}

可以发现,ProductDao中引用DBConnection,它们之间就是一种组合复用关系。

附录:

1.Maven 自定义archtype

目的:

       我发现idea 创建maven web 项目,项目结构不完整。我想自定义一个archtype.

1.1. 通过archtype-webapp创建web项目,命名为archtype-wf-web

1.2. 然后补全项目结构,添加java/resources,test/main/java,test/main/resources

1.3. 修改pom,在pom.xml中添加archtype插件。如下所示:

 

 1.4. 自定义archtype创建完成。

1.5. 然后在插件项目所在目录,执行命令mvn archetype:create-from-project

1.6. 会在项目下生成target目录,生成archetype插件。如下所示:

 

1.7.然后把archetype安装在本地【添加到maven仓库】,使用mvn install命令:

然后,使用自定义archtype创建项目。有两种方式:

》mvn命令行方式

》idea导入自定义archtype,通过它创建。

https://blog.csdn.net/qq_32331997/article/details/76177819

2.mvn命令行方式创建项目

1.进入自定义arctype目录,使用cmd

2.使用命令:mvn archetype:generate -DarchetypeCatalog=local

3.idea生成类图技巧

  对于多个关联要想一次性生成类图,先在包下选中所有类。如下所示:

然后,使用快捷键,ctrl+Alt+shift+U,这时生成的是没有关联关系的类图,如下所示:

然后,点击关联关系图标,生成类图如下所示:

原文地址:https://www.cnblogs.com/wfdespace/p/12671607.html