# 《Java极简设计模式》设计模式六大原则

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:介绍设计模式六大原则

# 什么是设计模式

# 概述

设计模式是解决功能逻辑开发中遇到的共享问题的方案,并不是具体的实现。相同的设计模式有着不同的实现。具体的实现需要根据系统环境做出适当的选择。

# 设计模式的由来

设计模式是系统服务设计中针对常⻅场景的⼀种解决⽅案,可以解决功能逻辑开发中遇到的共性问题。因为设计模式是⼀种开发设计指导思想,每⼀种设计模式都是解决某⼀类问题的概念模型,所以在实际的使⽤过程中,不要拘泥于某种已经存在的固定代码格式,⽽要根据实际的业务场景做出改变。正因为设计模式的这种特点,所以即使是同⼀种设计模式,在不同的场景中也有不同的代码实现⽅式。另外,即便是相同的场景,选择相同的设计模式,不同的研发⼈员也可能给出不⼀样的实现⽅案。所以,设计模式并不局限于最终的实现⽅案,⽽是在这种概念模型下,解决系统设计中的代码逻辑问题。设计模式的概念最早是由克⾥斯托弗·亚历⼭⼤在其著作《建筑模式语⾔》中提出的,2022年病逝,享年85岁

埃⾥希·伽玛、约翰·弗利赛德斯、拉尔夫·约翰逊和理查德·赫尔姆四位作者接受了模式的概念。他们于1994年出版了《设计模式:可复⽤⾯向对象软件的基础》⼀书,将设计模式的概念应⽤到程序开发领域中。

# 设计模式有哪些种类

设计模式共有23种,按⽬的分类可以分为三类:

# 创建型模式

提供创建对象的机制,提升已有代码的灵活性和可复⽤性。

(⼯⼚⽅法模式、抽象⼯⼚模式、单例模式、建造者模式、原型模式)

# 结构型模式

介绍如何将对象和类组装成较⼤的结构,并同时保持结构的灵活和⾼效。

(适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式)

# 行为模式

负责对象间的⾼效沟通和职责传递委派。

(策略模式、模板⽅法模式、观察者模式、迭代⼦模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式)

# 如何学习设计模式

以前学习设计模式,通常会先学设计模式的结构、掌握类图、编写代码,在我看来这是错误的。设计模式是前辈们⽇常总结的经验,⾸先我们要弄清楚到底⼯作中按原有⽅式编码会出现什么问题,再去通过某⼀种或某⼏种设计模式灵活运⽤去解决它,将设计模式能⽤在“⼑刃”上,这才是我们学习设计模式的⽬的所在~⾄于你使⽤了使⽤了单例模式的“懒汉”还是“饿汉”形式书写,这些都是战术问题,⼩Case。

# 六大设计原则

要学习设计模式,必须先了解软件设计的六⼤原则,了解软件设计的底层规律,才不⾄于设计跑偏。

# 开闭原则

⼀般认为最早提出开闭原则(Open-Close Principle,OCP)的是伯特兰·迈耶。他在1988 年发表的《⾯向对象软件构造》中给出的。

在⾯向对象编程领域中,开闭原则规定软件中的对象、类、模块和函数对扩展应该是开放的,但对于修改是封闭的。

开闭原则的核⼼思想也可以理解为⾯向抽象编程

错误的示范

public interface UserDAO {
 public void insert();
}

public class UserDAOImpl implements UserDAO{
 public void insert(){
 //基于JDBC实现数据插⼊
 ...
 pstmt.executeUpdate()
 }
}
1
2
3
4
5
6
7
8
9
10
11

新需求来了,放弃JDBC改⽤JNDI,于是你删除原有代码,在⽅法中重写逻辑

public class UserDAOImpl implements UserDAO{
 public void insert(){
 //pstmt.executeUpdate()
 //基于JNDI实现数据插⼊
 jndi.insert();
 }
}
1
2
3
4
5
6
7

如果我们在原有的方法上面进行修改,要以后两个方式都需要怎么办?

正确的做法

对修改关闭,对扩展开放。新建一个Jndi的实现类,实现insert方法

public interface UserDAOJndiImpl implements UserDAO{
 public void insert(){
 //基于JNDI实现数据插⼊
 jndi.insert();
 }
}
1
2
3
4
5
6

虽然可以通过继承UserDAOImpl类并覆盖其insert方法来实现新功能,但这并不是最佳实践。首先,Java并不鼓励过度使用继承,除非父类被定义为abstract或明确设计为被继承。此外,使用继承可能会导致类层次结构过于复杂,增加了理解和维护的难度。更重要的是,如果父类的方法实现发生了变化,可能会影响到所有子类的行为,这与开闭原则相悖

public class UserDAOJndiImpl extends UserDAOImpl{
 public void insert(){
 //基于JNDI实现数据插⼊
 jndi.insert();
 }
}
1
2
3
4
5
6

# 单⼀职责原则

单⼀职责原则(Single Responsibility Principle,SRP)⼜称单⼀功能原则

如果需要开发的⼀个功能需求不是⼀次性的,且随着业务发展的不断变化⽽变化,那么当⼀个Class类负责超过两个及以上的职责时,就在需求的不断迭代、实现类持续扩张的情况下,就会出现难以维护、不好扩展、测试难度⼤和上线⻛险⾼等问题

错误的做法

这⾥通过⼀个视频⽹站⽤户分类的例⼦,来帮助⼤家理解单⼀职责原则的构建⽅法。当在各类视频⽹站看电影、电视剧时,⽹站针对不同的⽤户类型,会在⽤户观看时给出不同的服务反馈,如以下三种。

  • 访客⽤户,⼀般只可以观看480P视频,并时刻提醒⽤户注册会员能观看⾼清视频。这表示视频业务发展需要拉客,以获取更多的新注册⽤户。
  • 普通会员,可以观看720P超清视频,但不能屏蔽视频中出现的⼴告。这表示视频业务发展需要盈利。
  • VIP 会员(属于付费⽤户),既可以观看 1080P 蓝光视频,⼜可以关闭或跳过⼴告。
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⾼清");
 }
 }
1
2
3
4
5
6
7
8
9
10

正确的做法

public interface IVideoUserService {
 // 视频清晰级别;480P、720P、1080P
 void definition();
 // ⼴告播放⽅式;⽆⼴告、有⼴告
 void advertisement();
}
1
2
3
4
5
6

访客逻辑

public class GuestVideoUserService implements IVideoUserService {
 public void definition() {
 System.out.println("访客⽤户,视频480P⾼清");
 }
 public void advertisement() {
 System.out.println("访客⽤户,视频有⼴告");
 }
}
1
2
3
4
5
6
7
8

普通会员逻辑

public class OrdinaryVideoUserService implements IVideoUserService {
 public void definition() {
 System.out.println("普通⽤户,视频720P超清");
 }
 public void advertisement() {
 System.out.println("普通⽤户,视频有⼴告");
 }
}
1
2
3
4
5
6
7
8

VIP会员逻辑

public class VipVideoUserService implements IVideoUserService {
 public void definition() {
 System.out.println("VIP⽤户,视频1080P蓝光");
 }
 public void advertisement() {
 System.out.println("VIP⽤户,视频⽆⼴告");
 }
}
1
2
3
4
5
6
7
8

# 里氏替换原则

继承必须确保超类所拥有的性质在⼦类中仍然成⽴

简单来说,⼦类可以扩展⽗类的功能,但不能改变⽗类原有的功能。也就是说:当⼦类继承⽗类时,除添加新的⽅法且完成新增功能外,尽量不要重写⽗类的⽅法。这句话包括了四点含义:

  1. ⼦类可以实现⽗类的抽象⽅法,但不能覆盖⽗类的⾮抽象⽅法。
  2. ⼦类可以增加⾃⼰特有的⽅法。
  3. 当⼦类的⽅法重载⽗类的⽅法时,⽅法的前置条件(即⽅法的输⼊参数)要⽐⽗类的⽅法更宽松。
  4. 当⼦类的⽅法实现⽗类的⽅法(重写、重载或实现抽象⽅法)时,⽅法的后置条件(即⽅法的输出或返回值)要⽐⽗类的⽅法更严格或与⽗类的⽅法相等。

⾥⽒替换原则的作⽤

  • ⾥⽒替换原则是实现开闭原则的重要⽅式之⼀。
  • 解决了继承中重写⽗类造成的可复⽤性变差的问题。
  • 是动作正确性的保证,即类的扩展不会给已有的系统引⼊新的错误,降低了代码出错的可能性。
  • 加强程序的健壮性,同时变更时可以做到⾮常好的兼容性,提⾼程序的维护性、可扩展性,降低需求变更时引⼊的⻛险。

错误的做法

储蓄卡和信⽤卡在使⽤功能上类似,都有⽀付、提现、还款、充值等功能,也有些许不同,例如⽀付,储蓄卡做的是账户扣款动作,信⽤卡做的是⽣成贷款单动作。下⾯这⾥模拟先有储蓄卡的类,之后继承这个类的基本功能,以实现信⽤卡的功能

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;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

信⽤卡

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();
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

⽤卡的功能实现是在继承了储蓄卡类后,进⾏⽅法重写:⽀付withdrawal()、还款recharge()。其实交易流⽔可以复⽤,也可以不⽤重写这个类

这种继承⽗类⽅式的优点是复⽤了⽗类的核⼼功能逻辑,但是也破坏了原有的⽅法。此时继承⽗类实现的信⽤卡类并不满⾜⾥⽒替换原则,也就是说,此时的⼦类不能承担原⽗类的功能,直接当储蓄卡使⽤

正确的做法

抽象银⾏卡⽗类

在抽象银⾏卡类中,提供了基本的卡属性,包括卡号、开卡时间及三个核⼼⽅法。正向⼊账,加钱;逆向⼊账,减钱。当然,实际的业务开发抽象出来的逻辑会⽐模拟场景多⼀些。接下来继承这个抽象类,实现储蓄卡的功能逻辑

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;
 }
    
public String getCardNo() {
 return cardNo;
 }
    
 public String getCardDate() {
 return cardDate;
 }
    
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

储蓄卡⼦类

储蓄卡类中继承抽象银⾏卡⽗类 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 amo
unt) {
 // 模拟⻛控校验
 logger.info("⻛控校验,卡号:{} 单号:{} ⾦额:{}", cardNo, orderId, am
ount);
 return true;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

信⽤卡⼦类

信⽤卡类在继承⽗类后,使⽤了公⽤的属性,即卡号 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);
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

通过以上的测试结果可以看到,储蓄卡功能正常,继承储蓄卡实现的信⽤卡功能也正常。同时,原有储蓄卡类的功能可以由信⽤卡类⽀持,即

CashCard creditCard=new CreditCard(...)。

继承作为⾯向对象的重要特征,虽然给程序开发带来了⾮常⼤的便利,但也引⼊了⼀些弊端。继承的开发⽅式会给代码带来侵⼊性,可移植能⼒降低,类之间的耦合度较⾼。当对⽗类修改时,就要考虑⼀整套⼦类的实现是否有⻛险,测试成本较⾼。⾥⽒替换原则的⽬的是使⽤约定的⽅式,让使⽤继承后的代码具备良好的扩展性和兼容性。在⽇常开发中使⽤继承的地⽅并不多,在有些公司的代码规范中也不会允许多层继承,尤其是⼀些核⼼服务的扩展。⽽继承多数⽤在系统架构初期定义好的逻辑上或抽象出的核⼼功能⾥。如果使⽤了继承,就⼀定要遵从⾥⽒替换原则,否则会让代码出现问题的概率变得更⼤

# 迪⽶特法则

迪⽶特法则(Law of Demeter,LoD)⼜称为最少知道原则(LeastKnowledge Principle,LKP),是指⼀个对象类对于其他对象类来说,知道得越少越好。也就是说,两个类之间不要有过多的耦合关系,保持最少关联性。

错误的做法

老师需要负责具体某⼀个学⽣的学习情况,⽽校⻓会关⼼⽼师所在班级的总体成绩,不会过问具体某⼀个学⽣的学习情况

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));
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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();
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

以上就是通过校⻓管理所有学⽣,⽼师只提供了⾮常简单的信息。虽然可以查询到结果,但是违背了迪⽶特法则,因为校⻓需要了解每个学⽣的情况。如果所有班级都让校⻓类统计,代码就会变得⾮常臃肿,也不易于维护和扩展

正确的做法

由⽼师负责分数统计

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;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

校⻓只负责从⽼师哪⾥收集信息即可,并不需要获取具体学⽣信息

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;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 接⼝隔离原则

⼀个类对另⼀个类的依赖应该建⽴在最⼩的接⼝上

接⼝隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞⼤的接⼝拆分成更⼩的和更具体的接⼝,让接⼝中只包含客户感兴趣的⽅法

正确的做法

Servlet事件监听器可以监听ServletContext、HttpSession、ServletRequest等域对象的创建和销毁过程,以及监听这些域对象属性的修改。

ServletContextListener接⼝

public void contextInitialized(servletContextEvent sce);
public void contextDestroyed(servletContextEvent sce);
1
2

HttpSessionListener接口

public void sessionCreated(HttpSessionEvent se);
public void sessionDestroyed(HttpSessionEvent se)
1
2

ServletRequestListener接⼝

public void requestInitialized(ServletRequestEvent sre);
public void requestDestroyed(ServletRequestEvent sre);
1
2

监听应⽤代码

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对象被销毁了");
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP)是指在设计代码架构时,⾼层模块不应该依赖于底层模块,⼆者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象

DIP就是我们常说的“⾯向接⼝编程”。依赖倒置原则是实现开闭原则的重要途径之⼀,它降低了类之间的耦合,提⾼了系统的稳定性和可维护性,同时这样的代码⼀般更易读,且便于传承

错误的做法

在互联⽹的营销活动中,经常为了拉新和促活,会做⼀些抽奖活动。这些抽奖活动的规则会随着业务的不断发展⽽调整,如随机抽奖、权重抽奖等。其中,权重是指⽤户在当前系统中的⼀个综合排名,⽐如活跃度、贡献度等

抽奖⽤户类

public class BetUser {
 private String userName; // ⽤户姓名
 private int userWeight; // ⽤户权重
}
1
2
3
4

抽奖逻辑类

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;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

正确的做法

抽奖接⼝

public interface IDraw {
 // 获取中奖⽤户接⼝
 List<BetUser> prize(List<BetUser> list, int count);
}
1
2
3
4

随机抽奖实现

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;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

权重抽奖实现

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;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

开奖

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);
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在这个类中体现了依赖倒置的重要性,可以把任何⼀种抽奖逻辑传递给这个类。这样实现的好处是可以不断地扩展,但是不需要在外部新增调⽤接⼝,降低了⼀套代码的维护成本,并提⾼了可扩展性及可维护性。另外,这⾥的重点是把实现逻辑的接⼝作为参数传递,在⼀些框架源码中经常会有这种做法