《高并发系统实战派》-- 你值得拥有
设计模式实战全集:
1,设计模式实战-工厂模式,别具匠心
2,设计模式实战-抽象工厂模式,分而治之
3,设计模式实战-单例模式,我就是天下无双
4,设计模式实战-建造者模式,做任何事情都需要步步为营
5,设计模式实战-原型模式,我们就来依法炮制
6 ,设计模式实战-适配器模式,承上启下
7 ,设计模式实战-桥接模式,想做月老吗?
8 ,设计模式实战-过滤器模式,你总是这么挑三拣四
9,设计模式实战-组合模式
10 ,设计模式实战-装饰器模式,教你怎么为代码添砖加瓦
11,设计模式实战-门面模式
12,设计模式实战-代理模式,来看看主公如何托孤
13,设计模式实战-责任链模式,超级实用
14,设计模式实战-命令模式
15,设计模式实战-解释器模式,今天给你解疑答惑
16,设计模式实战-迭代器模式,最常用的设计模式之一
17,设计模式实战-中介模式,为你牵桥搭线
18,设计模式实战-观察者模式,你知道发布订阅怎么实现吗
19,设计模式实战-状态模式,让代码更清爽简洁
20,设计模式实战-空对象模式,你肯定不知道,但是项目中会常用到到
21,设计模式实战-策略模式,想让你的代码变幻莫测吗?
22,设计模式实战-模板模式,代码复用神器
23,设计模式实战-访问者模式,减少代码体积
如果想系统学习高并发知识,可以看《高并发系统实战派》,人人都懂,人人都会的高并发系统设计噢
本节开始介绍设计模式的七大原则的基本概念,其中包括开闭原则、单一职责原则、里氏替换原则、依赖倒置原则,剩下的三大原则(接口隔离原则、迪米特法则、合成复用原则)会在下一节继续讲解。
本节以介绍基本概念为主,其中会加入部分演示代码、uml 类图讲解,能理解基本概念即可。后续章节设计模式的讲解会详细介绍这些原则的应用。
本节主要内容有:
什么是单一职责原则、里氏替换原则及依赖倒置原则
为何要遵循这些原则
开闭原则(Open Closed Principle,OCP)由勃兰特・梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),这就是开闭原则的经典定义。
开闭原则是设计模式中的总原则,开闭原则就是说:对拓展开放、对修改关闭。 模块应该在尽量不修改代码的前提下进行拓展,这就需要使用接口和抽象类来实现预期效果。
我们举例说明什么是开闭原则,以 4s 店销售汽车为例,其类图如图所示:
ICar 接口定义了汽车的两个属性:名称和价格。BenzCar 是一个奔驰车的实现类,代表所有奔驰车的总称。Shop4S 代表售卖的 4s 店,ICar 接口的代码清单如下:
public interface ICar {
// 车名
public String getName();
// 车价格
public int getPrice();
}
一般情况下 4s 店只售出一种品牌车,这里以梅赛德斯奔驰为例,奔驰车类如代码清单所示:
public class BenzCar implements ICar{
// 车名
private String name;
// 车价格
private int price;
// 通过构造方法实例化
public BenzCar(String _name, int _price) {
this.name = _name;
this.price = _price;
}
// 获取车名
@Override
public String getName() {
return this.name;
}
// 获取车价格
@Override
public int getPrice() {
return this.price;
}
}
然后我们模拟下 4s 店售车记录,Shop4S 类代码清单如下所示:
import java.util.ArrayList;
public class Shop4S {
private final static ArrayList carList = new ArrayList();
// 使用static代码块模拟数据初始化操作
static {
carList.add(new BenzCar("梅赛德斯-迈巴赫S级轿车",138));
carList.add(new BenzCar("梅赛德斯-AMG S 63 L 4MATIC+", 230));
carList.add(new BenzCar("梅赛德斯-奔驰V级", 50));
}
public static void main(String[] args) {
System.out.println("4s店售车记录:");
for (ICar car: carList){
System.out.println("车名:" + car.getName() + "\t价格:" + car.getPrice() + "万元");
}
}
}
在 Shop4S 类中,使用 static 代码块来模拟数据初始化过程,使用私有变量集合 carList 来记录所有售出车辆信息,一般项目中这部分都由持久化曾来完成,运行效果如下:
4s店售车记录:
车名:梅赛德斯-迈巴赫S级轿车 价格:138万元
车名:梅赛德斯-AMG S 63 L 4MATIC+ 价格:230万元
车名:梅赛德斯-奔驰V级 价格:50万元
暂时来看,以上设计是没有啥问题的。但是,某一天,4s 店老板说奔驰轿车统一要收取一笔金融服务费,收取规则是价格在 100 万元以上的收取 5%,50~100 万元的收取 2%,其余不收取。为了应对这种需求变化,之前的设计又该如何呢?
目前,解决方案大致有如下三种:
修改后的 FinanceBenzCar 类代码清单如下:
public class FinanceBenzCar extends BenzCar{
public FinanceBenzCar(String _name, int _price) {
super(_name, _price);
}
// 覆写价格信息
@Override
public int getPrice() {
// 获取原价
int selfPrice = super.getPrice();
int financePrice = 0;
if (selfPrice >= 100) {
financePrice = selfPrice + selfPrice * 5 / 100; // 收取5%的金融服务费
} else if (selfPrice >= 50) {
financePrice = selfPrice + selfPrice * 2 / 100; // 收取2%的金融服务费
} else {
financePrice = selfPrice; // 其余不收取服务费
}
return financePrice;
}
}
再来看看修改后的 Shop4S 类代码清单如下:
import java.util.ArrayList;
public class Shop4S {
private final static ArrayList carList = new ArrayList();
private final static ArrayList financeCarList = new ArrayList();
// 使用static代码块模拟数据初始化操作
static {
carList.add(new BenzCar("梅赛德斯-迈巴赫S级轿车",138));
carList.add(new BenzCar("梅赛德斯-AMG S 63 L 4MATIC+", 230));
carList.add(new BenzCar("梅赛德斯-奔驰V级", 50));
financeCarList.add(new FinanceBenzCar("梅赛德斯-迈巴赫S级轿车",138));
financeCarList.add(new FinanceBenzCar("梅赛德斯-AMG S 63 L 4MATIC+", 230));
financeCarList.add(new FinanceBenzCar("梅赛德斯-奔驰V级", 50));
}
public static void main(String[] args) {
System.out.println("4s店售车记录(不含金融服务费):");
for (ICar car: carList){
System.out.println("车名:" + car.getName() + "\t价格:" + car.getPrice() + "万元");
}
System.out.println("\n4s店售车记录(包含金融服务费):");
for (ICar car: financeCarList) {
System.out.println("车名:" + car.getName() + "\t价格:" + car.getPrice() + "万元");
}
}
}
运行效果如下:
4s店售车记录(不含金融服务费):
车名:梅赛德斯-迈巴赫S级轿车 价格:138万元
车名:梅赛德斯-AMG S 63 L 4MATIC+ 价格:230万元
车名:梅赛德斯-奔驰V级 价格:50万元
4s店售车记录(包含金融服务费):
车名:梅赛德斯-迈巴赫S级轿车 价格:144万元
车名:梅赛德斯-AMG S 63 L 4MATIC+ 价格:241万元
车名:梅赛德斯-奔驰V级 价格:51万元
这样,在业务规则发生改变的情况下,我们通过拓展子类及修改持久层(高层次模块)便足以应对多变的需求。开闭原则要求我们尽可能通过拓展来实现变化,尽可能少地改变已有模块,特别是底层模块。
开闭原则总结:
单一职责原则,简单来说就是保证设计类、接口、方法时做到功能单一,权责明确。怎么理解呢?比如应用开发时经常会有修改用户信息的接口,如下:这里我们定义 “更新用户” 的接口,倘若有一天新来的前端要求加一个修改用户密码的接口,后端直接说:“你去调 updateUser ” 接口吧,传入密码信息就行。这种后端往往不是太懒就是新手,updateUser 接口的粒度太粗,接口职责不够单一,所以应该将接口拆分为各个细分接口,比如修改如下:
这里很明显,我们看到分拆后的接口职责更加单一,权责更加清楚,日后维护开发也更加便捷。
单一职责原则,指的是一个类或者模块有且只有一个改变的原因。 如果模块或类承担的职责过多,就等于这些职责耦合在一起,这样一个模块的变快可能会削弱或抑制其它模块的能力,这样的耦合是十分脆弱地。所以应该尽量保持单一职责原则,此原则的核心就是解耦和增强内聚性。
在现在流行的微服务架构体系中,最头疼的就是服务拆分,拆分的粒度也很有讲究,标准的应该是遵从单一原则,避免服务拆分时发生各种撕逼行为:” 本应该在 A 服务中的被安排在了 B 服务中 “,所以服务的职责划分尤为重要。
再有就是,做 service 层开发时,早期的开发人员会将数据库操作放在 service 中,比如 getConnection,然后执行 prepareStatement,再就是 service 逻辑处理等等。可是后来发现数据库要由原来的 mysql 变更为 oracle,service 层代码岂不是需要重写一遍,天了噜… 直接崩溃跑路。
” 我单纯,所以我快乐 “用来形容单一职责原则再恰当不过了。
单一职责原则总结:
单一职责可以降低类的复杂性,提高代码可读性、可维护性
但是用 “职责” 或 “变化原因” 来衡量接口或类设计得是否优良,但是 “职责” 和 “变化原因” 都是不可度量的,因项目、环境而异;指责划分稍微不当,很容易造成资源浪费,代码量增多,好比微服务时服务边界拆分不清
里氏替换原则的解释是,所有引用基类的地方必须能透明地使用其子类的对象。 通俗来讲的话,就是说,只要父类能出现的地方子类就可以出现,并且使用子类替换掉父类的话,不会产生任何异常或错误,使用者可能根本就不需要知道是父类还是子类。反过来就不行了,有子类的地方不一定能使用父类替换。
比如某个方法接受一个 Map 型参数,那么它一定可以接受 HashMap、LinkedHashMap 等参数,但是反过来的话,一个接受 HashMap 的方法不一定能接受所有 Map 类型参数。
里氏替换原则是开闭原则的实现基础,它告诉我们设计程序的时候尽可能使用基类进行对象的定义及引用,具体运行时再决定基类对应的具体子类型。
接下来举个栗子,我们定义一个抽象类 AbstractAnimal 对象,该对象声明内部方法 “跳舞”,其中,Rabbit、Dog、Lion 分别继承该对象,另外声明一个 Person 类,该类负责喂养各种动物,Client 类负责逻辑调用,类图如下:
其中,Person 类代码如下:
public class Person {
private AbstractAnimal animal;
public void feedAnimal(AbstractAnimal _animal) {
this.animal = _animal;
}
public void walkAnimal(){
System.out.println("人开始溜动物...");
animal.dance();
}
}
main 函数调用的时候如下:
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.feedAnimal(new Rabbit());
person.walkAnimal();
}
}
打印输出:
人开始溜动物…
小白兔跳舞…
这里,Person 类中本该出现的父类 AbstractAnimal 我们运行时使用具体子类代替,只要是父类能出现的地方子类就能出现,这就要求我们模块设计时尽量以基类进行对象的定义及应用。
里氏替换原则总结:
里氏替换可以提高代码复用性,子类继承父类时自然继承到了父类的属性和方法
提高代码可拓展性,子类通过实现父类方法进行功能拓展,个性化定制
里氏替换中的继承有侵入性。继承,就必然拥有父类的属性和方法
增加了代码的耦合性。父类方法或属性的变更,需要考虑子类所引发的变更
依赖倒置原则的定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
依赖倒置原则,高层模块不应该依赖低层模块,都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。
举个栗子,拿顾客商店购物来说,定义顾客类如下,包含一个 shopping 方法:
public class Customer {
public void shopping (YanTaShop shop) {
System.out.println(shop.sell());
}
}
以上表示顾客在 "雁塔店" 进行购物,假如再加入一个新的店铺 "高新店",表示修改如下:
public class Customer {
public void shopping (GaoXinShop shop) {
System.out.println(shop.sell());
}
}
这显然是设计不合理的,违背了开闭原则。同时,顾客类的设计和店铺类绑定了,违背了依赖倒置原则。解决办法很简单,将 Shop 抽象为具体接口,shopping 入参使用接口形式,顾客类面向接口编程,如下:
public class Customer {
public void shopping (Shop shop) {
System.out.println(shop.sell());
}
}
interface Shop{
String sell();
}
高层模块不应该依赖低层模块,都应该依赖抽象(接口或抽象类)
接口或抽象类不应该依赖于实现类
实现类应该依赖于接口或抽象类
本节介绍了设计模式的几个原则,分别是开闭原则、单一职责原则、里氏替换原则、依赖倒置原则,重在理解即可,下节我们还会再介绍剩余几个原则。
在公众号菜单中可自行获取专属架构视频资料,包括不限于 java架构、python系列、人工智能系列、架构系列,以及最新面试、小程序、大前端均无私奉献,你会感谢我的哈
往期热门文章:
1,架构的本质:如何打造一个有序的系统?
2,分布式高可靠之负载均衡,今天看了你肯定会
3,分布式数据之缓存技术,一起来揭开其神秘面纱
4,分布式数据复制技术,今天就教你真正分身术
5,数据分布方式之哈希与一致性哈希,我就是个神算子
6 ,分布式存储系统三要素,掌握这些就离成功不远了
7 ,想要设计一个好的分布式系统,必须搞定这个理论
8 ,分布式通信技术之发布订阅,干货满满
9,分布式通信技术之远程调用:RPC
10 ,秒杀系统每秒上万次下单请求,我们该怎么去设计