定义
DIP原则定义中核心的两句描述为如下两条:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口(抽象)。
- 抽象接口(抽象)不应该依赖于具体实现。而具体实现则应该依赖于抽象接口(抽象)。
注:依赖倒置发生在层次结构的系统中。
目的
其他原则或者设计模式的解耦手段有很多,DIP的解耦手段就是确定不变,以不变应对需求的多变性,只有不变才是稳定的。
什么是不变?不变就是抽象。
方法
- 代码中多用抽象接口,尽最大可能避免使用具体实现类,且实现类中要一直确定组合优于继承的方式。
- virtual在设计时就应该是一个没有任何逻辑的空函数。
- 在抽象设计时,应用
Open
这类名称代替如OpenInventory
含有具体实现细节的名称。
依赖倒置原则在 Unity 开发中的体现
1. 传统的依赖关系(未倒置)
在没有依赖倒置的情况下,高层模块(如 GameManager)会直接依赖具体的实现类(如 Player)。例如:
public class GameManager : MonoBehaviour
{
private Player player;
void Start()
{
player = new Player(); // 直接依赖具体类,耦合度高
player.Move();
}
}
2. 依赖倒置后的设计
为了符合 DIP,我们可以引入一个抽象接口 ICharacter,让 GameManager 依赖于 抽象接口,而不是 Player 的具体实现:
public interface ICharacter
{
void Move();
}
public class Player : ICharacter
{
public void Move()
{
Debug.Log("Player is moving");
}
}
public class Enemy : ICharacter
{
public void Move()
{
Debug.Log("Enemy is moving");
}
}
// GameManager 依赖于 ICharacter,而不是具体的 Player 或 Enemy
public class GameManager : MonoBehaviour
{
private ICharacter character;
void Start()
{
character = new Player(); // 依赖的是抽象接口
character.Move();
}
}
3. 通过依赖注入进一步优化
我们可以通过 依赖注入(Dependency Injection, DI) 进一步降低 GameManager 对具体 Player 的创建依赖
public class GameManager : MonoBehaviour
{
private ICharacter character;
// 通过构造函数注入依赖
public GameManager(ICharacter character)
{
this.character = character;
}
void Start()
{
character.Move();
}
}
在 Unity 中,我们可以通过 工厂模式 或 服务定位器 来进行依赖注入,避免 GameManager 直接 new 一个 Player,提高代码的可扩展性。
DIP 具体“倒置”了哪些?
1. 高层模块(GameManager)不再依赖低层模块(Player/Enemy),而是依赖抽象(ICharacter)
→ 倒置了“高层依赖底层”的传统关系,变成 “低层模块依赖高层的抽象”。
2. 面向接口编程,解耦高层与低层
→ 以后如果 Enemy 也实现 ICharacter,GameManager 无需修改,只需注入不同实现,提高了可扩展性。
3. 支持依赖注入,减少硬编码
→ 通过 DI(如 Unity 的 Zenject 框架),可以让依赖关系更加清晰,减少 new 关键字导致的紧耦合。
为什么说依赖倒置
- 在依赖倒置原则(DIP) 中,"低层模块依赖高层的抽象" 可能会让人产生疑问,毕竟代码上看 "低层模块(如 Player)并没有直接依赖高层模块(如 GameManager)",为什么说是“低层依赖高层”呢?让我们深入理解。
1. 传统设计(未倒置的依赖)
GameManager → Player
(高层) (低层)
GameManager 直接依赖 Player 这个具体的类。如果以后 Player 发生变化,比如增加 Enemy,就需要修改 GameManager,导致 高层模块依赖具体实现(低层模块),代码耦合度高,扩展性差。
2. 依赖倒置后的设计
在 应用 DIP 之后,依赖关系变成了:
GameManager → ICharacter ← Player
(高层) (抽象) (低层)
// 抽象接口(高层定义)
public interface ICharacter
{
void Move();
}
// 低层模块实现抽象(依赖高层定义的 ICharacter)
public class Player : ICharacter
{
public void Move()
{
Debug.Log("Player is moving");
}
}
// 低层模块实现抽象
public class Enemy : ICharacter
{
public void Move()
{
Debug.Log("Enemy is moving");
}
}
// 高层模块依赖抽象,而不是具体实现
public class GameManager : MonoBehaviour
{
private ICharacter character;
public GameManager(ICharacter character)
{
this.character = character;
}
void Start()
{
character.Move();
}
}
3. 为什么说“低层模块依赖高层的抽象”?
DIP 的核心是 “高层模块不应该依赖低层模块,而应该依赖抽象”,“低层模块也要依赖同样的抽象”。
但这里有个容易混淆的点:“ICharacter 这个接口是谁定义的?”
ICharacter 是由高层模块(GameManager 这一层)定义的,而 Player 作为低层模块,它依赖了这个抽象(ICharacter)。
所以,尽管代码上 Player 并没有直接依赖 GameManager,但它的确 依赖了高层定义的抽象(ICharacter),这就是所谓的“低层依赖高层的抽象”。
如果 ICharacter 由 GameManager 层或更高的 业务逻辑层 定义,此时 Player 反而依赖了 ICharacter 这个高层定义的抽象,从而达成 DIP,也就达成了依赖的倒置。
总结
传统模式:
高层模块(GameManager)依赖低层模块(Player)。
代码耦合度高,难以扩展。
依赖倒置后:
高层模块(GameManager)依赖抽象(ICharacter),不依赖 Player。
低层模块(Player)实现这个抽象(ICharacter),从而依赖高层定义的接口。
依赖关系被“倒置”了,低层依赖高层的抽象,而不是高层依赖低层的具体实现。
DIP 不是指低层代码直接引用高层代码,而是指低层模块应该依赖于高层定义的抽象接口,而不是自己定义的具体实现!