原文地址——L is for the Liskov Substitution Principle——Donn Felker
到了SOLID在Android中的实践第三部,如果你错过了前面精彩的部分,可以点开第一部——单一职责原则,第二部——开/闭原则。
利斯科夫替换原则
SOLID的L代表利斯科夫替换原则(Liskov Substitution Principle,LSP),这是由Barbara Liskov在1987年会议上提出的重要课题,可以这样来描述
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
它真正的含义是什么呢?
在你反复品读这个句子试图去理解其真正含义时,如我所料,会感到越来越迷惑。不幸的是,如果在你维基百科里查询 Liskov Substitution Principle,会发现文章挖掘到更为深层次的各个计算机科学方面的知识。
写代码比理论更容易理解这个原则,在你写过的代码之中,很可能已经运用过LSP。
子类替换
Java属于静态类型语言。编译器能抓住并提示程序圆捣鼓的类型错误。例如把String
赋值给Long
或相反,编译器能如实抛出这些错误,所以编译器是写出符合利斯科夫替换原则代码的利器。
假设要写个List
,你肯定会写出过如下代码
// Get the ids somehow (loop, lookup, etc)
ArrayList ids = getCustomerIds();
List customers = customerRepository.getCustomersWithIds(ids);
这里的关键,getCustomersWithIds
方法会不会返回List
,也许CustomerRepository
已经写好了,但是后端仍未准备好,你决定使用接口隔离(Interface,或许是SOLID中的I) 。
public interface CustomerRepository {
List getCustomersWithIds(List ids);
}
public class CustomerRepositoryImpl implements CustomerRepository {
@Override
public List getCustomersWithIds(List ids) {
// Go to API, DB, etc and get the customers.
ArrayList customers = api.getWholeLottaCustomers(ids);
return customers;
}
}
CustomerRepository
需要id来获取对应的Customer
,把List
作为id列表参数,调用getCustomerIds
时,使用了ArrayList
作为返回结果。等一等,CustomerRepository
定义的返回值类型是List
而不是ArrayList
,这怎么没报错呢?
这就是利斯科夫替换原则。因为ArrayList
是List
的子类,所以当使用子类的实例替换父类的实例时,程序不会报错。
换而言之,在代码中我们使用抽象类List
,同时使用子类进行替换,为什么就不会报错呢?
原因是CustomerRepository
是使用List
来定义的接口,而ArrayList
则是继承List
,所以程序运行时,CustomerRepository
只会注意到是List
而不会注意ArrayList
。维基百科的文章解释得很好,我这里引用一下
Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable […] objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
简略而言,我们可以使用任何继承List
的子类来替换List
而无需改动其他代码。
我很肯定你肯定写过成千上万类似的代码,这很常见,切合利斯科夫替换原则很简单,对不对?
更进一步,既然CustomerRepositoryImpl
继承CustomerRepository
,任何其他继承CustomerRepository
的子类都是符合利斯科夫替换原则的。
怎么玩?
很简单,假设我们使用Dragger
注入MockCustomerRepository
,这是一个继承CustomerRepository
的子类。代码只知道如何处理CustomerRepository
的实例,因为MockCustomerRepository
继承了CustomerRepository
的所有特性,所以替换之后程序依旧正确。利斯科夫替换原则提供了巨大的可测试性和模拟性。
需要特定类型?
如果你熟悉Java,List
是继承Collection
的。
下面的编译结果如何?
// Get the ids somehow (loop, lookup, etc)
Collection ids = getCustomerIds();
List customers = customerRepository.getCustomersWithIds(ids);
为什么编译不通过?
getCustomersWithIds
只接受List
,List
是继承Collection
的,但Collection
不继承List
,所以List
是Collection
,而Collection
不一定是List
。编译器会说,这是不兼容的。
返回类型,参数及其他
利斯科夫替换原则并不局限于参数类型,例如CustomerRepository
返回ArrayList
public interface CustomerRepository {
List getCustomersWithIds(List ids);
}
public class CustomerRepositoryImpl implements CustomerRepository {
@Override
public List getCustomersWithids(List ids) {
// Go to API, DB, etc and get the customers.
ArrayList customers = api.getWholeLottaCustomers(ids);
return customers;
}
}
// Somewhere else in the program
List customers = customerRepository.getCustomersWithIds(...);
调用者并不知道返回类型是ArrayList
,只需要知道返回类型是List
就足够了。我们可以定义自己的CustomerRepository
子类,通过返回不同的List
子类,例如LinkedList
,从而实现不同的功能。
结论
利斯科夫替换原则很简单,你甚至无须知道它的名字。作为开发者,日常工作会经常运用这个原理,创造自己的接口并运用替换原则吧。
我们会在第四章讨论更多关于接口隔离的好处。
敬请期待。