Design Pattern, Java, OO Design Principles

Liskov Substitution Principle

Liskov Substitution (LSP)

Đây là nguyên lý thứ 3 trong SOLID. Nguyên lý này nói rằng các lớp dẫn xuất phải có thể được thay thế bởi lớp cha. Nguyên lý này được Barbara Liskov đề cập lần đầu tiên trong quyển “Data Abstraction and Hiearchy” xuất bản năm 1988. Xin nói thêm rằng Barbara Liskov là người phụ nữ đầu tiên ở Mỹ nhận bằng tiến sĩ trong lĩnh vực công nghệ thông tin.

Nguyên lý Thay thế Liskov phát biểu như sau:
“Lớp D được gọi là kế thừa từ lớp B khi và chỉ khi với mọi hàm F thao tác trên các đối tượng của B, cách cư xử (behavior) của F không đổi khi thay thế các đối tượng của B bằng các đối tượng của D”.

Để hiểu được nguyên lý trên, ta cần xem xét lại khái niệm kế thừa trong lập trình hướng đối tượng. Kế thừa (inheritance) là một trong 3 tính chất cơ bản nhất của lập trình hướng đối tượng (bên cạnh tính đóng gói (encapsulation) và tính đa hình (polymorphism)). Đó là tính chất một lớp có khả năng “tái sử dụng” các thuộc tính và phương thức của lớp khác và quan trọng hơn là nó có thể cư xử (behavior) như lớp đó. Quan hệ “IS A” thường được dùng để xác định kế thừa. A kế thừa B khi A quan hệ “IS A” với B. Có nghĩa A là trường hợp đặc biệt của B.

Nghe thì có vẻ phức tạp, lằng nhằng, nhưng khi nhìn vào thực tế thì nguyên lý này khá đơn giản. Hãy xem xét 1 ví dụ đơn giản sau đây.
Chúng ta biết “hình vuông là hình chữ nhật”. Nhưng hình chữ nhật có thay thế được hình vuông? Hãy xét mối quan hệ kế thừa đơn giản trong hướng đối tượng giữa hình chữ nhật và hình vuông.

// Violation of Likov's Substitution Principle
class Rectangle
{
	protected int m_width;
	protected int m_height;

	public void setWidth(int width){
		m_width = width;
	}

	public void setHeight(int height){
		m_height = height;
	}

	public int getWidth(){
		return m_width;
	}

	public int getHeight(){
		return m_height;
	}

	public int getArea(){
		return m_width * m_height;
	}
}

class Square extends Rectangle
{
	public void setWidth(int width){
		m_width = width;
		m_height = width;
	}

	public void setHeight(int height){
		m_width = height;
		m_height = height;
	}

}

class LspTest
{
	private static Rectangle getNewRectangle()
	{
		// it can be an object returned by some factory ...
		return new Square();
	}

	public static void main (String args[])
	{
		Rectangle r = LspTest.getNewRectangle();

		r.setWidth(5);
		r.setHeight(10);
		// user knows that r it's a rectangle.
		// It assumes that he's able to set the width and height as for the base class

		System.out.println(r.getArea());
		// now he's surprised to see that the area is 100 instead of 50.
	}
}

Ở ví dụ trên, chúng ta đã vi phạm nguyên lý thay thế Liskov bởi vì behavior của Width và Heigh đã bị thay đổi ở lớp Square. Vì vậy, khi thay thế Square bằng Rectangle sẽ dẫn đến kết quả không đúng. (Rectangle phải ra 5×10=50 chứ không phải 10×10=100).

Chúng ta phải bảo đảm rằng, khi một lớp con kế thừa từ một lớp khác, nó sẽ không làm thay đổi hành vi của lớp đó. Hay trong ví dụ này là lớp Square không được phép thay đổi hành vi của lớp Rectangle (Width và Height)

Bây giờ hãy tạo ra một thiết kế mới bằng cách áp dụng nguyên lý thay thế Liskov. Chúng ta có 1 lớp Shape sẽ là lớp cơ sở cho cả 2 lớp Rectangle và Square.

Shape.java

/**
 *    	OO Design Principle Tutorial
 *		View more at: https://edwardthienhoang.wordpress.com/
 */
package edward.tutorial.designprinciple.liskov;

/**
 * Class desciption
 * @author Edward
 */
public abstract class Shape {

	/** mHeight */
	protected int mHeight;

	/** mWidth */
	protected int mWidth;

	/**
	 * Get shape's width
	 * @author Edward
	 * @return
	 */
	public abstract int getWidth();

	/**
	 * Set shape's width
	 * @author Edward
	 */
	public abstract void setWidth(int inWidth);

	/**
	 * Get shape's height
	 * @author Edward
	 * @return
	 */
	public abstract int getHeight();

	/**
	 * Set shape's height
	 * @author Edward
	 */
	public abstract void setHeight(int inHeight);

    /**Return area of shape
     * @author Edward
     * @return
     */
    public int getArea() {
        return mHeight * mWidth;
    }
}

Rectangle.java

/**
 *    	OO Design Principle Tutorial
 *		View more at: https://edwardthienhoang.wordpress.com/
 */
package edward.tutorial.designprinciple.liskov;

/**
 * Rectangle class
 * @author Edward
 */
public class Rectangle extends Shape
{
	@Override
	public int getWidth() {
		return mWidth;
	}

	@Override
	public int getHeight() {
		return mHeight;
	}

	@Override
	public void setWidth(int inWidth) {
		mWidth = inWidth;
	}

	@Override
	public void setHeight(int inHeight) {
		mHeight = inHeight;
	}

}

Square.java

/**
 *    	OO Design Principle Tutorial
 *		View more at: https://edwardthienhoang.wordpress.com/
 */
package edward.tutorial.designprinciple.liskov;

/**
 * Class desciption
 * @author Edward
 */
public class Square extends Shape {

	@Override
	public int getWidth() {
		return mWidth;
	}

	@Override
	public void setWidth(int inWidth) {
		SetWidthAndHeight(inWidth);
	}

	@Override
	public int getHeight() {
		return mHeight;
	}

	@Override
	public void setHeight(int inHeight) {
		SetWidthAndHeight(inHeight);
	}

	/**
	 * Set both width and height with the same value
	 * @author Edward
	 * @param inValue
	 */
	private void SetWidthAndHeight(int inValue) {
        mHeight = inValue;
        mWidth = inValue;
    }

}

Main.java

/**
 *    	OO Design Principle Tutorial
 *		View more at: https://edwardthienhoang.wordpress.com/
 */
package edward.tutorial.designprinciple.liskov;

/**
 * Main class
 * @author Edward
 */
public class Main {

	static final String SQUARE = "Square";
	static final String RECTANGLE = "Rectangle";

	/**
	 * Main method
	 * @author Edward
	 * @param args
	 */
	public static void main(String[] args) {
		Shape shape1 = getShape(SQUARE);
		shape1.setHeight(10);
		shape1.setWidth(20);
		System.out.println(SQUARE + "'s area: " + shape1.getArea());

		Shape shape2 = getShape(RECTANGLE);
		shape2.setHeight(10);
		shape2.setWidth(20);
		System.out.println(RECTANGLE + "'s area: " + shape2.getArea());
	}

	/**
	 * Simple factory method
	 * @author Edward
	 * @param inShapeType
	 * @return
	 */
	static Shape getShape(String inShapeType) {
		if(inShapeType.equals(SQUARE)) {
			return new Square();
		}
		if(inShapeType.equals(RECTANGLE)) {
			return new Rectangle();
		}
		return null;
	}

}

Output:
Square’s area: 400
Rectangle’s area: 200

Bây giờ chạy, chúng ta đã có kết quả đúng.

Như vậy chúng ta vừa tìm hiểu xong 1 nguyên lý nữa trong SOLID. Nhìn thì có vẻ đơn giản, nhưng nếu các bạn vi phạm nguyên lý đơn giản này có thể để lại hậu quả không thể lường trước được. Hãy chắc rằng bạn chỉ nên override lại các thuộc tính hay hành vi trừu tượng ở lớp cha vả đảm bảo việc override đó không ảnh hưởng đến các hành vi và phương thức còn lại của lớp cha như ví dụ trên.

Nguồn: https://edwardthienhoang.wordpress.com/
Email: edwardthienhoang@gmail.com
Advertisements

3 thoughts on “Liskov Substitution Principle”

  1. Chào bạn, mình đọc bài của bạn đoạn mã đúng thì mình k bàn đến, nhưng đoạn mã k áp dụng nguyên tắc LSP thì mình thấy vấn đề như sau:
    Ở client mình sửa nó thành:

    private static Rectangle getNewRectangle(String type) {
    if (type.equals(SQUARE)) {
    return new Square();
    }
    if (type.equals(RECTANGLE)) {
    return new Rectangle();
    }
    return null;
    }

    public static void main(String[] args) {

    Square s = (Square) Client.getNewRectangle(SQUARE);
    s.setHeight(10);
    s.setWidth(20);
    System.out.println(s.getArea());

    Rectangle r = Client.getNewRectangle(RECTANGLE);
    r.setHeight(10);
    r.setWidth(20);
    System.out.println(r.getArea());
    }
    Thì mình thấy đoạn mã này có kết quả bình thường. Mình thấy bạn so sánh nên so sánh vs đoạn mã tương đương chứ còn đoạn mã sai ở trên ko nói lên được điều gì cả.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s