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,但在这种情况下没有必要。

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

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

相关推荐
寂寞的船7 小时前
【Unity知识速递】第10期 Unity GC 零分配实战:从 UnityEngine.Pool 到手动 GC 控制的全链路优化
jvm·unity·c#·游戏引擎·lucene
Martina_03211 天前
AI生成3D场景的霓虹灯一开就糊成光团?用6步检查Emissive、Bloom与曝光
人工智能·3d·ai·自然语言处理·游戏引擎·材质·贴图
寂寞的船2 天前
【Unity知识速递】第8期 IL2CPP 托管堆内存碎片与原生堆交互机制深度解析
unity·游戏引擎·交互
寂寞的船3 天前
【Unity知识速递】第5期 Unity 6.2 深度解析:AI Assistant 登场,World Space UI 与 Sentis 全面升级
人工智能·ui·unity
寂寞的船3 天前
【Unity知识速递】第4期 MCP for Unity:用 AI 自然语言控制编辑器
人工智能·unity·编辑器
淡海水3 天前
09-04-内存-Unity原生对象生命周期
unity·c#·游戏引擎·gc
寂寞的船3 天前
【Unity知识速递】第6期 URP性能优化完全指南:从内存到GPU的三维调优
unity·性能优化·编辑器
寂寞的船3 天前
【Unity Bug挑战】第10期 SerializeReference 内存泄漏之谜
unity·c#·游戏引擎·bug
tealcwu5 天前
【Unity基础】为什么游戏需要物理按键映射?从 Unity Input Manager 到跨引擎输入设计
java·游戏·unity
tealcwu6 天前
【Unity高级】Unity URP:构建同时支持 2D 与 3D 场景的项目完全指南
3d·unity·游戏引擎