编程思想
内聚
内聚:模块内部具有相同特点的相似程度
小到一个方法,一个接口、一个类,大到一个业务、一个功能、一个系统,都叫模块。
比如新闻管理类只负责新闻相关的功能(与新闻相关):新闻查看、新闻删除、新闻更新。而用户类只负责用户相关的功能(与用户相关):用户登陆、用户注册。
高内聚就是模块内部相同特点的相似程度很高,不会混入不相关的模块,比如新闻管理模块不会混入用户模块相关的内容。
耦合
耦合:代码之间的依赖程度,如果两个类或模块之间的依赖关系太强(高耦合),修改其中一个可能会影响另一个,导致代码难以维护和扩展。相反,如果依赖关系松散(低耦合),代码会更容易修改和复用。
例子
想象你在玩积木:
高耦合:两块积木粘在一起,拆开一个可能会破坏另一个。
低耦合:积木之间只是简单拼接,拆开一个不会影响其他积木。
在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
,如果以后需要攻击其他对象(如 Boss
或 Crate
),必须修改 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
接口。
可以轻松扩展,比如新增 Boss
或 Crate
,只要它们实现 IDamageable
,Player
就能攻击它们。
代码更灵活、更易维护
为什么依赖抽象会降低耦合?
依赖抽象的意思是,代码依赖于接口或抽象类,而不是具体的实现类。比如:
依赖抽象:
Player
依赖于IDamageable
接口。依赖具体:
Player
依赖于Enemy
类。
(1)减少对具体实现的依赖
依赖具体:如果
Player
直接依赖Enemy
,那么Player
的代码就和Enemy
的实现绑定了。如果Enemy
的实现变了(比如方法名改了),Player
的代码也得跟着改。依赖抽象:如果
Player
依赖IDamageable
接口,那么只要Enemy
实现了IDamageable
,Player
就不需要关心Enemy
的具体实现。即使Enemy
的实现变了,只要接口不变,Player
的代码就不需要改。
(2)提高代码的灵活性
依赖具体:如果
Player
只能攻击Enemy
,那么以后想攻击其他对象(比如Boss
或Crate
),就必须修改Player
的代码。依赖抽象:如果
Player
依赖IDamageable
,那么任何实现了IDamageable
的对象(如Enemy
、Boss
、Crate
)都可以被攻击,不需要修改Player
的代码。
(3)更容易测试
依赖具体:如果
Player
直接依赖Enemy
,那么在测试Player
时,必须创建一个真实的Enemy
对象,这可能会导致测试复杂(比如Enemy
依赖其他资源)。依赖抽象:如果
Player
依赖IDamageable
,那么在测试时可以用一个模拟对象(Mock Object)来代替Enemy
,这样测试更简单、更独立。
(4)符合开闭原则
开闭原则:软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
依赖抽象:通过依赖抽象,我们可以在不修改现有代码的情况下扩展功能。比如新增一个
Boss
类,只要它实现了IDamageable
,Player
就能直接攻击它,而不需要修改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
类的职责拆分为两个类:
PlayerHealth
:负责管理玩家的健康值。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中如何实践单一职责原则?
拆分职责:如果一个类做了太多事情,就把它拆分成多个小类。
组件化设计:Unity 的
GameObject
可以通过添加多个组件来实现功能。比如:Player
对象可以挂载PlayerHealth
和PlayerAttack
两个脚本。每个脚本只负责一个功能。
高内聚,低耦合:让每个类专注于自己的职责,减少类之间的依赖。
开闭原则(重要)
开闭:对扩展开放,对修改关闭。换句话说,当需要添加新功能时,应该通过扩展(添加新代码)来实现,而不是修改现有的代码。
核心目的:使用抽象,封装变化
结合例子
想象你在搭积木:
如果你每次想添加新功能(比如加一个窗户),都需要拆掉现有的积木(修改现有代码),那么整个结构会变得不稳定。
如果你设计了一个可以扩展的积木系统(比如预留插槽),那么你可以直接添加新积木(扩展新代码),而不需要拆掉现有的积木。
开闭原则就像这个可扩展的积木系统,它让代码更容易扩展,同时保持稳定。
违反开闭原则 vs 遵循开闭原则
假设我们有一个游戏,里面有不同类型的敌人(如 Enemy
和 Boss
)。我们需要计算每个敌人的伤害。如果直接在代码中写死每种敌人的伤害计算逻辑,就会违反开闭原则。
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
方法。现有代码保持稳定,新增功能不会影响原有逻辑
如何在游戏开发中,有意识的使用开闭原则?
将可变的行为抽象出来,让具体实现通过扩展来实现,而不是修改现有代码。
假设游戏中有多种敌人(如 NormalEnemy
、BossEnemy
),每种敌人的行为不同。
// 定义抽象类或接口
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:对象池集成
需求:有一个子弹工厂,管理子弹的创建,有两个方法,一个是获得子弹,一个是回收子弹。
评论区