里氏替换原则

# 里氏替换原则(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,当系统中完成子类对父类的替换的时候,结果的正确性发生了改变, 这在生产过程中可能会造成未知的错误,或者造成严重的兼容性问题; ## 里氏替换原则和多态 可以利用面向对象编程的多态性来实现,多态和里氏替换原则优点类似,但是它们关注的点不太一样,多态是面向对象编程的特性,而里氏替换原则是一种设计原则, 用来指导继承关系中子类如何设计,子类的设计要确保在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性; ## 里氏替换原则的实现 子类在设计的时候,要遵循父类的行为约定,父类定义了方法的行为,子类可以改变方法的内部实现逻辑,但不能改变方法原有的行为约定,比如接口或者方法, 声明要实现的功能,对参数值,返回值,异常的约定,甚至包括注释中所罗列的任何说明;