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 小时前
中级OpenGL教程 013:渲染器类架构设计与逐帧渲染流程详解
开发语言·c++·unity·游戏引擎·图形渲染·opengl·unreal
_helen_52012 小时前
unreal engine 虚拟现实 + airsim
游戏引擎·vr·虚幻
真鬼1232 天前
【Unity WebGL】内嵌网页与双向通信
unity·游戏引擎·webgl
zyh______2 天前
C#语法糖(按照实用性排序)
unity·c#
avi91113 天前
[AI教做人]CinemachineCamera 各项调整配置参数入门 + Profiler
unity·游戏开发·camera·cinemachine·visual camera·镜头配置
郝学胜-神的一滴4 天前
[简化版 GAMES 101] 计算机图形学 16:纹理走样、Mipmap、三线性插值、各向异性过滤原理全解
unity·游戏引擎·godot·图形渲染·three.js·opengl·unreal
2301_767113984 天前
Superpowers 游戏引擎从零开发实战指南
游戏引擎
吴梓穆4 天前
untiy TextMeshPro (TMP) text 操作 中文字符集 字体材质操作(添加纹理 添加描边 添加阴影)
unity
想你依然心痛4 天前
嵌入式单元测试:Unity/CMock框架与硬件在环测试——测试驱动、桩函数
unity·单元测试·游戏引擎
yangmu32035 天前
《星露谷物语》MOD配置与实战安装综合指南
游戏·游戏引擎·游戏程序