简介
SOLID是五个软件设计核心基本原则的助记首字母缩写:
- 单一职责原则(Single responsibility)
- 开闭原则(Open-closed)
- 里氏替换原则(Liskov substitution)
- 接口隔离原则(Interface segregation)
- 依赖倒置原则(Dependency inversion)
深入理解这些概念可以帮助我们编写更易于理解、灵活且易于维护的代码。
单一职责原则
一个类应该只有一个改变的理由,即它的单一职责。
单一职责原则(SRP)指出每个模块、类或函数应该只负责一件事,并且只封装该部分逻辑。
我们应该用许多较小的组件来组装项目,而不是构建庞大的类。较短的类和方法更容易解释、理解和实现。
当我们创建一个GameObject时,它包含各种较小的组件。例如,它可能包括:
- 一个MeshFilter,用于存储对3D模型的引用
- 一个Renderer,用于控制模型表面在屏幕上的显示方式
- 一个Transform,用于存储缩放、旋转和位置
- 一个Rigidbody,用于与物理模拟交互
1 | c#复制代码public class UnrefactoredPlayer : MonoBehaviour |
这个未重构的Player类包含了多种不同的职责:当玩家与物体发生碰撞时播放声音、管理输入以及处理移动等。尽管目前这个类的代码量还比较少,但随着项目的演进,它会变得越来越难以维护。建议将Player类拆分成几个更小的类。
1 | cs复制代码[RequireComponent(typeof(PlayerAudio), typeof(PlayerInput), |
开闭原则
在软件设计中,有一个叫做”开闭原则”的重要原则。它的意思是说,当我们设计一个类(class)的时候,应该让这个类易于扩展新的功能,但是不需要修改原来的代码。
举个例子,假设我们要写一个计算图形面积的程序。我们可以先写一个AreaCalculator
的类,里面有计算矩形面积和圆面积的方法。
要计算矩形的面积,只需要知道矩形的宽和高;要计算圆的面积,只需要知道圆的半径和π的值。
1 | c#复制代码public class AreaCalculator |
这样写程序没什么问题,但是如果以后要给AreaCalculator
添加新的图形,比如五边形、八边形,甚至更多其他图形,就必须在AreaCalculator
“里一直加新的方法,让这个类变得越来越臃肿。
另一种方法是写一个”图形”的父类,在里面写一个处理各种图形的方法。但这样的话,方法里面就要写一堆 if else 的判断语句来区分不同的图形,扩展起来也很麻烦。
我们真正想要的,是在不改动”面积计算器”原有代码的情况下,让它能够灵活地支持新的图形。虽然目前的AreaCalculator
能用,但它并没有遵守”开闭原则”,因为添加新图形的时候不得不修改原来的代码。
实际上,我们可以定义出一个抽象类Shape
,然后让Rectangle和Circle去实现,那么后期只需新增新的实现类,不改AreaCalculator的代码就可以拓展功能。
1 | c#复制代码public abstract class Shape |
1 | c#复制代码public class Rectangle : Shape |
这种新的设计方式可以让调试变得更容易。如果新类引入了错误,你不必再去修改”AreaCalculator”的代码。原有的代码保持不变,你只需要检查新代码中是否有错误的逻辑。要充分利用接口和抽象。这有助于避免在代码逻辑中使用冗长的 switch 或 if 语句,因为那样以后会很难扩展。一旦你习惯了按照”开闭原则”来设计类,以后添加新代码就会变得更简单。
里氏替换原则
“里氏替换原则”意思是说子类必须能够替换掉它们的父类,而不会影响程序的正确性。
假设在游戏中需要一个叫做”Vehicle”的类,它将作为其他具体车辆类(Car、Trunk)的父类。在任何使用”Vehicle”类的地方,你都应该能够使用它的子类(Car、Trunk),而不会引起程序出错。
1 | c#复制代码public class Vehicle |
假设我们需要在地图上移动各种车辆。再写一个叫做”Navigator”的类,用来指挥车辆沿着特定的路径行驶。
1 | c#复制代码public class Navigator |
这个Navigator
类的 Move 方法应该可以接受任何Vehicle
对象,用它来移动汽车和卡车都没问题。但是,如果你想要再实现一个Train
类呢?
“导航器”类中的 TurnLeft 和 TurnRight 方法显然不适用于火车,因为火车不能离开铁轨。如果把一个火车对象传入 Move 方法,程序运行到那几行代码时,会抛出一个未实现的异常,或者什么也不做。
一个类只能继承一个抽象类,但可以实现多个接口。为了满足里氏替换原则,我们用接口来重新设计。
1 | c#复制代码public interface ITurnable |
1 | c#复制代码public class RoadVehicle : IMovable, ITurnable |
通过接口实现来拆分父类,我们解决了子类不能实现父类方法的问题。
接口隔离原则
接口隔离原则简单来说就是子类不能去实现接口中用不到的方法,接口的职责要清晰。
下面的屎山代码就是典型的反例。
1 | c#复制代码public interface IUnitStats |
我们来重新设计接口,将原来的大接口拆分为4个接口,具体实现类去实现不同的接口,如下所示。
1 | c#复制代码public interface IMovable |
依赖倒置原则
依赖倒置原则其实就是高层模块不应该直接依赖低层模块,而是通过接口进行设计。
如下图所示,好的设计应该是高内聚,低耦合。
我们来看一个例子,Switch类直接依赖Door。
最大的问题就是扩展性差,如果下次加入了一个新的NewDoor类,Switch类要重新引入依赖,引起不必要的变更。
重构也很简单,我们引入一个 ISwitch 接口,解除Switch类对Door类的依赖。
不难发现,引入接口后,系统更加容易扩展,我们可以加入 Light类、NPC类等等。
1 | c#复制代码public interface ISwitchable |
当然我们也可以使用抽象类来来支持静态成员变量、常量。
但最大的问题就是一个类只能继承一个抽象类。
对NPC类来说,它实际上应该继承Robot抽象类来复用代码,实现多个不同的接口来保证一定的拓展性。
我们在选用抽象类或者接口要根据自身需求,如下表所示。
本文转载自: 掘金