里氏替换原则(LSP)
子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
什么是LSP?
LSP在较高级别上指出,在面向对象的程序中,如果我们用其任何子类的对象替代超类对象引用,则程序不应中断。
假设我们有一个使用超类对象引用进行某些操作的方法:
class SomeClass {
void aMethod(SuperClass superClassReference) {
doSomething(superClassReference);
}
// definition of doSomething() omitted
}
对于传递给它的每个可能的子类对象,SuperClass这应该按预期工作。如果用子类对象替换超类对象以意外的方式更改了程序行为,则会违反LSP。
当通过扩展类或实现接口而存在超类型与子类型的继承关系时,可以使用LSP。我们可以将超类型中定义的方法视为定义合同。每个子类型都应遵守该合同。如果子类不遵守超类的合同,则说明它违反了LSP。
从直觉上讲,这是合理的-类的合同告诉其客户期望什么。如果子类以意想不到的方式扩展或覆盖了超类的行为,则会破坏客户端。
子类中的方法如何破坏超类方法的契约?有几种可能的方法:
返回与超类方法返回的对象不兼容的对象。
抛出超类方法未抛出的新异常。
更改不属于超类的合同的语义或引入副作用。
Java和其他静态类型的语言Object通过在编译时进行标记来防止1(除非我们使用非常通用的类,如)和2(用于检查的异常)。仍然有可能通过第三种方式违反这些语言中的LSP。
LSP为什么重要?
违反LSP是一种设计气味。我们可能过早地概括了一个概念,并创建了一个不需要的超类。对该概念的将来要求可能不适合我们创建的类层次结构。
如果客户端代码不能用子类对象随意替换超类引用,则将被迫进行instanceof检查并专门处理某些子类。如果这种条件代码分布在整个代码库中,则将很难维护。
每次添加或修改子类时,我们都必须遍历代码库并更改多个位置。这是困难且容易出错的。
它也没有达到首先引入超类型抽象的目的,即易于增强程序。
甚至不可能标识所有地点并更改它们-我们可能不拥有或控制客户代码。例如,我们可以将我们的功能开发为一个库并将其提供给外部用户。
违反LSP-示例
假设我们正在为电子商务网站构建付款模块。客户在网站上订购产品,并使用信用卡或借记卡之类的付款工具付款。
当客户提供其卡详细信息时,我们希望
验证一下
通过第三方欺诈检测系统运行它,
然后将详细信息发送到付款网关进行处理。
虽然所有卡都需要进行一些基本验证,但信用卡上还需要其他验证。付款完成后,我们会将其记录在数据库中。由于各种安全和监管原因,我们不会将实际的卡详细信息存储在我们的数据库中,而是由付款网关返回的指纹识别符。
鉴于这些要求,我们可以对我们的类进行如下建模:
abstract class PaymentInstrument {
String name;
String cardNumber;
String verificationCode;
Date expiryDate;
String fingerprint;
void validate() throws PaymentInstrumentInvalidException {
// basic validation on name, expiryDate etc.
if (name == null || name.isEmpty()) {
throw new PaymentInstrumentInvalidException("Name is invalid");
}
// other validations
}
void runFraudChecks() throws FraudDetectedException {
// run checks against a third-party system
}
void sendToPaymentGateway() throws PaymentFailedException {
// send details to payment gateway (PG) and set fingerprint from
// the payment gateway response
}
}
class CreditCard extends PaymentInstrument {
@Override
void validate() throws PaymentInstrumentInvalidException {
super.validate();
// additional validations for credit cards
}
// other credit card-specific code
}
class DebitCard extends PaymentInstrument {
// debit card-specific code
}
代码库中用于处理付款的其他区域可能看起来像这样:
class PaymentProcessor {
void process(OrderDetails orderDetails, PaymentInstrument paymentInstrument) {
try {
paymentInstrument.validate();
paymentInstrument.runFraudChecks();
paymentInstrument.sendToPaymentGateway();
saveToDatabase(orderDetails, paymentInstrument);
} catch (...){
// exception handling
}
}
void saveToDatabase(
OrderDetails orderDetails,
PaymentInstrument paymentInstrument) {
String fingerprint = paymentInstrument.getFingerprint();
// save fingerprint and order details in DB
}
}
当然,在实际的生产系统中,将要处理许多复杂的方面。上面的单个处理器类很可能是跨服务和存储库层的多个包中的一类类。
一切都很好,我们的系统正在按预期方式处理付款。在某个时候,营销团队决定引入奖励积分,以提高客户忠诚度。客户每次购买可获得少量奖励积分。他们可以使用积分在网站上购买产品。
理想情况下,我们应该能够添加一个RewardsCard扩展类PaymentInstrument并对其进行处理。但是我们发现添加它违反了LSP!
奖励卡没有欺诈检查。详细信息不会发送到支付网关,也没有指纹识别器的概念。PaymentProcessor我们添加后即会中断RewardsCard。
我们可以尝试RewardsCard通过重写runFraudChecks()并sendToPaymentGateway()使用空的,不做任何事情的实现来强制适合当前的类层次结构。
这仍然会破坏应用程序-因为指纹可能是,所以我们可能会NullPointerException从saveToDatabase()方法中得到a null。我们是否可以saveToDatabase()通过instanceof检查PaymentInstrument参数来作为特殊情况处理一次呢?
但是我们知道,如果我们做一次,我们会再做一次。不久,我们的代码库将布满多个检查和特殊情况,以处理由不正确的类模型引起的问题。我们可以想象,每次我们增强支付模块时,都会带来痛苦。
例如,如果企业决定接受比特币怎么办?还是营销引入了一种新的付款方式,例如货到付款?
修正设计
让我们重新设计并创建超类型抽象,前提是它们足够通用,足以创建可灵活更改需求的代码。我们还将使用以下面向对象的设计原则:
程序接口,不执行
封装变化
优先考虑组成而不是继承
首先,我们可以确定的是,我们的应用程序现在和将来都需要收取款项。认为我们想验证收集到的所有付款细节也是合理的。几乎所有其他一切都可能改变。因此,让我们定义以下接口:
interface IPaymentInstrument {
void validate() throws PaymentInstrumentInvalidException;
PaymentResponse collectPayment() throws PaymentFailedException;
}
class PaymentResponse {
String identifier;
}
PaymentResponse封装了identifier-可以是信用卡和借记卡的指纹,也可以是奖励卡的卡号。将来使用其他付款方式可能还有其他用途。IPaymentInstrument如果将来的付款工具具有更多数据,则封装可确保保持不变。
PaymentProcessor 现在的类如下所示:
class PaymentProcessor {
void process(
OrderDetails orderDetails,
IPaymentInstrument paymentInstrument) {
try {
paymentInstrument.validate();
PaymentResponse response = paymentInstrument.collectPayment();
saveToDatabase(orderDetails, response.getIdentifier());
} catch (...) {
// exception handling
}
}
void saveToDatabase(OrderDetails orderDetails, String identifier) {
// save the identifier and order details in DB
}
}
不再有runFraudChecks()和 sendToPaymentGateway()通话PaymentProcessor-这些通话的通用性不足以适用于所有付款方式。
让我们为问题概念中足够通用的其他概念添加一些接口:
interface IFraudChecker {
void runChecks() throws FraudDetectedException;
}
interface IPaymentGatewayHandler {
PaymentGatewayResponse handlePayment() throws PaymentFailedException;
}
interface IPaymentInstrumentValidator {
void validate() throws PaymentInstrumentInvalidException;
}
class PaymentGatewayResponse {
String fingerprint;
}
这里是实现:
class ThirdPartyFraudChecker implements IFraudChecker {
// members omitted
@Override
void runChecks() throws FraudDetectedException {
// external system call omitted
}
}
class PaymentGatewayHandler implements IPaymentGatewayHandler {
// members omitted
@Override
PaymentGatewayResponse handlePayment() throws PaymentFailedException {
// send details to payment gateway (PG), set the fingerprint
// received from PG on a PaymentGatewayResponse and return
}
}
class BankCardBasicValidator implements IPaymentInstrumentValidator {
// members like name, cardNumber etc. omitted
@Override
void validate() throws PaymentInstrumentInvalidException {
// basic validation on name, expiryDate etc.
if (name == null || name.isEmpty()) {
throw new PaymentInstrumentInvalidException("Name is invalid");
}
// other basic validations
}
}
让我们以不同的方式构成上述构建基块进行构建CreditCard和DebitCard抽象。我们首先定义一个实现的类:IPaymentInstrument
abstract class BaseBankCard implements IPaymentInstrument {
// members like name, cardNumber etc. omitted
// below dependencies will be injected at runtime
IPaymentInstrumentValidator basicValidator;
IFraudChecker fraudChecker;
IPaymentGatewayHandler gatewayHandler;
@Override
void validate() throws PaymentInstrumentInvalidException {
basicValidator.validate();
}
@Override
PaymentResponse collectPayment() throws PaymentFailedException {
PaymentResponse response = new PaymentResponse();
try {
fraudChecker.runChecks();
PaymentGatewayResponse pgResponse = gatewayHandler.handlePayment();
response.setIdentifier(pgResponse.getFingerprint());
} catch (FraudDetectedException e) {
// exception handling
}
return response;
}
}
class CreditCard extends BaseBankCard {
// constructor omitted
@Override
void validate() throws PaymentInstrumentInvalidException {
basicValidator.validate();
// additional validations for credit cards
}
}
class DebitCard extends BaseBankCard {
// constructor omitted
}
虽然CreditCard并DebitCard扩展了一个类,但它与以前不同。现在,我们代码库的其他区域仅取决于IPaymentInstrument接口,而不取决于BaseBankCard。下面的代码片段显示了CreditCard对象的创建和处理:
IPaymentGatewayHandler gatewayHandler =
new PaymentGatewayHandler(name, cardNum, code, expiryDate);
IPaymentInstrumentValidator validator =
new BankCardBasicValidator(name, cardNum, code, expiryDate);
IFraudChecker fraudChecker =
new ThirdPartyFraudChecker(name, cardNum, code, expiryDate);
CreditCard card =
new CreditCard(
name,
cardNum,
code,
expiryDate,
validator,
fraudChecker,
gatewayHandler);
paymentProcessor.process(order, card);
现在,我们的设计具有足够的灵活性,可以让我们添加RewardsCard-无需压配合,也无需进行条件检查。我们只需添加新类,它就会按预期工作。
class RewardsCard implements IPaymentInstrument {
String name;
String cardNumber;
@Override
void validate() throws PaymentInstrumentInvalidException {
// Rewards card related validations
}
@Override
PaymentResponse collectPayment() throws PaymentFailedException {
PaymentResponse response = new PaymentResponse();
// Steps related to rewards card payment like getting current
// rewards balance, updating balance etc.
response.setIdentifier(cardNumber);
return response;
}
}
这是使用新卡的客户代码:
RewardsCard card = new RewardsCard(name, cardNum);
paymentProcessor.process(order, card);
新设计的优点
新设计不仅解决了LSP违规问题,而且为我们提供了一组松散耦合的灵活类来处理不断变化的需求。例如,添加新的付款工具(如比特币和货到付款)很容易-我们只需添加实现的新类IPaymentInstrument。
企业需要借记卡由其他支付网关处理吗?没问题-我们添加了一个新类来实现IPaymentGatewayHandler并将其注入DebitCard。如果DebitCard的需求开始与CreditCard有所不同,我们可以IPaymentInstrument直接实现而不是扩展BaseBankCard-不影响其他任何类。
如果我们需要进行内部欺诈检查RewardsCard,则添加一个InhouseFraudChecker实现的工具IFraudChecker,将其注入RewardsCard并且只能更改RewardsCard.collectPayment()。
如何识别LSP违规?
识别LSP违规的一些好的指标是:
客户端代码中的条件逻辑(使用instanceof运算符或object.getClass().getName()标识实际的子类)
子类中一个或多个方法的空操作
UnsupportedOperationException从子类方法引发一个或其他意外异常
对于以上第3点,从超类的合同角度来看,该异常必须是意外的。因此,如果我们的超类方法的签名明确指定子类或实现可以抛出UnsupportedOperationException,则我们不会将其视为LSP违规。
考虑java.util.List<E>接口的add(E e)方法。由于java.util.Arrays.asList(T ...)返回一个不可修改的列表,它增加了一个元素到客户端的代码List,如果它获得通过一个将打破List由返回Arrays.asList。
这是LSP违规吗?否-该List.add(E e)方法的合同规定,实现可能会抛出UnsupportedOperationException。使用此方法时,希望客户处理此问题。
结论
在开发新应用程序以及增强或修改现有应用程序时,记住LSP是一个非常有用的想法。
在为新应用程序设计类层次结构时,LSP有助于确保我们不会在问题域中过早地概括概念。
通过添加或更改子类来增强现有应用程序时,牢记LSP有助于确保我们所做的更改与超类的合同一致,并且可以继续满足客户代码的期望。