3.Unity面向对象-里氏替换原则

里氏替换原则(LSP)指出派生类必须可以替代其基类。面向对象编程中的继承允许你通过子类添加功能。然而,若操作不当,可能导致不必要的复杂性。

SOLID的第三支柱告诉你如何应用继承使你的子类更健壮和灵活。

想象你的游戏需要一个名为Vehicle的类。这将是你将为应用程序创建的车辆子类的基类。例如,你可能需要一辆汽车或卡车。

一切从Vehicle继承。

在你可以使用基类(Vehicle)的任何地方,你应该能够使用子类(如Car或Truck)而不破坏应用程序。

你的Vehicle类可能看起来像这样:

复制代码
public class Vehicle
{
    public float speed = 100;
    public Vector3 direction;
    
    public void GoForward() { ... }
    public void Reverse() { ... }
    public void TurnRight() { ... }
    public void TurnLeft() { ... }
}

假设你正在构建一个回合制游戏,你在板上移动车辆。

汽车对火车的示例游戏

你可以有另一个名为Navigator的类来沿着预定路径驾驶车辆:

复制代码
public class Navigator
{
    public void Move(Vehicle vehicle)
    {
        vehicle.GoForward();
        vehicle.TurnLeft();
        vehicle.GoForward();
        vehicle.TurnRight();
        vehicle.GoForward();
    }
}

使用这个类,你期望能够将任何车辆传递给Navigator的Move方法,这对汽车和卡车很有效。但是,当你想实现一个名为Train的类时会发生什么?

火车会违反你的基类。

TurnLeft和TurnRight方法在Train类中不起作用,因为火车不能离开轨道。如果你确实将火车传递给Navigator的Move方法,当你到达那些行时,它会抛出未实现的异常(或什么都不做)。如果你不能用一个类型替换其子类型,你就违反了里氏替换原则。

由于Train是Vehicle的子类型,你期望在接受Vehicle的任何地方使用它。否则可能会使你的代码行为不可预测。

考虑一些更严格遵守里氏替换原则的技巧:

  • 如果你在子类删除某个功能,你可能违反里氏替换:NotImplementedException是一个明显的信号,表明你违反了这个原则。留空一个方法也是如此。如果子类不像基类那样行事,你就没有遵循LSP------即使没有明确的错误或异常。
  • 保持抽象简单:你放入基类的逻辑越多,你越有可能违反LSP。基类应该只表达派生子类的共同功能。
  • 子类需要具有与基类相同的公共成员:这些成员在调用时也需要具有相同的特性和行为。
  • 在建立类层次结构之前考虑类API:即使你将它们都视为车辆,Car和Train继承自单独的父类可能更有意义。现实中的分类并不总是转化为类层次结构。
  • 优先考虑组合而不是继承:不要试图通过继承传递功能,创建一个接口或单独的类来封装特定行为。然后通过混合和匹配不同的功能来建立"组合"。

    优先考虑组合而不是继承

要修复这个设计,废弃原始的Vehicle类型,然后将大部分功能移入接口:

复制代码
public interface ITurnable
{
    public void TurnRight();
    public void TurnLeft();
}

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

通过更严格地遵循LSP,创建一个RoadVehicle类型和RailVehicle类型。然后Car和Train将各自继承自相应的基类。

考虑Liskov替换进行重构

复制代码
public class RoadVehicle : IMovable, ITurnable
{
    public float speed = 100f;
    public float turnSpeed = 5f;
    
    public virtual void GoForward() { ... }
    public virtual void Reverse() { ... }
    public virtual void TurnLeft() { ... }
    public virtual void TurnRight() { ... }
}

public class RailVehicle : IMovable
{
    public float speed = 100;
    
    public virtual void GoForward() { ... }
    public virtual void Reverse() { ... }
}

public class Car : RoadVehicle { ... }
public class Train : RailVehicle { ... }

这样,功能通过接口而不是继承来实现。Car和Train不再共享相同的基类,现在满足了LSP。虽然你可以从同一个基类派生RoadVehicle和RailVehicle,但在这种情况下没有必要。

这种思维方式可能违反直觉,因为你对现实世界有某些假设。在软件开发中,这被称为圆-椭圆问题。并不是每个实际的"是"关系都转化为继承。记住,你希望你的软件设计驱动你的类层次结构,而不是你对现实的经验知识。

遵循里氏替换原则来限制你使用继承的方式,以保持你的代码库可扩展和灵活。

相关推荐
一只蝉nahc1 天前
vue使用iframe内嵌unity模型,并且向模型传递信息,接受信息
前端·vue.js·unity
WiChP1 天前
【V0.1B6】从零开始的2D游戏引擎开发之路
java·log4j·游戏引擎
小拉达不是臭老鼠1 天前
Unity05_3D数学
学习·unity·游戏引擎
风酥糖2 天前
Godot游戏练习01-第28节-显示效果与音效
游戏·游戏引擎·godot
油炸自行车2 天前
Unity URDF 导入后运行报错问题笔记
笔记·unity·游戏引擎·数字孪生·urdf·工业仿真·虚拟与现实
南無忘码至尊2 天前
Unity学习90天 - 第 5 天 - 阶段小项目
学习·unity·c#·游戏引擎
郝学胜-神的一滴2 天前
中级OpenGL教程 001:从Main函数到相机操控的完整实现
c++·程序人生·unity·图形渲染·unreal engine·opengl
RReality2 天前
【Unity Shader URP】顶点波浪动画(Vertex Wave)实战教程
ui·unity·游戏引擎·图形渲染
魔士于安2 天前
Unity 简单水面效果URP
游戏·unity·游戏引擎·贴图·模型
mxwin2 天前
Unity Shader 毛发 & 草海渲染Alpha‑to‑Coverage 抗锯齿技术详解
unity·游戏引擎·shader