Liskov substitution principle

里氏替換原則(Liskov substitution principle):子類(Subclass)必須要可以替代父類(Super class),即任何父類出現的地方,都可以使用其子類替換,而不影響程式的正確性。

在考慮里氏替換原則時,可以遵循以下幾點:

  • 避免移除特性:如果在父類的行為中,如果子類中沒有這個特性的話,你可能違反了里氏替換原則(If you are removing features when subclassing, you are likely breaking Liskov substitution)。在設計時,
    • 如果子類中出現了NotImplementedException,那你可能違反了里氏替換原則
    • 如果出現只能是空白的方法也可能違反了里氏替換原則
  • 保持抽象簡單:盡量讓抽象保持簡單(Keep abstractions simple):如果在基類放入越多的邏輯,那你有很大的機率違反了里氏替換原則。基類應只包含子類可以共有的方法。
  • 子類需要有與基類相同的公有成員(A subclass needs to have the same public members as the base class):這些成員在呼叫時還需要具有相同的行為。
  • 先考慮類的API再建立層次結構:在建立類的層次結構前先考慮它們的API(Consider the class API before establishing class hierarchies):現實中的分類並不總能完全轉為類的結構,例如汽車(Car)與火車(Train)皆是車輛但它們不能直接繼承同一個父類,最好是將它們分開繼承
  • 優先考慮組合而非繼承(Favor composition over inheritance):在實作時,先考慮使用介面 (Interface) 或是將行為委託給其他類,而不是直接使用繼承。

例如:假設有一個交通工具 (Vehicle) 的類層次結構,會思考:汽車 (Car) 和卡車 (Truck) 繼承自交通工具 (Vehicle),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Vehicle
{
public float Speed { get; set; }
public Vector3 Direction { get; set; }

public virtual void GoForward()
{
// 移動向前的邏輯
}

public virtual void Reverse()
{
// 倒車的邏輯
}

public virtual void TurnRight()
{
// 右轉的邏輯
}

public virtual void TurnLeft()
{
// 左轉的邏輯
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Navigator
{
public void Move(Vehicle vehicle)
{
vehicle.GoForward();
vehicle.TurnLeft();
vehicle.GoForward();
vehicle.TurnRight();
vehicle.GoForward();
}
}

但若是將火車(Train)作為車輛(Vehicle)的子類則會違反里氏替換原則,這是因為火車沒有右轉(TurnRight)與左轉(TurnLeft)的行為,因此火車不能作為車輛(Vehicle)的子類。

為了修正,可以將轉彎(Turn)和移動(Move)動作抽象為介面可轉彎(ITurnable)和可移動(IMovable)

1
2
3
4
5
6
7
8
9
10
11
public interface ITurnable
{
void TurnRight();
void TurnLeft();
}

public interface IMovable
{
void GoForward();
void Reverse();
}

並將車輛分為可以在一般道路上行駛的車輛RoadVehicle,它實作可轉彎(ITurnable)和可移動(IMovable)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RoadVehicle : IMovable, ITurnable
{
public float Speed { get; set; }
public float TurnSpeed { get; set; }

public virtual void GoForward()
{
// 移動向前的邏輯
}

public virtual void Reverse()
{
// 倒車的邏輯
}

public virtual void TurnLeft()
{
// 左轉的邏輯
}

public virtual void TurnRight()
{
// 右轉的邏輯
}
}

與需要在軌道上行駛的車輛RailVehicle,它只實作可移動(IMovable)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RailVehicle : IMovable
{
public float Speed { get; set; }

public virtual void GoForward()
{
// 移動向前的邏輯
}

public virtual void Reverse()
{
// 倒車的邏輯
}
}

最後,定義汽車和火車的類:

1
2
3
4
5
6
7
8
9
public class Car : RoadVehicle
{
// Car特有的實作
}

public class Train : RailVehicle
{
// Train特有的實作
}

這樣,Navigator 類可以安全地操作所有可以移動的交通工具,而不需要關心它們是否能轉彎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Navigator
{
public void Move(IMovable movable)
{
movable.GoForward();
movable.Reverse();
}

public void Turn(ITurnable turnable)
{
turnable.TurnLeft();
turnable.TurnRight();
}
}

評論