编程思想

内聚

内聚:模块内部具有相同特点的相似程度

小到一个方法,一个接口、一个类,大到一个业务、一个功能、一个系统,都叫模块。

比如新闻管理类只负责新闻相关的功能(与新闻相关):新闻查看、新闻删除、新闻更新。而用户类只负责用户相关的功能(与用户相关):用户登陆、用户注册。

高内聚就是模块内部相同特点的相似程度很高,不会混入不相关的模块,比如新闻管理模块不会混入用户模块相关的内容。

耦合

耦合:代码之间的依赖程度如果两个类或模块之间的依赖关系太强(高耦合),修改其中一个可能会影响另一个,导致代码难以维护和扩展。相反,如果依赖关系松散(低耦合),代码会更容易修改和复用。

例子

想象你在玩积木:

  • 高耦合:两块积木粘在一起,拆开一个可能会破坏另一个。

  • 低耦合:积木之间只是简单拼接,拆开一个不会影响其他积木。

在Unity中,低耦合的代码就像积木一样,模块之间可以独立修改和替换,而不会影响其他部分。

高耦合 vs 低耦合

假设我们有一个玩家角色(Player),它需要攻击敌人(Enemy)。在高耦合的设计中,Player 直接依赖 Enemy 的具体实现;

// Enemy.cs
public class Enemy : MonoBehaviour
{
    public int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Destroy(gameObject);
        }
    }
}

// Player.cs
public class Player : MonoBehaviour
{
    public Enemy enemy; // 直接依赖 Enemy 类

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            enemy.TakeDamage(10); // 直接调用 Enemy 的方法
        }
    }
}

我们看上面的代码,Player 直接依赖 Enemy,如果以后需要攻击其他对象(如 BossCrate),必须修改 Player 的代码。它的代码复用性差,Player 只能攻击 Enemy

而在低耦合的设计中,Player 只依赖一个抽象的接口(如 IDamageable),这样任何实现了 IDamageable 的对象都可以被攻击

Player 不再依赖具体的 Enemy,而是依赖抽象。

// IDamageable.cs
public interface IDamageable
{
    void TakeDamage(int damage);
}

// Enemy.cs
public class Enemy : MonoBehaviour, IDamageable
{
    public int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Destroy(gameObject);
        }
    }
}

// Crate.cs(一个新的可被攻击对象)
public class Crate : MonoBehaviour, IDamageable
{
    public int health = 50;

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Destroy(gameObject);
        }
    }
}

// Player.cs
public class Player : MonoBehaviour
{
    public IDamageable target; // 依赖抽象接口

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (target != null)
            {
                target.TakeDamage(10); // 攻击任何实现了 IDamageable 的对象
            }
        }
    }
}

Player 不再依赖具体的 Enemy,而是依赖 IDamageable 接口。

可以轻松扩展,比如新增 BossCrate,只要它们实现 IDamageablePlayer 就能攻击它们。

代码更灵活、更易维护

为什么依赖抽象会降低耦合?

依赖抽象的意思是,代码依赖于接口抽象类,而不是具体的实现类。比如:

  • 依赖抽象:Player 依赖于 IDamageable 接口。

  • 依赖具体:Player 依赖于 Enemy 类。

(1)减少对具体实现的依赖

  • 依赖具体:如果 Player 直接依赖 Enemy,那么 Player 的代码就和 Enemy 的实现绑定了。如果 Enemy 的实现变了(比如方法名改了),Player 的代码也得跟着改。

  • 依赖抽象:如果 Player 依赖 IDamageable 接口,那么只要 Enemy 实现了 IDamageablePlayer 就不需要关心 Enemy 的具体实现。即使 Enemy 的实现变了,只要接口不变,Player 的代码就不需要改。

(2)提高代码的灵活性

  • 依赖具体:如果 Player 只能攻击 Enemy,那么以后想攻击其他对象(比如 BossCrate),就必须修改 Player 的代码。

  • 依赖抽象:如果 Player 依赖 IDamageable,那么任何实现了 IDamageable 的对象(如 EnemyBossCrate)都可以被攻击,不需要修改 Player 的代码。

(3)更容易测试

  • 依赖具体:如果 Player 直接依赖 Enemy,那么在测试 Player 时,必须创建一个真实的 Enemy 对象,这可能会导致测试复杂(比如 Enemy 依赖其他资源)。

  • 依赖抽象:如果 Player 依赖 IDamageable,那么在测试时可以用一个模拟对象(Mock Object)来代替 Enemy,这样测试更简单、更独立。

(4)符合开闭原则

  • 开闭原则:软件实体(类、模块、函数)应该对扩展开放,对修改关闭。

  • 依赖抽象:通过依赖抽象,我们可以在不修改现有代码的情况下扩展功能。比如新增一个 Boss 类,只要它实现了 IDamageablePlayer 就能直接攻击它,而不需要修改 Player 的代码。

单一职责原则SRP

一个类只做一件事

结合例子

想象你在厨房做饭:

  • 如果你用一个工具(比如刀)来切菜、削皮、搅拌,那么这个工具会变得复杂且难以维护。

  • 如果你用不同的工具(刀切菜、削皮器削皮、搅拌器搅拌),每个工具只做一件事,那么它们会更简单、更高效。

单一职责原则就像厨房里的工具分工,每个类只负责一件事,这样代码会更清晰、更易维护。

违反单一职责 VS 单一职责

假设我们有一个 Player 类,它负责管理玩家的状态(如健康值)和玩家的攻击行为。如果把这些功能都塞到一个类里,就会违反单一职责原则。

public class Player : MonoBehaviour
{
    public int health = 100;

    // 负责管理健康值
    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        Debug.Log("Player has died!");
    }

    // 负责攻击行为
    public void Attack(Enemy enemy)
    {
        enemy.TakeDamage(10);
    }
}

Player 类既负责管理健康值,又负责攻击行为。如果健康值逻辑或攻击逻辑需要修改,Player 类就会变得复杂。

如果以后需要扩展(比如增加防御行为),Player 类会变得越来越臃肿。

遵循单一职责原则的代码

Player 类的职责拆分为两个类:

  1. PlayerHealth:负责管理玩家的健康值。

  2. PlayerAttack:负责玩家的攻击行为。

// PlayerHealth.cs
public class PlayerHealth : MonoBehaviour
{
    public int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        Debug.Log("Player has died!");
    }
}

// PlayerAttack.cs
public class PlayerAttack : MonoBehaviour
{
    public void Attack(Enemy enemy)
    {
        enemy.TakeDamage(10);
    }
}

// Player.cs
public class Player : MonoBehaviour
{
    public PlayerHealth health;
    public PlayerAttack attack;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            attack.Attack(FindObjectOfType<Enemy>());
        }
    }
}

在Unity中如何实践单一职责原则?

  1. 拆分职责:如果一个类做了太多事情,就把它拆分成多个小类。

  2. 组件化设计:Unity 的 GameObject 可以通过添加多个组件来实现功能。比如:

    • Player 对象可以挂载 PlayerHealthPlayerAttack 两个脚本。

    • 每个脚本只负责一个功能。

  3. 高内聚,低耦合:让每个类专注于自己的职责,减少类之间的依赖。

开闭原则(重要)

开闭:对扩展开放,对修改关闭。换句话说,当需要添加新功能时,应该通过扩展(添加新代码)来实现,而不是修改现有的代码

核心目的:使用抽象,封装变化

结合例子

想象你在搭积木:

  • 如果你每次想添加新功能(比如加一个窗户),都需要拆掉现有的积木(修改现有代码),那么整个结构会变得不稳定。

  • 如果你设计了一个可以扩展的积木系统(比如预留插槽),那么你可以直接添加新积木(扩展新代码),而不需要拆掉现有的积木。

开闭原则就像这个可扩展的积木系统,它让代码更容易扩展,同时保持稳定。

违反开闭原则 vs 遵循开闭原则

假设我们有一个游戏,里面有不同类型的敌人(如 EnemyBoss)。我们需要计算每个敌人的伤害。如果直接在代码中写死每种敌人的伤害计算逻辑,就会违反开闭原则。

public class Enemy
{
    public string type;

    public int CalculateDamage()
    {
        if (type == "Normal")
        {
            return 10;
        }
        else if (type == "Boss")
        {
            return 50;
        }
        return 0;
    }
}

如果新增一种敌人(如 MiniBoss),必须修改 CalculateDamage 方法。

每次新增敌人类型,都需要修改现有代码,这可能会导致引入新的 bug。

遵循开闭原则的代码

通过使用抽象类接口,我们可以让代码对扩展开放,对修改关闭。

// 定义抽象类
public abstract class Enemy
{
    public abstract int CalculateDamage();
}

// 具体实现:普通敌人
public class NormalEnemy : Enemy
{
    public override int CalculateDamage()
    {
        return 10;
    }
}

// 具体实现:Boss
public class BossEnemy : Enemy
{
    public override int CalculateDamage()
    {
        return 50;
    }
}

// 具体实现:MiniBoss(新增类型)
public class MiniBoss : Enemy
{
    public override int CalculateDamage()
    {
        return 30;
    }
}

如果需要新增一种敌人(如 MiniBoss),只需要创建一个新的类(如 MiniBoss),而不需要修改现有的 Enemy 类或 CalculateDamage 方法。现有代码保持稳定,新增功能不会影响原有逻辑

如何在游戏开发中,有意识的使用开闭原则?

将可变的行为抽象出来,让具体实现通过扩展来实现,而不是修改现有代码。

假设游戏中有多种敌人(如 NormalEnemyBossEnemy),每种敌人的行为不同。

// 定义抽象类或接口
public abstract class Enemy
{
    public abstract void Attack();
    public abstract void TakeDamage(int damage);
}

// 具体实现:普通敌人
public class NormalEnemy : Enemy
{
    public override void Attack()
    {
        Debug.Log("Normal Enemy attacks!");
    }

    public override void TakeDamage(int damage)
    {
        Debug.Log($"Normal Enemy takes {damage} damage!");
    }
}

// 具体实现:Boss
public class BossEnemy : Enemy
{
    public override void Attack()
    {
        Debug.Log("Boss Enemy attacks with a powerful strike!");
    }

    public override void TakeDamage(int damage)
    {
        Debug.Log($"Boss Enemy takes {damage} damage and gets angrier!");
    }
}

依赖倒置原则

客户端代码尽量依赖抽象的组件。因为抽象是稳定的,实现是多变的

  • 高层模块(调用者)不应该依赖于底层模块(被调用者)。两个都应该依赖于抽象。

  • 抽象不应该依赖于细节,细节应该依赖于抽象

案例:歌手可以唱情歌。

分析:找对象、找行为

  • 对象1:歌手(唱歌)

  • 对象2:情歌(返回歌词)

class QingGe{
	public void GetQingGeWords(){
		Debug.log("情歌歌词")
	}
}
class Singer{
	public void SongQingGe(QingGe qingge){
		qingge.GetQingGeWords();
	}
}

// 客户端
Singer s = new Singer();
s.SongQingGe(new QingGe());

我们发现高层模块(Singer)依赖于了底层模块(QingGe)

此时添加一个新的功能,歌手可以唱古典歌曲,我们还要去修改Singer的代码。

故不符合依赖倒置原则。

为了不去修改Singer的代码,我们抽象歌曲

interface ISongWords{
	void GetSongsWords();
}

class QingGe : ISongWords{
	public void GetSongsWords(){
		Debug.log("情歌歌词")
	}
}

class GudianGe : ISongWords{
	public void GetSongsWords(){
		Debug.log("情歌歌词")
	}
}

class Singer{
	public void SongGe(ISongWords song){
		song.GetSongsWords();
	}
}

// 客户端
Singer s = new Singer();
s.SongGe(new QingGe());
s.SongGe(new GudianGe());

可以看到,我们不能让歌手唱什么个,Singer都不会修改代码了。

那么Singer就不会依赖于QingGe了。

享元模式

享元模式分为外部状态、内部状态、工厂

外部状态:共享不变的

内部状态:特有可变的

工厂管理:确定内部状态只创建一次

作用:优化内存

例子1

开放世界游戏中有10,000棵树(假设每棵树的模型数据为50KB+位置数据16B)

❌ 传统方式:每棵树独立存储

一颗树有50KB,那么10,000颗树为500MB

✅ 享元模式:共享一棵树,不共享位置

总内存:50KB + 16B * 10,000 = 210KB

using System;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;


namespace DYFramework.Examples
{

    public class TreeType
    {
        public readonly string Name;
        public readonly string Color;

        public TreeType(string name, string color)
        {
            this.Name = name;
            this.Color = color;
        }

        public void Draw(int x, int y)
        {
            Debug.Log($"在坐标({x},{y})绘制一棵{Color}色的{Name}");
        }
    }

    public class Tree
    {
        private TreeType _type;
        private int _x, _y;

        public Tree(TreeType treeType, int x, int y)
        {
            this._type = treeType;
            this._x = x;
            this._y = y;
        }

        public void Draw()
        {
            _type.Draw(_x, _y);
        }


    }


    public class TreeFactory
    {
        private static Dictionary<string, TreeType> _types = new();

        public static TreeType GetTreeType(string name, string color)
        {
            string key = $"{name}_{color}";
            if (!_types.ContainsKey(key))
            {
                _types.Add(key, new TreeType(name, color));
            }

            return _types[key];
        }
        
    }
    
    public class TreeTest : MonoBehaviour
    {
        
        
        
        private void Start()
        {
            Test2();

        }

        /// <summary>
        /// 测试结果:80KB
        /// </summary>
        private void Test1()
        {
            long baseMemory =  GC.GetTotalMemory(true);
            
            Tree[] forest = new Tree[10000];
            
            for (int i = 0; i < forest.Length; i++)
            {
                bool isOak = MathUtil.Percent(50);
                TreeType type = isOak ? 
                    TreeFactory.GetTreeType("橡树", "绿") : 
                    TreeFactory.GetTreeType("松树", "深绿");
                forest[i] = new Tree(type, Random.Range(0, 1000), Random.Range(0,1000));
            }

            
            GC.Collect();
            long usedMemory = GC.GetTotalMemory(true) - baseMemory;
            Debug.Log($"\n实际内存用量:{usedMemory / 1024} KB");
        }
        
        private void Test2()
        {
            List<TreeType> _holder = new List<TreeType>(); // 新增容器保持对象引用
            long baseMemory = GC.GetTotalMemory(true);
            Tree[] forest = new Tree[10000];
    
            for (int i = 0; i < forest.Length; i++)
            {
                var type = new TreeType($"动态树_{i}", $"颜色_{i}"); // 使用唯一字符串
                _holder.Add(type); // 阻止GC回收
                forest[i] = new Tree(type, Random.Range(0, 1000), Random.Range(0,1000));
            }
    
            GC.Collect();
            long usedMemory = GC.GetTotalMemory(true) - baseMemory;
            Debug.Log($"实际内存用量:{usedMemory / 1024} KB");
        }
    }
}

路由设计模式

例子1

例子1:快递的分拣系统,你告诉系统要寄什么类型的快递,系统会自动把快递分给对应的快递公司处理。(就是把不同请求分法给对应的处理模块)

例子2

例子2:假设有一个NPC守卫,你挥剑,它格挡。你送礼物,它会感谢。你攻击村民,它会追击你。(不同的动作触发不同的行为)

例子3

例子3:技能按键系统

Q键:火球术

E键:治疗术

R键:召唤闪电

using System.Collections.Generic;
using UnityEngine;

public class SkillRouter : MonoBehaviour
{
    // 用字典建立按键与技能的映射关系(路由表)
    private Dictionary<KeyCode, System.Action> skillMap;

    void Start()
    {
        // 初始化路由表
        skillMap = new Dictionary<KeyCode, System.Action> {
            { KeyCode.Q, Fireball },
            { KeyCode.E, Heal },
            { KeyCode.R, Lightning }
        };
    }

    void Update()
    {
        // 遍历所有映射键
        foreach(var kvp in skillMap)
        {
            if(Input.GetKeyDown(kvp.Key))
            {
                // 路由分发:找到对应的技能方法执行
                kvp.Value?.Invoke();
            }
        }
    }

    void Fireball()
    {
        Debug.Log("发射火球!造成火焰伤害");
        // 实际可以在这里添加粒子效果和伤害计算
    }

    void Heal()
    {
        Debug.Log("绿色治疗波!恢复生命值");
        // 添加治疗特效和数值处理
    }

    void Lightning()
    {
        Debug.Log("召唤闪电链!连锁电击敌人");
        // 添加闪电特效和连锁伤害计算
    }
}

桥接模式

定义

将抽象(功能定义)和实现(具体操作)分离,允许两者对变化

抽象:定义高层的控制逻辑

实现:定义底层的具体操作

桥接:通过组合(而非继承)连接抽象与实现

练习题1

  • 问题:实现一个绘图系统,支持不同形状(圆形、方形)和颜色(红、蓝)的任意组合

  • 要求

    • 新增一个三角形形状和金色颜色时,只需添加2个新类

    • 创建组合示例:红色圆形、蓝色三角形

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Examples
{
    public interface IColor
    {
        string GetName();
    }

    public abstract class Shape
    {
        protected IColor _color;
        public Shape(IColor color)
        {
            _color = color;
        }
        
        public abstract void Draw();
    }

    public class Circle : Shape
    {
        public Circle(IColor color) : base(color)
        {
        }

        public override void Draw()
        {
            string color = _color.GetName();
            Debug.Log($"Color:{color}, shape is Circle");
        }
    }
    
    public class Rectangle : Shape
    {
        public Rectangle(IColor color) : base(color)
        {
        }

        public override void Draw()
        {
            string color = _color.GetName();
            Debug.Log($"Color:{color}, shape is Rectangle");
        }
    }
    
    public class Red : IColor
    {
        public string GetName()
        {
            return "Red";
        }
    }
    
    public class Blue : IColor
    {
        public string GetName()
        {
            return "Blue";
        }
    }

    public class Gold : IColor
    {
        public string GetName()
        {
            return "Gold";
        }
    }

    public class Triangle : Shape
    {
        public Triangle(IColor color) : base(color)
        {
        }

        public override void Draw()
        {
            string color = _color.GetName();
            Debug.Log($"Color:{color}, shape is Triangle");
        }
    }
    
    
    public class 桥接模式练习1 : MonoBehaviour
    {
        private void Start()
        {
            IColor red = new Red();
            IColor blue = new Blue();
            Shape circle = new Circle(red);
            Shape triangle = new Triangle(blue);
            circle.Draw();
            triangle.Draw();
            
            // 新增一个维度是否只需添加类,不用修改已有代码?
            // 新增一个金色的方形,并不会修改原有的代码
            IColor gold = new Gold();
            Shape rectangle = new Rectangle(gold);
            rectangle.Draw();
            
        }
    }
}

桥接模式练习2

游戏角色装备系统

  • 问题:设计角色(战士、法师)与武器(剑、法杖)的组合系统

  • 要求

    • 战士持剑攻击显示 "战士用剑劈砍"

    • 法师持法杖攻击显示 "法师用法杖施法"

    • 允许运行时更换武器

using System;
using UnityEngine;

namespace Examples
{
    public interface IWeapon
    {
        void Use();
    }
    
    public abstract class Character
    {
        protected string Name;
        protected IWeapon Weapon;
        
        public Character(string name, IWeapon weapon)
        {
            Weapon = weapon;
            Name = name;
        }

        public abstract void Attack();

        public virtual void SwitchWeapon(IWeapon newWeapon)
        {
            Weapon = newWeapon;
            Debug.Log("更换武器成功!");
        }

    }

    public class Warrior : Character
    {
        public Warrior(string name, IWeapon weapon) : base(name, weapon)
        {
        }

        public override void Attack()
        {
            Debug.Log($"{Name}");
            Weapon.Use();
        }
    }
    
    public class Mage :Character
    {
        public Mage(string name, IWeapon weapon) : base(name, weapon)
        {
        }

        public override void Attack()
        {
            Debug.Log($"{Name}");
            Weapon.Use();
        }
    }
    
    public class Sword : IWeapon
    {
        public void Use() => Debug.Log("用剑劈砍");
    }

    public class Staff : IWeapon
    {
        public void Use() => Debug.Log("用魔杖施法");
    }
    
    
    
    
    public class 桥接模式练习2 : MonoBehaviour
    {
        private void Start()
        {
            IWeapon sword = new Sword();
            Character warrior = new Warrior("战士", sword);
            warrior.Attack();
            
            IWeapon staff = new Staff();
            Character mage = new Mage("法师", staff);
            mage.Attack();

        }
    }
}

桥接模式练习3

  • 问题:实现UI控件(按钮、输入框)支持多主题(暗黑、明亮)

  • 要求

    • 暗黑主题按钮显示 "黑色背景+白色文字"

    • 明亮主题输入框显示 "白色背景+黑色文字"

    • 控件渲染方法返回颜色配置字符串

using System;
using System.Threading;
using UnityEngine;

namespace Examples
{
    public interface ITheme
    {
        string BgColor { get; }
        string TextColor { get; }
    }
    
    public class DarkTheme : ITheme
    {
        public string BgColor => "黑色背景";
        public string TextColor => "白色文字";
    }
    
    public class WhiteTheme : ITheme
    {
        public string BgColor => "白色背景";
        public string TextColor => "黑色文字";
    }

    public abstract class UIView
    {
        protected ITheme theme;
        
        public UIView(ITheme theme)
        {
            this.theme = theme;
        }
        
        public abstract void Render();
    }

    public class Button : UIView
    {
        public Button(ITheme theme) : base(theme)
        {
        }

        public override void Render()
        {
            Debug.Log($"{theme.BgColor}+{theme.TextColor}的按钮");
        }
    }

    public class InputBox : UIView
    {
        public InputBox(ITheme theme) : base(theme)
        {
        }

        public override void Render()
        {
            Debug.Log($"{theme.BgColor}+{theme.TextColor}的文本输入框");
        }
    }
    
    
    public class 桥接模式练习3 : MonoBehaviour
    {
        private void Start()
        {
            ITheme dark = new DarkTheme();
            UIView button = new Button(dark);
            button.Render();
            
            ITheme white = new WhiteTheme();
            UIView inputBox = new InputBox(white);
            inputBox.Render();
        }
    }
}

工厂模式

工厂模式的核心目的是将对象的创建和使用进行解耦,客户端只负责对象的使用,不在乎对象是怎么创建的。

案例1:武器生成

需求:有一个工厂,可以生成不同的武器,客户端只需要将武器的名字给工厂,工厂就会生成对应的武器。

分析:我们可以提取不同武器的共有行为,比如攻击行为,然后将其定义为接口。然后创建的对象由这个接口来接收。

using UnityEngine;

namespace DYFramework.Examples
{
    public interface IWeapon
    {
        void Attack();
    }

    public class Sword : IWeapon
    {
        public void Attack()
        {
            Debug.Log("剑攻击");
        }
    }

    public class Staff : IWeapon
    {
        public void Attack()
        {
            Debug.Log("法杖攻击");
        }
    }
    
    public class WeaponFactory{
        public IWeapon CreateWeapon(string weaponType)
        {
            switch (weaponType)
            {
                case "Sword":
                    return new Sword();
                case "Staff":
                    return new Staff();
                default:
                    Debug.Log("没有这个武器");
                    break;
            }
            return null;
        }
    }
    
    public class 工厂模式练习题 : MonoBehaviour
    {
        private void Start()
        {
            WeaponFactory weaponFactory = new WeaponFactory();
            IWeapon sword = weaponFactory.CreateWeapon("Sword");
            sword.Attack();
            
            IWeapon staff = weaponFactory.CreateWeapon("Staff");
            staff.Attack();
        }
    }
}

案例2:对象池集成

需求:有一个子弹工厂,管理子弹的创建,有两个方法,一个是获得子弹,一个是回收子弹。