散发着坏味道的单例

发布时间 2024-01-03 23:01:36作者: 阿客Ake

想起自己在刚刚入行时,接触到单例这种设计模式,大为欣喜,居然有这样一种方式可以让某些类不受作用域的限制,也不需要考虑层层传参,就可以在程序的任何角落直接使用。这样的便利性让我对单例的使用欲罢不能,但随着经验的积累,我渐渐开始闻到了单例的坏味道。在最近的一次阅读同事代码时,这种坏味道十分明显,也促使自己想认真地聊聊这个话题。

同事当时负责的业务是一个类似于大富翁的走棋盘小游戏,但玩法相当简略,没有多少内容。在这块业务中,他采用了一个叫QFramework的Unity框架,这个框架之前我有所耳闻,但从未见过相关的工业实践。同事向我介绍它的特点,其中之一就是可以在一个系统当中任意的建立子系统,然而这些子系统之间,可以通过相关接口直接访问对方。直接访问,实际上就是单例的特点,即全局访问。需要强调一下,我想讨论的不仅仅是单例这种设计模式,而是所有具有全局访问特效的组织形式,像C#语言中的静态类和刚刚提到的框架都是如此。

来看看实际例子,在这个小游戏中,棋子,棋盘都是被抽象为对象,这一点没什么好说的。其中也肯定会有把棋子移动棋盘上的某个点这样的功能,而同事大概是这样实现的:

class ChessPiece
{
	void Move()
	{
		this.position = GetSystem<Grids>().GetGrid(i).position;
	}
}

class Grids { ... }

一个明显而直观的问题出现了,即ChessPiece和Grids产生了耦合。当然此刻你可能会有疑问,这个耦合和单例又有什么关系呢?好,我们先把问题放在这。下面的版本是我在接手他的工作之后,进行的重构,内容如下:

class ChessPiece
{
	Vector3 position;

	public void Move(Vector3 pos)
	{
		this.position = pos;
	}
}

class Grids { ... }

class MiniGameManager
{
    ChessPiece _chessPiece;
    Grids _grids;

    void Main()
    {
        _chessPiece.Move(_grids.GetGrid(i).position);
    }
}

我自己的设计是用一个Manager的对象来管理整个流程,而游戏内各个元素之间的交互,就全部由Manager来处理。这样各个元素之间,就不存在相互调用,他们只需要管理好自己的事情,并暴露相应的接口给外部做处理。而这种做法,我很快就获得了额外的收益,因为策划需要编辑棋盘上的内容,Grids这个类我就原封不动地移了棋盘编辑器的业务那块去了。由于Grids写得很干净,里面没有保留任何和外界相关的引用,所有可以被当做很纯粹的模块使用,也可以进行单元测试。

好,我们现在再说回来单例和耦合之间的关系。正如开头就说到,单例的优势在于它的便利性,可以全局无障碍地访问,但这个无障碍也十分容易变成无所顾忌。有些开发者习惯了在系统当中随性地,不假思索地,大量地使用单例,那这毫无疑问是一种十分偷懒的行为,因为开发者渐渐地不去考虑封装,不去考虑层级的划分,对代码结构的组织变得松懈,甚至放弃了思考。这样的系统会高度的耦合,就如同放置在抽屉里的无数根数据线,相互缠绕。

尽管如此,这并不意味着我们就要放弃使用单例,它本身具有合适的应用场景,只是我们应当变得更加谨慎。当我们在引入一个单例时,可以多思考一下它的必要性,考虑是否可以在外部传入相关的参数即可。如果出现两个单例在一个方法中双向引用这种更糟糕的情况,那或许得思考这个方法本身是不是就设计的不合理。总之,请不要再追求使用单例时所带来的快感,因为那很有可能使代码走向腐败。