Skip to content

开闭原则

开闭原则(Open-closed Principle(OCP)),即一个软件实体(类、模块、函数等)应对扩展开放,对修改关闭。

也就是说,当你的程序需要新功能时,你应该通过添加新代码(扩展)来实现功能,而不是修改已有的代码(修改)。

这样可以避免破坏现有系统,提高系统的可维护性、可复用性,也能减少因改动旧代码而引发的bug

因为动旧代码就像——在费尽功夫造好的建筑中不小心拆掉一根承重柱,一不小心就全倒了

开闭原则就像给系统留了“插槽”,每次只要插新模块,而不是动老模块 在游戏开发里无论是技能、怪物、道具、关卡逻辑……只要模块化设计得好,都会变得灵活

角色、技能、AI 行为、武器系统等都是特别适合运用开闭原则的领域

例子1

假设你在开发一个 RPG 游戏,里面有各种敌人(Enemy),最初你只设计了一个 Goblin

不符合开闭原则的写法:

gdscript
class_name Enemy

var type: String

func attack() -> void:
    if type == "Goblin":
        print("Goblin attacks with a dagger!")
    elif type == "Orc":
        print("Orc smashes with a club!")
    # 每次添加新敌人都要修改这个类
csharp
public class Enemy
{
    public string Type;

    public void Attack()
    {
        if (Type == "Goblin")
        {
            Console.WriteLine("Goblin attacks with a dagger!");
        }
        else if (Type == "Orc")
        {
            Console.WriteLine("Orc smashes with a club!");
        }
        // 每次添加新敌人都要修改这个类
    }
}

这种写法不好!每次增加一个敌人类型都要来改 Enemy 类,改一次就可能出新 bug!而且代码臃肿又不好测试!


符合开闭原则的写法:

我们可以使用基类Enemy来扩展功能,

这样,当你要添加新的敌人类型时,只需要新增一个类继承 Enemy 就可以了,完全不需要动旧代码:

gdscript
class_name Enemy

func attack() -> void: pass

# 如果是Godot 4.5版本,可以使用抽象类
@abstract class_name Enemy

@abstract func attack() -> void

class_name Goblin extends Enemy

func attack() -> void:
    print("Goblin attacks with a dagger!")
    
class_name Orc extends Enemy

func attack() -> void:
    print("Orc samshes with a club!")
    
class_name Dragon extends Enemy

func attack() -> void:
    print("Dragon breathes fire!")
csharp
public abstract class Enemy
{
    public abstract void Attack();
}

public class Goblin : Enemy
{
    public override void Attack()
    {
        Console.WriteLine("Goblin attacks with a dagger!");
    }
}

public class Orc : Enemy
{
    public override void Attack()
    {
        Console.WriteLine("Orc smashes with a club!");
    }
}

public class Dragon : Enemy
{
    public override void Attack()
    {
        Console.WriteLine("Dragon breathes fire!");
    }
}

例子2

技能系统设计

没有遵循开闭原则的写法:

gdscript
class_name SkillSystem

func use_skill(skill_name: String) -> void:
    if skill_name == "Fireball":
        print("Cast Fireball!")
    elif skill_name == "IceBlast":
        print("Cast Ice Blast!")
    elif skill_name == "Heal":
        print("Cast Heal!")
    # 添加技能就要一直加 elif
csharp
public class SkillSystem 
{
    public void UseSkill(string skillName) 
    {
        if (skillName == "Fireball") 
        {
            Console.WriteLine("Cast Fireball!");
        } 
        else if (skillName == "IceBlast") 
        {
            Console.WriteLine("Cast Ice Blast!");
        }
        else if (skillName == "Heal") 
        {
            Console.WriteLine("Cast Heal!");
        }
        // 添加技能就要一直加 else if……
    }
}

如果游戏后期加了“闪电术”,就又得改 UseSkill,这样就违背了“修改封闭”,代码容易变得一团乱麻(技能越多越可怕)


遵循开闭原则的写法:

gdscript
class_name Skill

func cast() —> void: pass

# 如果是Godot4.5也可以使用抽象类
@abstract class_name Skill

@abstract func cast() -> void

class_name Fireball extends Skill

func cast() -> void:
    pirnt("Cast Fireball!")
    
class_name IceBlast extends Skill

func cast() -> void:
    print("Cast Ice Blast!")
    
class_name SkillSystem

var _skills: Dictionary[String, Skill] = {}

func register_skill(name: String, skill: Skill) -> void:
    _skills[name] = skill
    
func use_skill(name: String) -> void:
    if _skills.has(name):
        _skills[name].cast()
csharp
public interface ISkill 
{
    void Cast();
}

public class Fireball : ISkill 
{
    public void Cast() 
    {
        Console.WriteLine("Cast Fireball!");
    }
}

public class IceBlast : ISkill {
    public void Cast() {
        Console.WriteLine("Cast Ice Blast!");
    }
}

public class SkillSystem {
    private Dictionary<string, ISkill> _skills = new();

    public void RegisterSkill(string name, ISkill skill) 
    {
        _skills[name] = skill;
    }

    public void UseSkill(string name) 
    {
        if (_skills.TryGetValue(name, out var skill)) 
        {
            skill.Cast();
        }
    }
}

然后在游戏初始化的时候:

csharp
skillSystem.RegisterSkill("Fireball", new Fireball());
skillSystem.RegisterSkill("IceBlast", new IceBlast());
gdscript
skill_system.register_skill("Fireball", Fireball.new())
skill_system.register_skill("IceBlast", IceBlast.new())