里氏替换原则
# 里氏替换原则(Liskov Substitution Principle)
> 如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有对象O1被替换成O2时,程序P的行为没有发生变化,那么类型
>T2是类型T1的子类型;通俗的说,就是子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变以及正确性不被破坏;
>一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变;
>
## 举个栗子
举一个很常见的栗子,实际生产过程中,我们有一些热点数据是存放在本地缓存的,比如业务量不大的情况下,单体应用,这样做没什么毛病,但是业务量
稍微上升了,单体已经满足不了业务量了,这个时候就会上多实例,此时我们考虑将本地缓存的数据存放到redis中,这种设计应该是比较自然的;
下面看一下本地缓存的写法:👇
### CacheManager 本地缓存
```text
public class CacheManager {
Map<String,String> cache = new HashMap<>(16);
public String get(String key){
String val = cache.get(key);
if (StringUtils.isEmpty(val)){
return null;
}
return val;
}
public void set(String key,String value){
if (StringUtils.isEmpty(key)){
return;
}
cache.put(key,value);
}
}
```
### UserService 业务服务
```text
public class UserService {
private static Map<String, String> db = new HashMap<>(16);
static {
db.put("key1", "value1");
}
private CacheManager cacheManager;
public UserService(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public String doBiz(String key) {
String val = cacheManager.get(key);
if (StringUtils.isEmpty(val)) {
val = getFromDb(key);
cacheManager.set(key, val);
} else {
System.err.println("Get form cache! " + val);
}
return val;
}
public String getFromDb(String key){
String val = db.get(key);
System.err.println("Get form db! " + val);
return val;
}
}
```
### Client 测试类
```text
public class Client {
public static void main(String[] args) {
UserService userService = new UserService(new CacheManager());
userService.doBiz("key1");
System.err.println();
userService.doBiz("key1");
}
}
```
测试结果:
```text
Get form db! value1
Get form cache! value1
```
分析:
第一次本地Cache中肯定是没有数据的,所以第一次查询,是查询的数据库中的数据,查出来之后,将数据同步到缓存中;
第二次查询的时候,缓存中是有数据的,可以直接命中本地缓存;
那如果把Client中的key1改成Key2,则测试结果如下:
```text
Get form db! null
Get form db! null
```
分析:
1、查询key为key1的时候,能够查出来是因为,在UserService中初始化了key1的值value1;
2、这里查询key2,显然本地缓存中没有数据,数据库中也没有数据;
### RedisMamager 继承 CacheManager
```text
public class RedisMamager extends CacheManager {
Map<String, String> redis = new HashMap<>(16);
private final static String EMPTY = "empty";
@Override
public String get(String key) {
String val = redis.get(key);
if (StringUtils.isEmpty(val)) {
System.err.println("Get from redis! " + val);
return null;
}
return val;
}
@Override
public void set(String key, String value) {
if (StringUtils.isEmpty(key)) {
return;
}
if (StringUtils.isEmpty(value)) {
value = EMPTY;
}
redis.put(key, value);
}
}
```
RedisMamager会重写父类的方法,这里说明的一点是,redis在set的时候,如果数据是空的,会将一个 "empty" 字符串存放到redis中,
这样可以保护数据库,防止缓存穿透;
那我们测试一下查询key1的场景:
```text
Get from redis! null
Get form db! value1
Get form cache! value1 // 3
```
分析:
1、第一次查询redis,redis肯定是空的,然后去查询数据库,数据库是有数据的,然后也会将数据同步到redis一份;
2、第二次查询就可以从redis中查询出来了,//3 其实是从redis中查出来的;
那我们在测试一下查询key2的情况:
```text
Get from redis! null
Get form db! null
Get form cache! empty // 3
```
分析:
1、查询key2,第一次redis中是没有的,数据库中查出来的是null,然后去同步redis的时候,redis在set的时候发现value是null,
但是为了防止缓存穿透,在redis中存放了字符串 "empty";
2、第二次查询的时候,就在缓存中根据key2查询出的结果是 "empty";这。。。这显然和查询本地缓存的结果不一致嘛!
以上显然是违背了里氏替换原则,RedisManager继承了CacheManage,当系统中完成子类对父类的替换的时候,结果的正确性发生了改变,
这在生产过程中可能会造成未知的错误,或者造成严重的兼容性问题;
## 里氏替换原则和多态
可以利用面向对象编程的多态性来实现,多态和里氏替换原则优点类似,但是它们关注的点不太一样,多态是面向对象编程的特性,而里氏替换原则是一种设计原则,
用来指导继承关系中子类如何设计,子类的设计要确保在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性;
## 里氏替换原则的实现
子类在设计的时候,要遵循父类的行为约定,父类定义了方法的行为,子类可以改变方法的内部实现逻辑,但不能改变方法原有的行为约定,比如接口或者方法,
声明要实现的功能,对参数值,返回值,异常的约定,甚至包括注释中所罗列的任何说明;