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

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

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

相关推荐
叶帆2 天前
【YFIOs】用C#开发硬件之设备上云
开发语言·unity·c#
久数君2 天前
AI三维建模工具“造形家”:地理场景三维化的高效解决方案
unity·glb·ai算法·ai三维建模工具·地图框选·造形家·城市建筑模型
会思考的猴子3 天前
Unity VFX 属性 Postion 和 TargetPostion
unity
hai3152475433 天前
九章编程法 · 猜数字游戏 (GW-BASIC 重构版) *
人工智能·microsoft·游戏引擎·游戏程序
心前阳光3 天前
Unity资源导入之自动化资源导入
unity·自动化·游戏引擎
心前阳光3 天前
Unity之2021.3.45f2c1发布安卓程序遇到的问题
android·unity·游戏引擎
纪纯3 天前
PicoVR Unity Integration SDK 3.4 常用交互API
unity·游戏引擎·vr·pico
龙智DevSecOps解决方案3 天前
3A 游戏优化技术栈:如何打通引擎级分析工具与 DevOps 持续集成管线?
unity·性能优化·游戏开发·技术美术·perforce·unrealengine
葛兰岱尔3 天前
从 SolidWorks 到 Three.js,从 Inventor 到 Unity——制造业CAD模型“几何-语义一体化“转换,不再是天方夜谭!
开发语言·javascript·unity
鼎艺创新科技3 天前
三维电子沙盘中OSGB倾斜摄影数据的加载与渲染
游戏引擎·cocos2d