设计模式六大原则之里氏替换原则

一、概念:

里氏替换原则:LSP (Liskov Substitution Principle),如果对每一个类型为T1的对象o1,都有类型为T2o2,使得以定义的所有程序P在所有的对象o1都换成时,程序的行为没有变化,那么类型是类型的子类型。

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

二、例子:

以浇水为例。人,拿到工具【水管、水桶、瓶子】,装水后都可以浇水。【水管、桶、瓶子】都可以获取水。应该有个loadWater方法。有watering 浇水功能。人浇水,人只关注浇水。拿到工具就浇水,不用考虑浇水的细节。流程是,人拿工具,用拿到的工具浇水。

类图如下:



代码如下:

Tools 抽象类:

package dim.LSP.simples;

public abstract class Tools {

	/**
	 * 装水
	 */
	public void loadWater() {
	}
	/**
	 * 浇水
	 */
	public void watering() {
	}
}

Bottle瓶子也可以是浇水工具,继承工具类Tools
package dim.LSP.simples;

public class Bottle extends Tools{

	@Override
	public void loadWater() {
		// TODO Auto-generated method stub
		System.out.println("Bottle load water");
	}

	@Override
	public void watering() {
		// TODO Auto-generated method stub
		System.out.println("bottle watering");
	}

}


waterPipe类:
package dim.LSP.simples;

public class WaterPipe  extends Tools{

	@Override
	public void loadWater() {
		// TODO Auto-generated method stub
		System.out.println("pipe load water");
	}

	@Override
	public void watering() {
		// TODO Auto-generated method stub
		System.out.println("pipe watering");
	}

}

Bucket类:
package dim.LSP.simples;

public class Bucket extends Tools{

	@Override
	public void loadWater() {
		// TODO Auto-generated method stub
		System.out.println("bucket load water");
	}

	@Override
	public void watering() {
		// TODO Auto-generated method stub
		System.out.println("bucket watering");
	}

}

种植户,浇水的人:
package dim.LSP.simples;

public class Planter {

	Tools tool=null;
	public Planter() {
		// TODO Auto-generated constructor stub
		
	}
	public void setTool(Tools tool)
	{
		this.tool=tool;
	}
	public void waterPlant()
	{
		tool.loadWater();
		tool.watering();
	}
	
}

测试类:

package dim.LSP.simples;

public class TestClass {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub

	         <span style="color:#3333ff;"><strong>	Planter planter=new Planter();
		//用瓶子浇水
		planter.setTool(new Bottle());
		planter.waterPlant();
		
		//用水管浇水
		planter.setTool(new WaterPipe());
		planter.waterPlant();
		
	</strong></span>
		
	}

}


运行结果如下:用瓶子装水,浇水。用水管装水,浇水。

Bottle load water
bottle watering


pipe load water
pipe watering

看测试类代码,浇水的人,拿到工具就浇水。planter 里面:

	public void setTool(Tools tool)
	{
		this.tool=tool;
	}
	public void waterPlant()
	{
		tool.loadWater();
		tool.watering();
	}

测试类里的代码,只要拿了工具,就可以浇水。不用考虑浇水的细节:

	Planter planter=new Planter();
		//用瓶子浇水
		planter.setTool(new Bottle());
		planter.waterPlant();
		
		//用水管浇水
		planter.setTool(new WaterPipe());
		planter.waterPlant();


实现子类对象用父类对象替换。父类能出现的地方,子类就可以出现。也就是概念中的,引用基类的地方必须能透明地使用子类对象。


但是这里有个问题,水管,怎么还要装水。水管直接可以浇水。怎么处理比较合适?可以把水管独立出来,独立为直接浇水的工具,做个单独的抽象类。

类图如下:




package dim.LSP.simples;

public abstract class DirectTools {


}

DirectTools类,可扩展:

package dim.LSP.simples;

public abstract class DirectTools {


}

DirectWaterPipe 代码:

package dim.LSP.simples;

public class DirectWaterPipe extends DirectTools {

	Tools tool=new Tools() {
		
		@Override
		public void watering() {
			// TODO Auto-generated method stub
			System.out.println("watering directly");
		}
		
		@Override
		public void loadWater() {
			// TODO Auto-generated method stub
			
		}
	};
    public Tools  getTools()
    {
    	
    	return tool;
    }
}

测试类:

package dim.LSP.simples;

public class TestClass {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub

		Planter planter=new Planter();
		//用瓶子浇水
		planter.setTool(new Bottle());
		planter.waterPlant();
		
		//用水管浇水
		planter.setTool(new WaterPipe());
		planter.waterPlant();
		
<span style="color:#3333ff;"><strong>		//用水管直接浇水
		planter.setTool(new DirectWaterPipe().getTools());
		planter.waterPlant();</strong></span>
		
	}

}


运行结果:

Bottle load water
bottle watering


pipe load water
pipe watering


watering directly


也可以把DirectWaterPipe 类直接继承Tools,重写loadWater 方法,里面什么也不做。这样有点变扭。


例2:

以视图View为例。View 可以是Button,TextView等等。View 有获取ID,设置ID,监听click等方法。把Button 的对象传给父类View 的对象。

类图如下:



代码如下:

View 抽象类:

package dim.LSP.simples.view;

public   abstract  class View {

	/**
	 * set the id of view
	 * @return
	 */
	public int getId() {
		return 0;
	}
	/**
	 * get the id of view
	 * @param id
	 */
	public   void setId(int id) {
	}
	/**
	 * listener 
	 */
	public void onClickListener() {
	}
}

Button类,继承View类:

package dim.LSP.simples.view;

public class Button extends  View{

	int btnId=0;
	@Override
	public int getId() {
		// TODO Auto-generated method stub
		return btnId;
	}

	@Override
	public void setId(int id) {
		// TODO Auto-generated method stub
		this.btnId=id;
	}

	@Override
	public void onClickListener() {
		// TODO Auto-generated method stub
		System.out.println("click button now");
	}

}

TextView类:

package dim.LSP.simples.view;

public class TextView extends View{

	private int textVid=0;
	@Override
	public int getId() {
		// TODO Auto-generated method stub
		return textVid;
	}

	@Override
	public void setId(int id) {
		// TODO Auto-generated method stub
		this.textVid=id;
	}

	@Override
	public void onClickListener() {
		// TODO Auto-generated method stub
		System.out.println("click textView now ");
	}

}

Activity类:

package dim.LSP.simples.view;

public class Activity {

	
	public int  getId(View v)
	{
		return v.getId();
	}
	
	public void click(View v)
	{
		System.out.println("view Id is "+v.getId());
		v.onClickListener();
	}
}

测试类:

package dim.LSP.simples.view;

public class TestClass {
	
public static void main(String[] args) {
	Activity activity=new Activity();
	
	//设置button ID,按一下,button
	View  btn=new Button();
	btn.setId(111);
	 activity.click(btn);

	 //设置TextView id ,按一下TextView
	View textView=new TextView();
	textView.setId(888);
	activity.click(textView);
}
}

测试结果:

view Id is 111
click button now
view Id is 888
click textView now


上面的类都做了简单的抽象,如果不用抽象类会如何?

类图如下:


使用者,每次用新工具时,都要,调用loadWater 和watering 。每次用新工具都要修改Planter类。不知道会不会抓狂。抽象了之后,可以屏蔽很多细节。


三、4层含义:

里氏替换原则为良好的继承定义了一个规范,定义包括4层含义:

  • 子类可以实现父类的抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。


这里可能会有疑问,为什么不把View和Tools设为接口。感兴趣可以看看这篇文章:接口与抽象类的区别


有所不足、多多指正、共同进步!


相关链接:设计模式六大原则之单一职责原则


参考资料:

接口与抽象类的区别

《设计模式之禅》

《HeadFirst》

《StartUML详解》

设计模式六大原则

设计模式之六大原则

相关文章

什么是设计模式一套被反复使用、多数人知晓的、经过分类编目...
单一职责原则定义(Single Responsibility Principle,SRP)...
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强...
适配器模式将一个类的接口转换成客户期望的另一个接口,使得...
策略模式定义了一系列算法族,并封装在类中,它们之间可以互...
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,...