文章目录
参考教程:https://www.bilibili.com/video/BV1D341177SV
参考书籍:《秒懂设计模式》刘韬
参考网站:https://refactoringguru.cn/design-patterns/singleton
一 设计模式介绍
- 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
- 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
- 项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
目的
编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好
- 代码重用性(即:相同功能的代码,不用多次编写)
- 可读性(即:编程规范性,便于其他程序员的阅读和理解)
- 可扩展性(即:当需要增加新的功能时,非常的方便,称为可维护
- 可靠性(即:当我们增加新的功能后,对原来的功能没有影响)
- 使程序呈现高内聚,低耦合的特性
二 设计模式中的原则
- 单一职责原则
- 接口隔离原则
- 依赖倒转(倒置)原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 复合原则
2.1 单一职责原则(职责单一)
对类来说的,即一个类应该只负责一项职责(不是说一个类里只能有一个方法,指的是一项职责,比如这个类管订单了,就不要去管物流。),如类A负责两个不同职责: 职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为 A1,A2
package main.java;
/**
* @description: 不同是用户类型视频类型
* @author: shu
* @createDate: 2022/9/4 10:17
* @version: 1.0
*/
public class VideoUserService {
/**
* 不同的访客用户,提供服务
* @param userType
*/
public void serverGrade(String userType){
if ("VIP用户".equals(userType)){
System.out.println("VIP用户,视频1080P蓝光");
} else if ("普通用户".equals(userType)){
System.out.println("普通用户,视频720P超清");
} else if ("访客用户".equals(userType)){
System.out.println("访客用户,视频480P高清");
}
}
}
package main.java;
/**
* @description: 不同的用户视频类型接口
* @author: shu
* @createDate: 2022/9/4 10:49
* @version: 1.0
*/
public interface VideoUser {
/**
* 提供的类型
*/
void definition();
/**
* 广告
*/
void advertisement();
}
package main.java.impl;
import main.java.VideoUser;
/**
* @description: 访客用户
* @author: shu
* @createDate: 2022/9/4 10:51
* @version: 1.0
*/
public class GuestVideoUserService implements VideoUser {
/**
* 提供的类型
*/
@Override
public void definition() {
System.out.println("访客用户,视频480P高清");
}
/**
* 广告
*/
@Override
public void advertisement() {
System.out.println("访客用户,视频有广告");
}
}
package main.java.impl;
import main.java.VideoUser;
/**
* @description: 普通用户
* @author: shu
* @createDate: 2022/9/4 10:52
* @version: 1.0
*/
public class OrdinaryVideoUserService implements VideoUser {
/**
* 提供的类型
*/
@Override
public void definition() {
System.out.println("普通用户,视频720P超清");
}
/**
* 广告
*/
@Override
public void advertisement() {
System.out.println("普通用户,视频有广告");
}
}
package main.java.impl;
import main.java.VideoUser;
/**
* @description:
* @author: shu
* @createDate: 2022/9/4 10:53
* @version: 1.0
*/
public class VipVideoUserService implements VideoUser {
/**
* 提供的类型
*/
@Override
public void definition() {
System.out.println("VIP用户,视频1080P蓝光");
}
/**
* 广告
*/
@Override
public void advertisement() {
System.out.println("VIP会员,视频无广告");
}
}
原则与细节
- 降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性,可维护性。
- 降低变更引起的风险。
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一-职责原则。
2.2 开闭原则(对外扩展开放,修改关闭)
- 开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的“。
- 开闭原则(Open Closed Principle) 是编程中最基础、最重要的设计原则。
- 一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
package impl;
/**
* @description: 计算区域面积接口
* @author: shu
* @createDate: 2022/9/4 11:22
* @version: 1.0
*/
public interface ICalculationArea {
/**
* 计算面积,长方形
*
* @param x 长
* @param y 宽
* @return 面积
*/
double rectangle(double x, double y);
/**
* 计算面积,三角形
* @param x 边长x
* @param y 边长y
* @param z 边长z
* @return 面积
*
* 海伦公式:S=√[p(p-a)(p-b)(p-c)] 其中:p=(a+b+c)/2
*/
double triangle(double x, double y, double z);
/**
* 计算面积,圆形
* @param r 半径
* @return 面积
*
* 圆面积公式:S=πr²
*/
double circular(double r);
}
package impl;
/**
* @description: 原来实现的方法,不允许修改
* @author: shu
* @createDate: 2022/9/4 11:25
* @version: 1.0
*/
public class CalculationArea implements ICalculationArea{
/**
* 计算面积,长方形
*
* @param x 长
* @param y 宽
* @return 面积
*/
@Override
public double rectangle(double x, double y) {
return 0;
}
/**
* 计算面积,三角形
*
* @param x 边长x
* @param y 边长y
* @param z 边长z
* @return 面积
* <p>
* 海伦公式:S=√[p(p-a)(p-b)(p-c)] 其中:p=(a+b+c)/2
*/
@Override
public double triangle(double x, double y, double z) {
return 0;
}
/**
* 计算面积,圆形
*
* @param r 半径
* @return 面积
* <p>
* 圆面积公式:S=πr²
*/
@Override
public double circular(double r) {
return 0;
}
}
package impl;
/**
* @description: 在原来的类进行扩展
* @author: shu
* @createDate: 2022/9/4 11:26
* @version: 1.0
*/
public class CalculationAreaExt extends CalculationArea {
private final static double π = 3.141592653D;
@Override
public double circular(double r) {
return π * r * r;
}
}
总结
- 优点是比较好理解,简单易操作。
- 缺点是违反了设计模式的ocp原则,即对扩展开放(提供方),对修改关闭(使用方)。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码。
2.3 里氏替换原则(保留父类特性)
- 子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
- 子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。
- 这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义了父类和子类之间的关系,也可以替换成接口和实现类之间的关系。
package com.shu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 银行卡抽象类
* @author: shu
* @createDate: 2022/9/4 11:57
* @version: 1.0
*/
public abstract class BankCard {
private Logger logger = LoggerFactory.getLogger(BankCard.class);
private String cardNo; // 卡号
private String cardDate; // 开卡时间
public BankCard(String cardNo, String cardDate) {
this.cardNo = cardNo;
this.cardDate = cardDate;
}
abstract boolean rule(BigDecimal amount);
// 正向入账,+ 钱
public String positive(String orderId, BigDecimal amount) {
// 入款成功,存款、还款
logger.info("卡号{} 入款成功,单号:{} 金额:{}", cardNo, orderId, amount);
return "0000";
}
// 逆向入账,- 钱
public String negative(String orderId, BigDecimal amount) {
// 入款成功,存款、还款
logger.info("卡号{} 出款成功,单号:{} 金额:{}", cardNo, orderId, amount);
return "0000";
}
/**
* 交易流水查询
*
* @return 交易流水
*/
public List<String> tradeFlow() {
logger.info("交易流水查询成功");
List<String> tradeList = new ArrayList<String>();
tradeList.add("100001,100.00");
tradeList.add("100001,80.00");
tradeList.add("100001,76.50");
tradeList.add("100001,126.00");
return tradeList;
}
public String getCardNo() {
return cardNo;
}
public String getCardDate() {
return cardDate;
}
}
package com.shu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
/**
* @description: 储蓄卡
* @author: shu
* @createDate: 2022/9/4 12:00
* @version: 1.0
*/
public class CashCard extends BankCard {
private final Logger logger = LoggerFactory.getLogger(CashCard.class);
public CashCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
boolean rule(BigDecimal amount) {
return true;
}
/**
* 提现
*
* @param orderId 单号
* @param amount 金额
* @return 状态码 0000成功、0001失败、0002重复
*/
public String withdrawal(String orderId, BigDecimal amount) {
// 模拟支付成功
logger.info("提现成功,单号:{} 金额:{}", orderId, amount);
return super.negative(orderId, amount);
}
/**
* 储蓄
*
* @param orderId 单号
* @param amount 金额
*/
public String recharge(String orderId, BigDecimal amount) {
// 模拟充值成功
logger.info("储蓄成功,单号:{} 金额:{}", orderId, amount);
return super.positive(orderId, amount);
}
/**
* 风险校验
*
* @param cardNo 卡号
* @param orderId 单号
* @param amount 金额
* @return 状态
*/
public boolean checkRisk(String cardNo, String orderId, BigDecimal amount) {
// 模拟风控校验
logger.info("风控校验,卡号:{} 单号:{} 金额:{}", cardNo, orderId, amount);
return true;
}
}
package com.shu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
/**
* @description: 信用卡
* @author: shu
* @createDate: 2022/9/4 12:01
* @version: 1.0
*/
public class CreditCard extends CashCard {
private Logger logger = LoggerFactory.getLogger(CreditCard.class);
public CreditCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
boolean rule2(BigDecimal amount) {
return amount.compareTo(new BigDecimal(1000)) <= 0;
}
/**
* 提现,信用卡贷款
*
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String loan(String orderId, BigDecimal amount) {
boolean rule = rule2(amount);
if (!rule) {
logger.info("生成贷款单失败,金额超限。单号:{} 金额:{}", orderId, amount);
return "0001";
}
// 模拟生成贷款单
logger.info("生成贷款单,单号:{} 金额:{}", orderId, amount);
// 模拟支付成功
logger.info("贷款成功,单号:{} 金额:{}", orderId, amount);
return super.negative(orderId, amount);
}
/**
* 还款,信用卡还款
*
* @param orderId 单号
* @param amount 金额
* @return 状态码
*/
public String repayment(String orderId, BigDecimal amount) {
// 模拟生成还款单
logger.info("生成还款单,单号:{} 金额:{}", orderId, amount);
// 模拟还款成功
logger.info("还款成功,单号:{} 金额:{}", orderId, amount);
return super.positive(orderId, amount);
}
}
总结:
在不改变父类的功能上,对类进行扩展,比如银行卡分为储蓄卡,信用卡,当然他们具有共同的特性,也具有各自的特性。
2.4 迪米特性原则 (降低耦合)
- 迪米特法则的核心是降低类之间的耦合。
- 迪米特法则:意义在于降低类之间的耦合。由于每个对象尽量减少对其他对象的了解,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
- 但是注意: 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有 依赖关系。
- 耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
package com.shu;
/**
* @description: 学习信息
* @author: shu
* @createDate: 2022/9/4 13:30
* @version: 1.0
*/
public class Student {
private String name; // 学生姓名
private int rank; // 考试排名(总排名)
private double grade; // 考试分数(总分)
public Student() {
}
public Student(String name, int rank, double grade) {
this.name = name;
this.rank = rank;
this.grade = grade;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getRank() {
return rank;
}
public void setRank(int rank) {
this.rank = rank;
}
public double getGrade() {
return grade;
}
public void setGrade(double grade) {
this.grade = grade;
}
}
package com.shu;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 每个老师来管理几级负责班级的学生
* @author: shu
* @createDate: 2022/9/4 13:32
* @version: 1.0
*/
public class Teacher {
private String name; // 老师名称
private String clazz; // 班级
private static List<Student> studentList; // 学生
public Teacher() {
}
public Teacher(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
static {
studentList = new ArrayList<>();
studentList.add(new Student("花花", 10, 589));
studentList.add(new Student("豆豆", 54, 356));
studentList.add(new Student("秋雅", 23, 439));
studentList.add(new Student("皮皮", 2, 665));
studentList.add(new Student("蛋蛋", 19, 502));
}
// 总分
public double clazzTotalScore() {
double totalScore = 0;
for (Student stu : studentList) {
totalScore += stu.getGrade();
}
return totalScore;
}
// 平均分
public double clazzAverageScore(){
double totalScore = 0;
for (Student stu : studentList) {
totalScore += stu.getGrade();
}
return totalScore / studentList.size();
}
// 班级人数
public int clazzStudentCount(){
return studentList.size();
}
public static List<Student> getStudentList() {
return studentList;
}
public String getName() {
return name;
}
public String getClazz() {
return clazz;
}
}
package com.shu;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 每个老师来管理几级负责班级的学生
* @author: shu
* @createDate: 2022/9/4 13:32
* @version: 1.0
*/
public class Teacher {
private String name; // 老师名称
private String clazz; // 班级
private static List<Student> studentList; // 学生
public Teacher() {
}
public Teacher(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
static {
studentList = new ArrayList<>();
studentList.add(new Student("花花", 10, 589));
studentList.add(new Student("豆豆", 54, 356));
studentList.add(new Student("秋雅", 23, 439));
studentList.add(new Student("皮皮", 2, 665));
studentList.add(new Student("蛋蛋", 19, 502));
}
// 总分
public double clazzTotalScore() {
double totalScore = 0;
for (Student stu : studentList) {
totalScore += stu.getGrade();
}
return totalScore;
}
// 平均分
public double clazzAverageScore(){
double totalScore = 0;
for (Student stu : studentList) {
totalScore += stu.getGrade();
}
return totalScore / studentList.size();
}
// 班级人数
public int clazzStudentCount(){
return studentList.size();
}
public static List<Student> getStudentList() {
return studentList;
}
public String getName() {
return name;
}
public String getClazz() {
return clazz;
}
}
2.5 接口隔离原则 (最小接口)
- 接口隔离原则:要求程序员尽量将臃肿庞大的接口拆分为更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
- 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
interface Interface1 {
void operation1();
}
//接口2
interface Interface2 {
void operation2();
void operation3();
}
//接口3
interface Interface3 {
void operation4();
void operation5();
}
class A {
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface2 i) {
i. operation2();
}
public void depend3(Interface2 i) {
i. operation3();
}
}
class C {
public void depend1(Interface1 i) {
i . operation1();
}
public void depend4(Interface3 i) {
i. operation4();
}
public void depend5(Interface3| i) {
i. operation5();
}
}
2.6 依赖倒置原则 (权利倒置)
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
- 依赖倒转(倒置)的中心思想是面向接口编程。
- 依赖倒转原则是基于这样的设计理念: 相对于细节的多变性,抽象的东西要稳定的多。在java中, 抽象指的是接口或抽象类,细节就是具体的实现类。
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
package com.shu;
import java.util.List;
/**
* @description: 抽奖
* @author: shu
* @createDate: 2022/9/4 14:30
* @version: 1.0
*/
public interface IDraw {
/**
* 抽奖
* @param list
* @param count
* @return
*/
List<BetUser> prize(List<BetUser> list, int count);
}
package com.shu;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 权重抽奖
* @author: shu
* @createDate: 2022/9/4 14:32
* @version: 1.0
*/
public class DrawWeightRank implements IDraw {
@Override
public List<BetUser> prize(List<BetUser> list, int count) {
// 按照权重排序
list.sort((o1, o2) -> {
int e = o2.getUserWeight() - o1.getUserWeight();
if (0 == e) return 0;
return e > 0 ? 1 : -1;
});
// 取出指定数量的中奖用户
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
package com.shu;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @description: 随机抽奖
* @author: shu
* @createDate: 2022/9/4 14:31
* @version: 1.0
*/
public class DrawRandom implements IDraw{
/**
* 抽奖
*
* @param list
* @param count
* @return
*/
@Override
public List<BetUser> prize(List<BetUser> list, int count) {
// 集合数量很小直接返回
if (list.size() <= count) return list;
// 乱序集合
Collections.shuffle(list);
// 取出指定数量的中奖用户
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
package com.shu;
import java.util.List;
/**
* @description: 抽奖控制
* @author: shu
* @createDate: 2022/9/4 14:33
* @version: 1.0
*/
public class DrawControl {
private IDraw draw;
public List<BetUser> doDraw(IDraw draw, List<BetUser> betUserList, int count) {
return draw.prize(betUserList, count);
}
}
2.7 合成复用原则 (合成聚合)
- 原则是尽量使用合成/聚合的方式,而不是使用继承。
- B类需要使用A类的方法,可以使用继承。但是使用继承会让B与A的耦合性增强。
- 改进,将A作为参数传入B类的方法中(即,依赖关系),B类依然可以使用到A类的方法。
- 或者使B类中包含A类(即,聚合关系)。