接口隔离原则
接口隔离原则((Interface Segregation Principle, (ISP)),和单一职责、里氏替换原则类似,它强调,客户端不应该被强迫依赖它不使用的方法。
更通俗的说法是:一个类不应该被强制实现它用不到的接口,如果接口太大、太杂,就应该拆成多个“小而专”的接口,你可以把接口想成“协议”或“契约”,如果契约太大,签约对象可能会因为“条款太多”而苦不堪言
这条原则是专门用来避免“胖接口”的,比如你总不能让一只鸟去游泳吧
例子
先来看 C# 不太好的设计
csharp
public interface ICharacter
{
void Move();
void Attack();
void CastSpell();
}然后你有一个 Warrior 类:
csharp
public class Warrior : ICharacter
{
public void Move() { Console.WriteLine("走位~"); }
public void Attack() { Console.WriteLine("挥剑!"); }
public void CastSpell()
{
throw new NotImplementedException("战士不会魔法啊喂!");
}
}这就很尴尬…… Warrior 明明不会法术,但被接口“强制要求”去实现 CastSpell(),只好硬塞一个 throw,这就是违反了 ISP
这种情况下,你可以拆分接口,尊重各自特长
csharp
public interface IMovable
{
void Move();
}
public interface IAttackable
{
void Attack();
}
public interface ISpellCaster
{
void CastSpell();
}这样就可以按需组合了
csharp
public class Warrior : IMovable, IAttackable
{
public void Move() { /* ... */ }
public void Attack() { /* ... */ }
}
public class Mage : IMovable, IAttackable, ISpellCaster
{
public void Move() { /* ... */ }
public void Attack() { /* ... */ }
public void CastSpell() { /* ... */ }
}对于GDScript它并不支持接口,所以像上面那种通过 interface 拆行为的方式是没法直接照搬的。
你需要做的是让一个类不要写的那么臃肿,为此,你可以:
按行为拆基类(像前面我们聊过的
Attackable、Healable)或者使用组合(把技能行为抽成组件)
或者通过鸭子类型 + has_method() 来判定
违反 ISP 的例子(GDScript 版)
gdscript
class_name Character
func move():
pass
func attack():
pass
func cast_spell():
pass然后你有个 Warrior.gd:
gdscript
extends Character
func cast_spell():
push_error("战士不会法术!")这其实也就是“接口太胖”的问题在 GDScript 里的变体。
在Godot4.5版本使用抽象类和抽象方法的情况下也是一样
gdscript
@abstract class_name Character
@abstract func move()
@abstract func attack()
@abstract func cast_spell()这时候如果你让一个战士继承它:
gdscript
class_name Warrior
extends CharacterActions
func move(): print("前进!")
func attack(): print("挥砍!")
func cast_spell(): push_error("战士不会法术!")这种情况下,你要么不将cast_spell作为抽象方法,要么使用组合,将角色的行为拆分成不同的组件