【软件构造】子类型通过重载保证LSP原则
LSP原则
LSP原则,即Liskov替换原则,可以简单概括为子类型必须能够替换其父类使用,两者的表现行为不能有任何不同。 子类型继承父类时,必须保证: 前置条件不能强化 后置条件不能弱化 不变量要保持 子类型方法参数:逆变 子类型方法的返回值:协变 异常类型:协变 对LSP原则的重要性和必要性我一直一知半解,直到在课程实验中遇到了相关问题,特此记录下我的解决问题的过程。
问题
给定一个接口:public interface Poll用于进行一次投票活动,中有一个待实现方法:
/** * 接收一个投票人对全体候选对象的投票 * * @param vote 一个投票人对全体候选对象的投票记录集合 */ public void addVote(Vote<C> vote);
已写好了第一个实现类public class GeneralPoll implements Poll用于实现匿名投票功能,在实现addVote时不涉及投票人信息,只是检查单张Vote的合法性:
@Override
public void addVote(Vote<C> vote) {
votes.add(checkSinglevote(vote));
}
另一个类RealnamePoll继承了GeneralPoll类用于实现实名投票功能,其中,我重写了addvote方法,要求输入参数是特定的类型:
public class RealnamePoll<C> extends GeneralPollImpl<C> {
......
/**
* 接收一个投票人对全体候选对象的投票
*
* @param vote 一个投票人对全体候选对象的投票记录集合
* @throws IllegalArgumentException 如果参数类型不是是RealNameVote或RealNameVote_sub
*/
@Override
public void addVote(Vote<C> vote1) throws IllegalArgumentException{
//由于RealNameVote和RealNameVote_sub具有观察等价性,用户可以随意混用,但导致了同一段代码复制两遍的代价
if(vote1.getClass().equals(RealNameVote.class)){
//略
}
else if(vote1.getClass().equals(RealNameVote_sub.class)) {
//略
}
else
throw new IllegalArgumentException("用错了!实名投票不能用匿名投票类型");
super.checkRep();
}
......
}
后来我才发现,RealnamePoll类对于addVote的重写虽然在语法上是可行的,并且满足一般的使用需求,但它违反了LSP原则:如果父类的方法不声明抛出异常,子类就不能声明抛出异常,因为这就违反子类型必须能够替换其父类使用的原则。
解决
发现这一问题时我一时陷入了迷惑:作为子类的RealnamePoll要求只接受实名投票本该是合情合理的,在一次投票过程中,如果匿名和实名的投票混用显然是不和逻辑的。一种修改方式是将throws声明去掉,在前置条件中加入限制,但这也违背了“前置条件不能强化”的LSP原则。 那么,如何在保证使用功能的条件下使之符合LSP原则呢,答案是应用JAVA的重载。将上诉addvote函数用以下两个重载方法替换:
/**
* 重载方法,接收一个投票人对全体候选对象的投票
* @param vote1 一个投票人对全体候选对象的投票记录集合
*/
public void addVote(RealNameVote<C> vote1) {
//略
}
/**
* 重载方法,接收一个投票人对全体候选对象的投票
* @param vote1 一个投票人对全体候选对象的投票记录集合
*/
public void addVote(RealNameVote_sub<C> vote1) {
//略
}
这两个都是重载方法,它们与父类方法的区别在于只接受父类所接受的参数的某个具体的子类,因此,父类的addVote方法得以保留。 当用户调用addVote并传入一个参数时,Java会自动在三个方法中寻找最能匹配用户传入参数的方法开始执行。因此,当用户传入实名投票时可视为作为实名投票类来运行,传入匿名投票时可视为作为匿名投票类来运行。 这样以来,就保证了子类型可替换父类型,也就保证了LSP原则。
