面向对象编程原则(05)——里氏替换原则

Posted 谷哥的小弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象编程原则(05)——里氏替换原则相关的知识,希望对你有一定的参考价值。


版权声明

  • 本文原创作者:谷哥的小弟
  • 作者博客地址:http://blog.csdn.net/lfdfhl

参考资料

  1. 《大话设计模式》 作者:程杰
  2. 《Java设计模式》 作者:刘伟
  3. 《图解设计模式》 作者:结城浩
  4. 《重学Java设计模式》 作者:付政委
  5. 《Head First设计模式》作者:埃里克·弗里曼

里氏替换原则概述

里氏替换原则(Liskov Substitution Principle,LSP)由2008年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院的Barbara Liskov(芭芭拉·利斯科夫)教授和卡内基·梅隆大学的JeannetteWing教授于1994年提出;所以,里氏替换原则以BarbaraLiskov教授的姓氏命名。

该原则定义如下:

所有引用基类的地方必须能透明地使用其子类的对象。

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

里氏替换原则表明,在软件中将一个基类对象替换成它的子类对象时程序将不会产生任何错误和异常。反过来则不成立,即:如果一个软件实体使用的是一个子类对象,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是,我喜欢狗不能据此断定我喜欢所有的动物。

里氏替换原则是实现开闭原则的重要方式之一,由于在使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型用子类对象来替换父类对象。

在运用里氏替换原则时应该将父类设计为抽象类或者接口,让子类继承父类或实现父接口并实现在父类中声明的方法;在运行时子类实例替换父类实例可以很方便地扩展系统的功能而无需修改原有子类的代码。与此同时,假若需要增加新的功能则可以通过增加一个新的子类来实现。

通俗地说,对于里氏替换原则,我们可作如下表述:任何基类可以出现的地方,子类一定可以出现。所以,子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时除添加新的方法完成新增功能外尽量不要重写父类的方法。

古代哲学与墨子的智慧

《墨子:小取》中说,“白马,马也;乘白马,乘马也。骊马,马也;乘骊马,乘马也”。原文的意思是:白马和黑马都是马,乘白马或者乘黑马都是乘马。在面向对象中我们可以这样理解:马是一个父类,白马和黑马都是马的子类;我们说乘马是没有问题的,那么我们把父类换成具体的子类,也就是乘白马和乘黑马也是没有问题的,这就是我们刚才说的里氏替换原则。

墨子同时还指出了反过来是不能成立的情况。《墨子:小取》中说:“娣,美人也,爱娣,非爱美人也”。原文的意思是:我的妹妹是美人,我爱我的妹妹,但是不等于我爱美人。

里氏替换原则案例

在此,以案例形式介绍里氏替换原则。

版本一

从数学常识来说正方形是特殊的长方形。所以,我们可以把长方形作为父类,把正方形作为子类。当我们为长方形设置了长宽后再让其宽自动增长直至超过长。类似地,我们可以为正方形设置长宽后再让其宽自动增长直至超过长么?答案是否定的,因为正方形的长和宽是一样的。

Rectangle

package com.liskov01;
/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Rectangle 
    private double length;
    private double width;
    
    public Rectangle() 
		
	

	public Rectangle(double length, double width) 
		super();
		this.length = length;
		this.width = width;
	

	public double getLength() 
        return length;
    

    public void setLength(double length) 
        this.length = length;
    

    public double getWidth() 
        return width;
    

    public void setWidth(double width) 
        this.width = width;
    

	@Override
	public String toString() 
		return "Rectangle [length=" + length + ", width=" + width + "]";
	
    

Square

package com.liskov01;
/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Square extends Rectangle 
    
	// 设置长方形的宽时同时修改其长
	@Override
    public void setWidth(double width) 
        super.setWidth(width);
        super.setLength(width);
    

	// 设置长方形的长时同时修改其宽
	@Override
    public void setLength(double length) 
		super.setWidth(length);
        super.setLength(length);
    

Test

package com.liskov01;
/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Test 

	public static void main(String[] args) 
		Rectangle rectangle = new Rectangle();
		rectangle.setWidth(10);
		rectangle.setLength(20);
		resize(rectangle);
		printLengthAndWidth(rectangle);

		System.out.println("---The next code is wrong---");

		Rectangle square = new Square();
		square.setLength(20);
		square.setWidth(20);
		resize(square);
		printLengthAndWidth(square);
	

	// 设置长方形的宽大于长
	public static void resize(Rectangle rectangle) 
		while (rectangle.getWidth() <= rectangle.getLength()) 
			double width = rectangle.getWidth();
			rectangle.setWidth(width + 1);
		
	

	// 打印长方形的长和宽
	public static void printLengthAndWidth(Rectangle rectangle) 
		System.out.println("length="+rectangle.getLength());
		System.out.println("width="+rectangle.getWidth());
	




错误分析

当Rectangle类的对象调用resize( )方法是不会错误的;但是,当Rectangle的子类Square类的对象调用resize( )方法却会发生系统溢出错误。显然,此处违背了里氏替换原则——不能用子类替换父类。也就是说:在resize( )方法中,Rectangle类型的参数是不能被Square类型的参数所代替。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系也不成立。从编码角度来说,正方形不是长方形。

版本二

我们单独创建一个Shape图形接口,让Rectangle和Square实现该接口。

Shape

package com.liskov02;
/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public interface Shape 
    //获取长
    double getLength();
    //获取宽
    double getWidth();

Rectangle

package com.liskov02;
/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Rectangle implements Shape
    private double length;
    private double width;
    
    public Rectangle() 
		
	

	public Rectangle(double length, double width) 
		super();
		this.length = length;
		this.width = width;
	

	// 实现Shape接口中的方法
	public double getLength() 
        return length;
    

    public void setLength(double length) 
        this.length = length;
    

    // 实现Shape接口中的方法
    public double getWidth() 
        return width;
    

    public void setWidth(double width) 
        this.width = width;
    

	@Override
	public String toString() 
		return "Rectangle [length=" + length + ", width=" + width + "]";
	
    

Square

package com.liskov02;

/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Square implements Shape 

	private double side;

	public double getSide() 
		return side;
	

	public void setSide(double side) 
		this.side = side;
	

	// 实现Shape接口中的方法
	public double getLength() 
		return side;
	

	// 实现Shape接口中的方法
	public double getWidth() 
		return side;
	


Test

package com.liskov02;
/**
 * 本文作者:谷哥的小弟 
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Test 

	public static void main(String[] args) 
		Rectangle rectangle = new Rectangle();
		rectangle.setWidth(10);
		rectangle.setLength(20);
		resize(rectangle);
		printLengthAndWidth(rectangle);

		System.out.println("---The next code is right---");

		Square square = new Square();
		square.setSide(20);
		printLengthAndWidth(square);
	

	// 设置长方形的宽大于长
	public static void resize(Rectangle rectangle) 
		while (rectangle.getWidth() <= rectangle.getLength()) 
			double width = rectangle.getWidth();
			rectangle.setWidth(width + 1);
		
	

	// 打印图形的长和宽
	public static void printLengthAndWidth(Shape shape) 
		System.out.println("length="+shape.getLength());
		System.out.println("width="+shape.getWidth());
	




以上是关于面向对象编程原则(05)——里氏替换原则的主要内容,如果未能解决你的问题,请参考以下文章

面向对象原则之一 里氏替换原则

设计模式里氏代换原则

面向对象编程设计原则

里氏替换原则

面向对象设计原则三:里氏替换原则(LSP)

面向对象设计原则 里氏替换原则(Liskov Substitution Principle)