答:架构是对依赖的统一管理。
依赖就是持有对象,或者说是持有一个非空的引用。
正如项目开发中,对象和对象之间都会有相互持有、相互调用的需求的。而对象间的持有就是一种依赖。A想要完成一个逻辑处理,需要调用B的一个方法来实现,那么我们就可以说A对B产生了依赖。
好,现在A持有了B的引用,可以直接调用B的方法,这就构成了单向依赖。
单项依赖是最基本、最常见的一种依赖关系,初次接触编程项目的小同志也比较容易理解和使用单向依赖。但是产生单项依赖也要根据A和B具体的身份层次而确定,正如老子打小子好像天经地义,儿子打老子自然算作“忤逆”。
正常健康的单向依赖关系是父对象持有子对象。
这也比较容易理解,如果子对象持有父对象关系,那么随着业务量的增加,父对象的方法逻辑内容会越来越多,子对象若想完成某个处理逻辑直接调用父对象的方法来实现,自然会造成头重脚轻的不健康的状态。
对于对象间单项依赖合不合理的判断要看产生依赖关系的两个对象间的层级关系。
如果是父子关系,那么原则是父对象持有子对象的依赖较为合适;
如果是兄弟关系,尽量通过共同持有的父对象进行交流。兄弟间相互持有依赖,也会增加维护成本,尽量通过共同持有一个对象方式来进行“交流”。
也就是说最好的依赖关系就是上级依赖下级,同级别对象之间依赖不推荐,子对象依赖父对象应当避免。
当然这属于是一种编程设计思想,不仅仅局限于对象和对象之间的依赖关系的实现中,模块与模块中间、层级和层级之间都应遵循上级持有下级的设计准则。
游戏项目也应是遵循表层(高层)对底层依赖。
上文内容已经展示出对象之间交互的三种方式:
父对象持有子对象,直接可以调用子对象中的方法,完成交互。
首先表明:不建议出现双向依赖。
因为相互持有的对象关系间的耦合度过高,不符合我们一贯追求的“低耦合,高内聚”准则,为此我们坚决杜绝双向依赖关系,不管是父子、兄弟层级之间的,任何形式的双向依赖关系都不应现。
既然把话说的那么绝对,那么需要相互通信怎么办?
这就提到上文中的另外两种交互方式--委托和事件。
试想一下,典型需要相互沟通的一个例子。
按钮点击,按钮控制脚本A需要检测用户的点击、用户点击之后要告知具体执行的脚本B。这是A对B有交流需要,假如B的具体操作是播放一个动画,在动画播放期间按钮要失活,防止用户再次点击,等B的动画播放完毕了需要B告知A,让A恢复按钮状态并继续检测用户的点击,这是B对A产生的依赖需求。
如果相互持有对方对象,直接调用对方的相互方法即可,但是要解耦,自然需要用到委托和事件。
上文中阐述出单向依赖要符合长辈持有儿孙的原则,那么儿孙对长辈有通信需要但不能持有长辈依赖怎么办?
这里使用委托来完成下层对上层的通信需求。
namespace DependencyDemo
{
public class A : MonoBehaviour
{
B b;
void Start()
{
b = transform.Find("Animation").GetComponent<B>();
// 注册完成的事件
b.OnDoSomethingDone += ()=>{
Debug.Log("动画播放完毕");
};
}
}
public class B : MonoBehaviour
{
// 定义委托
public Action OnDoSomethingDone = ()=>{};
//当动画播放完毕后调用
public void DoSomething()
{
//触发委托中的函数执行
OnDoSomethingDone();
}
}
}
在B中声明一个委托,待动画播放完毕之后触发调用委托中的函数。而持有B的A则可以拿到B中的委托,将动画播放完毕要做的操作函数添加到此委托中,这样实现了下级B对上级A的通信,而B并没有持有A。
那么使用委托有什么缺点呢?
委托中注册函数和注销函数要成对出现!
委托中注册函数要保证最后注销掉,也就是说注册和注销要成对出现,以免出现空引用的问题。使用委托应当像使用没有GC功能的编程语言那样,申请内存使用完毕之后要保证释放,养成成对的好习惯。
其实不论是委托还是内存使用,在编程中常常要注意使用的模块、对象的生命周期,例如调用某个函数时发现对象没有实例化,加载资源时候发现资源管理类还未初始化,诸如此类问题,,自然要我们清除的知晓调用的对象的生命周期。
委托另一点就是会增加代码量,可以想象委托算作一个回调函数容器,一个函数执行时候要有个对应的回调,那么随着业务量增加,函数增多,对应的委托也相应的增多,而且委托需要相应的声明,不免也增加了对委托管理的工作量。
至此,本节展示了使用委托来解耦下级对上级依赖。但是同级别之间的单项依赖则不建议使用委托。
自此我们知晓这样一个原则或者观念:底部向高层通信可以使用委托。