1、设计模式的由来
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
2、设计模式的分类
1)创建型模式
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
2)结构型模式
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
3)行为型模式
这些设计模式特别关注对象之间的通信(沟通和职责传递委派)。
3、设计模式五原则一法则
1)开闭原则(OCP , Open-Close Principle)
开闭原则的核⼼思想也可以理解为⾯向抽象编程。
在⾯向对象编程领域中,开闭原则规定软件中的对象、类、模块和函数对扩展应该是开放的,但对于修改是封闭的。
案例示范
(1)场景
此时有一个插入功能,使用原生JDBC的方式进行插入,代码如下:
public interface UserDAO {
public void insert();
}
public interface UserDAOImpl implements UserDAO{
public void insert(){
//基于JDBC实现数据插⼊
...
pstmt.executeUpdate()
}
}
(2)错误做法
紧接着团队领导说不能使用JDBC,要改为JNDI的方式对数据进行操作,这时例如一个实习生进行删除代码,重写逻辑:
public interface UserDAOImpl implements UserDAO{
public void insert(){
//pstmt.executeUpdate()
//基于JNDI实现数据插⼊
jndi.insert();
}
}
(3)正确做法
不违背开闭原则,对修改关闭,进行扩展开放,重新添加UserDAOJndiImpl进行扩展:
public interface UserDAOJndiImpl implements UserDAO{
public void insert(){
//基于JNDI实现数据插⼊
jndi.insert();
}
}
(4)不推荐的做法
使用继承,Java对继承不友好,除非父类明确abstract,否则不推荐使用extends。
public interface UserDAOJndiImpl extends UserDAOImpl{
public void insert(){
//基于JNDI实现数据插⼊
jndi.insert();
}
}
2)单一职责原则(SRP,Single Responsibility Principle)
单⼀职责原则⼜称单⼀功能原则。
如果需要开发的⼀个功能需求不是⼀次性的,且随着业务发展的不断变化⽽变化,那么当⼀个Class类负责超过两个及以上的职责时,就在需求的不断迭代、实现类持续扩张的情况下,就会出现难以维护、不好扩展、测试难度⼤和上线⻛险⾼等问题。
案例示范
(1)场景
通过付费视频⽹站⽤户分类的例⼦,当在各类视频⽹站看电影、电视剧时,⽹站针对不同的⽤户类型,会在⽤户观看时给出不同的服务反馈,如以下三种:
- 访客⽤户
⼀般只可以观看480P视频,并时刻提醒⽤户注册会员能观看⾼清视频。
表示视频业务发展需要拉客,以获取更多的新注册⽤户。 - 普通会员
可以观看720P超清视频,但不能屏蔽视频中出现的⼴告。
表示视频业务发展需要盈利。 - VIP 会员(属于付费⽤户)
既可以观看 1080P 蓝光视频,⼜可以关闭或跳过⼴告。
(2)错误做法
观看视频的清晰度,和是否可跳过广告(有广告)两个维度功能冗余在了一起:
public class VideoUserService {
public void serveGrade(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⾼清");
}
}
}
(3)正确做法
将功能进行抽象出来,单一职责,每个方法负责一个维度的功能:
//功能接口
public interface IVideoUserService {
// 视频清晰级别;480P、720P、1080P
void definition();
// ⼴告播放⽅式;⽆⼴告、有⼴告
void advertisement();
}
//访客用户
public class GuestVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("访客⽤户,视频480P⾼清");
}
public void advertisement() {
System.out.println("访客⽤户,视频有⼴告");
}
}
//普通会员
public class OrdinaryVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("普通⽤户,视频720P超清");
}
public void advertisement() {
System.out.println("普通⽤户,视频有⼴告");
}
}
//VIP用户
public class VipVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("VIP⽤户,视频1080P蓝光");
}
public void advertisement() {
System.out.println("VIP⽤户,视频⽆⼴告");
}
}
3)里式代换原则(LSP,Liskov Substitution Principle)
继承必须确保超类所拥有的性质在⼦类中仍然成⽴。(⼦类可以扩展⽗类的功能,但不能改变⽗类原有的功能)
也就是说:当⼦类继承⽗类时,除添加新的⽅法且完成新增功能外,尽量不要重写⽗类的⽅法。细化下来,还可以囊括出四点:
- ⼦类可以实现⽗类的抽象⽅法,但不能覆盖⽗类的⾮抽象⽅法。
- ⼦类可以增加⾃⼰特有的⽅法。
- 当⼦类的⽅法重载⽗类的⽅法时,⽅法的前置条件(即⽅法的⼊参)要⽐⽗类的⽅法更宽松。
- 当⼦类的⽅法实现⽗类的⽅法(重写、重载或实现抽象⽅法)时,⽅法的后置条件(即⽅法的输出或返回值)要⽐⽗类的⽅法更严格或与⽗类的⽅法相等。
案例示范
(1)场景
储蓄卡和信⽤卡在使⽤功能上类似,都有⽀付、提现、还款、充值等功 能,也有些许不同,例如⽀付,储蓄卡做的是账户扣款动作,信⽤卡做的是⽣成贷款单动作。
(2)错误做法
下⾯这⾥模拟先有储蓄卡的类,之后继承这个类的基本功能,以实现信⽤卡的功能。
//储蓄卡
public class CashCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
// 模拟⽀付成功
public String withdrawal(String orderId, BigDecimal amount) {
logger.info("提现成功,单号:{} ⾦额:{}", orderId, amount);
return "0000";
}
// 模拟充值成功
public String recharge(String orderId, BigDecimal amount) {
logger.info("储蓄成功,单号:{} ⾦额:{}", orderId, amount);
return "0000";
}
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 class CreditCard extends CashCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
@Override
public String withdrawal(String orderId, BigDecimal amount) {
// 校验
if (amount.compareTo(new BigDecimal(1000)) >= 0){
logger.info("贷款⾦额校验(限额1000元),单号:{} ⾦额:{}", orderId,amount);
return "0001";
}
// 模拟⽣成贷款单
logger.info("⽣成贷款单,单号:{} ⾦额:{}", orderId, amount);
// 模拟⽀付成功
logger.info("贷款成功,单号:{} ⾦额:{}", orderId, amount);
return "0000";
}
@Override
public String recharge(String orderId, BigDecimal amount) {
// 模拟⽣成还款单
logger.info("⽣成还款单,单号:{} ⾦额:{}", orderId, amount);
// 模拟还款成功
logger.info("还款成功,单号:{} ⾦额:{}", orderId, amount);
return "0000";
}
@Override
public List<String> tradeFlow() {
return super.tradeFlow();
}
}
信⽤卡的功能实现是在继承了储蓄卡类后,对 ⽀付 withdrawal()、还款recharge()等方法⽅法重写。其实交易流⽔tradeFlow()可以复⽤,也可以不⽤重写这个方法。
这种继承⽗类⽅式的优点是复⽤了⽗类的核⼼功能逻辑,但是也破坏了原有的⽅法。
此时继承⽗类实现的信⽤卡类并不满⾜⾥⽒替换原则(信用卡的继承,不能够保证超类所拥有的性子在子类里仍然成立),也就是说,此时的⼦类不能承担原⽗类的功能,不能直接当储蓄卡使⽤。
(3)正确做法
将银⾏卡作为⽗类进行抽象,因为信用卡和储蓄卡都是银行卡。
在抽象银⾏卡类中,提供了基本的卡属性,包括卡号、开卡时间及三个核⼼⽅法。正向⼊账,加钱;逆向⼊账,减钱;流水查询。
当然,实际的业务开发 抽象出来的逻辑会⽐模拟场景多⼀些。
接下来实现银行卡这个抽象类:
//抽象银行卡
@Data
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, amo
unt);
return "0000";
}
// 逆向⼊账,- 钱
public String negative(String orderId, BigDecimal amount) {
// ⼊款成功,存款、还款
logger.info("卡号{} 出款成功,单号:{} ⾦额:{}", cardNo, orderId, amo
unt);
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;
}
}
储蓄卡类中继承抽象银⾏卡⽗类 BankCard,实现的核⼼功能包括规则过滤rule、提现withdrawal、储蓄recharge和新增的扩展⽅法,即⻛控 校验 checkRisk。
实现储蓄卡的功能逻辑:
public class CashCard extends BankCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
public CashCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
boolean rule(BigDecimal amount) {
return true;
}
// 模拟⽀付成功
public String withdrawal(String orderId, BigDecimal amount) {
logger.info("提现成功,单号:{} ⾦额:{}", orderId, amount);
return super.negative(orderId, amount);
}
// 模拟充值成功
public String recharge(String orderId, BigDecimal amount) {
logger.info("储蓄成功,单号:{} ⾦额:{}", orderId, amount);
return super.positive(orderId, amount);
}
// 模拟⻛控校验
public boolean checkRisk(String cardNo, String orderId, BigDecimal amount) {
logger.info("⻛控校验,卡号:{} 单号:{} ⾦额:{}", cardNo, orderId, amount);
return true;
}
}
信⽤卡⼦类信⽤卡类在继承⽗类后,使⽤了公⽤的属性,即卡号 cardNo、开卡时间 cardDate,同时新增了符合信⽤卡功能的新⽅法,即贷款loan、还款repayment,并在两个⽅法中都使⽤了抽象类的核⼼功能。
另外,关于储蓄卡中的规则校验⽅法,新增了⾃⼰的规则⽅法 rule2, 并没有破坏储蓄卡中的校验⽅法。
以上的实现⽅式都是在遵循⾥⽒替换原则下完成的,⼦类随时可以替代 储蓄卡类。
//信用卡
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;
}
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);
}
public String repayment(String orderId, BigDecimal amount) {
// 模拟⽣成还款单
logger.info("⽣成还款单,单号:{} ⾦额:{}", orderId, amount);
// 模拟还款成功
logger.info("还款成功,单号:{} ⾦额:{}", orderId, amount);
return super.positive(orderId, amount);
}
}
(4)总结
通过以上的测试结果可以看到,储蓄卡功能正常,继承储蓄卡实现的信 ⽤卡功能也正常。
同时,原有储蓄卡类的功能可以由信⽤卡类⽀持,即 CashCard creditCard=new CreditCard(…)。 继承作为⾯向对象的重要特征,虽然给程序开发带来了⾮常⼤的便利,但也引⼊了⼀些弊端。
继承的开发⽅式会给代码带来侵⼊性,可移植能 ⼒降低,类之间的耦合度较⾼。当对⽗类修改时,就要考虑⼀整套⼦类的实现是否有⻛险,测试成本较⾼。
⾥⽒替换原则的⽬的是使⽤约定的⽅式,让使⽤继承后的代码具备良好 的扩展性和兼容性。
在⽇常开发中使⽤继承的地⽅并不多,在有些公司的代码规范中也不会允许多层继承,尤其是⼀些核⼼服务的扩展。⽽继承多数⽤在系统架构初期定义好的逻辑上或抽象出的核⼼功能⾥。
如果使⽤了继承,就⼀定要遵从⾥⽒替换原则,否则会让代码出现问题的概率变得更⼤。
4)倒转依赖原则 (DIP,Dependence Inversion Principle)
在设计代码架构师,高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
DIP就是常说的“面相接口编程”,它是实现开闭原则重要途径之一,降低耦合,提高系统的可维护性,这样的代码通常更加易读、传承。
案例分析
在互联网的营销活动中,经常为了拉新和促活会做一些抽奖活动。这些抽奖活动的规则会随着业务的不断发展而调整,如随机抽奖、权重抽奖等等。
(1)错误的做法
抽象用户类:
public class BetUser {
private String userName; // ⽤户姓名
private int userWeight; // ⽤户权重
}
抽奖逻辑类:
public class DrawControl {
// 随机抽取指定数量的⽤户,作为中奖⽤户
public List<BetUser> doDrawRandom(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;
}
// 权重排名获取指定数量的⽤户,作为中奖⽤户
public List<BetUser> doDrawWeight(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;
}
}
(3)正确的做法
将抽奖逻辑分离,使用时只关注自己的抽奖逻辑,新增抽奖逻辑只需实现接口,不需要再修改抽奖逻辑类。
抽奖逻辑接口:
public interface IDraw {
// 获取中奖⽤户接⼝
List<BetUser> prize(List<BetUser> list, int count);
}
随机抽奖逻辑类:
public class DrawRandom implements IDraw {
@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;
}
}
权重抽奖逻辑类:
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;
}
}
抽奖:
public class DrawControl {
public List<BetUser> doDraw(IDraw draw, List<BetUser> betUserList, int count) {
return draw.prize(betUserList, count);
}
public static void main(String[] args){
List<BetUser> userList = new ArrayList();
//初始化userList
//这⾥的重点是把实现逻辑的接⼝作为参数传递
new DrawControl().doDraw(new DrawWeightRank() , userList , 3);
}
}
5)接口隔离原则 (ISP,Interface Segregation Principle)
一个类对另一个类的依赖应该建立在最小的接口上
接口隔离原则,要求尽量将臃肿庞大的接口插分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
示例
Servlet事件监听器可以监听ServletContext、HttpSession、ServletRequest等域对象的创建和销毁过程,以及监听这些域对象属性的修改。
ServletContextListener接口:
public void contextInitialized(servletContextEvent sce);
public void contextDestroyed(servletContextEvent sce);
HttpSessionListener接口:
public void sessionCreated(HttpSessionEvent se);
public void sessionDestroyed(HttpSessionEvent se);
ServletRequestListener接口:
public void requestInitialized(ServletRequestEvent sre);
public void requestDestroyed(ServletRequestEvent sre);
可以看到,它是把不同对象的相同功能给打散了为不同接口,使用类在应用时只需要关注自己需要的借口进行实现即可,不需要将一个接口进行实现就完成了所有域对象的创建和销毁方法的重写。
监听器的应用代码:
public class MyListener implements ServletRequestListener, HttpSessionList
ener, ServletContextListener {
public void contextInitialized(ServletContextEvent arg0) {
System.out.println("ServletContext对象被创建了");
}
public void contextDestroyed(ServletContextEvent arg0) {
System.out.println("ServletContext对象被销毁了");
}
public void sessionCreated(HttpSessionEvent arg0) {
System.out.println("HttpSession对象被创建了");
}
public void sessionDestroyed(HttpSessionEvent arg0) {
System.out.println("HttpSession对象被销毁了");
}
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("ServletRequest对象被创建了");
}
public void requestDestroyed(ServletRequestEvent arg0) {
System.out.println("ServletRequest对象被销毁了");
}
}
6)迪米特法则 (DP,Demeter Principle)
又称最少知道原则,是指一个对象类对于其他对象来说,知道的越少越好。也就是说,两个类之间不要有过多的耦合关系,保持最少的关联性。
案例示范
(1)场景
老师需要负责某一个学生的学习情况,而校长会关心老师所在班级的整体成绩,不会过问具体某一个学生的学习情况。
(2)错误做法
public class Student {
private String name; // 学⽣姓名
private int rank; // 考试排名(总排名)
private double grade; // 考试分数(总分)
...
}
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 class Principal {
private Teacher teacher = new Teacher("丽华", "3年1班");
// 查询班级信息,总分数、学⽣⼈数、平均值
public Map<String, Object> queryClazzInfo(String clazzId) {
// 获取班级信息;学⽣总⼈数、总分、平均分
int stuCount = clazzStudentCount();
double totalScore = clazzTotalScore();
double averageScore = clazzAverageScore();
// 组装对象,实际业务开发会有对应的类
Map<String, Object> mapObj = new HashMap<>();
mapObj.put("班级", teacher.getClazz());
mapObj.put("⽼师", teacher.getName());
mapObj.put("学⽣⼈数", stuCount);
mapObj.put("班级总分数", totalScore);
mapObj.put("班级平均分", averageScore);
return mapObj;
}
// 总分
public double clazzTotalScore() {
double totalScore = 0;
for (Student stu : teacher.getStudentList()) {
totalScore += stu.getGrade();
}
return totalScore;
}
// 平均分
public double clazzAverageScore(){
double totalScore = 0;
for (Student stu : teacher.getStudentList()) {
totalScore += stu.getGrade();
}
return totalScore / teacher.getStudentList().size();
}
// 班级⼈数
public int clazzStudentCount(){
return teacher.getStudentList().size();
}
}
错误分析:
通过场景来看,校长只关心班级的整体情况,不需要知道班级里的每个学生的学习成绩,所以老师应该吧校长关心的数据计算好,让校长可知道即可,不能讲学生成绩输出给校长。虽然 可以查询到结果,但是违背了迪⽶特法则,因为校⻓需要了解每个学⽣ 的情况。如果所有班级都让校⻓类统计,代码就会变得⾮常臃肿,也不 易于维护和扩展。
(3)正确做法
由⽼师负责分数统计
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 String getName() {
return name;
}
public String getClazz() {
return clazz;
}
}
public class Principal {
private Teacher teacher = new Teacher("丽华", "3年1班");
// 查询班级信息,总分数、学⽣⼈数、平均值
public Map<String, Object> queryClazzInfo(String clazzId) {
// 获取班级信息;学⽣总⼈数、总分、平均分
int stuCount = teacher.clazzStudentCount();
double totalScore = teacher.clazzTotalScore();
double averageScore = teacher.clazzAverageScore();
// 组装对象,实际业务开发会有对应的类
Map<String, Object> mapObj = new HashMap<>();
mapObj.put("班级", teacher.getClazz());
mapObj.put("⽼师", teacher.getName());
mapObj.put("学⽣⼈数", stuCount);
mapObj.put("班级总分数", totalScore);
mapObj.put("班级平均分", averageScore);
return mapObj;
}
}