设计模式--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("width:"+rectangle.getWidth() +",Height:"+rectangle.getHeight()); } System.out.println("Resize End ,width:"+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("width:"+rectangle.getWidth() +",Height:"+rectangle.getHeight()); } System.out.println("Resize End ,width:"+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,这时生成的是没有关联关系的类图,如下所示:
然后,点击关联关系图标,生成类图如下所示: