一談到 『IoC』 ,有經驗的程式員馬上會聯想到控制反轉,將創建對象的責任反轉給工廠。IoC是依賴註入 『DI』 的核心,大名鼎鼎的Spring框架就是一個非常卓越的的控制反轉、依賴註入框架。遺憾的是,我們顯然不能在Unity 3D中去使用Spring框架,但思想是相通的——IoC也好,控制反轉也罷 ...
一談到 『IoC』,有經驗的程式員馬上會聯想到控制反轉,將創建對象的責任反轉給工廠。IoC是依賴註入 『DI』 的核心,大名鼎鼎的Spring框架就是一個非常卓越的的控制反轉、依賴註入框架。遺憾的是,我們顯然不能在Unity 3D中去使用Spring框架,但思想是相通的——IoC也好,控制反轉也罷,本質上是一個工廠,或者又被稱為容器,我們可以自己維護一個工廠來實現對對象的管理,這也是本文的核心內容。
工廠模式初探
工廠,顧名思義,就是生產對象的地方。如果之前沒有接觸過設計模式,你可能會疑惑,我直接使用 『new』 關鍵字難道不能創建對象嗎?為什麼還要大費周章的讓工廠來創建?當然這是沒錯的,直接使用 『new』 關鍵字很簡潔,也很易懂,但你考慮過對象的釋放嗎?你可能會說不用考慮啊,GC會幫我們回收啊。
其實問題就出在這裡,因為你沒有考慮對象管理的動機,所以就不會有工廠這個概念。試想一下,使用ADO.NET或者JDBC去訪問資料庫,我們是不是要先建立一個Connection,當工作結束後,Close了這個連接。當再一次需要連接資料庫時,再建立一次Connection,這背後其實有隱患。因為和資料庫建立連接是非常耗時的,只是我們感受不到。我們能不能在關閉連接時,不銷毀對象,而是將其放到一個對象池,當下一次請求來時,直接從對象池中獲取。這就是工廠的動機,對對象的創建和釋放進行管理,這樣可以有效的提高效率。
註:釋放指的是對象實現了IDisposable介面的非托管資源,在uMVVM框架,工廠維護的都是托管資源,銷毀由GC決定
工廠的分類
在uMVVM框架中,我將工廠分為三類:單例(Singleton),臨時(Transient),池(Pool)。
- Singleton :該工廠生產的對象是單例的,即一旦生產出來的對象將處理所有的請求,不會因為不同的請求而產生新的對象,通常需要考慮多線程併發問題
- Transient :該工廠生產的對象是臨時的,轉瞬即逝的,即每一次請求產生一個新對象,處理請求完畢後就被銷毀
- Pool:該工廠並不會無限的創建對象,取而代之的是內部維護了一個對象池,當請求來時,從對象池中獲取,當請求處理完畢後,對象也不會被銷毀,而是再次放回對象池中
我們可以為這三種工廠聲明公共的介面:IObjectFactory,這是非常有必要的,方便在運行時根據需求動態的切換不同工廠:
public interface IObjectFactory
{
object AcquireObject(string className);
object AcquireObject(Type type);
object AcquireObject<TInstance>() where TInstance : class, new();
void ReleaseObject(object obj);
}
這個介面功能很簡單,通過統一的入口對對象進行創建與銷毀的管理。
Singleton Factory
有了統一的工廠的介面之後,接下來就是去實現對應的工廠了,第一個要實現的就是 Singleton Factory:
public class SingletonObjectFactory:IObjectFactory
{
/// <summary>
/// 共用的字典,不會因為不同的SingletonObjectFactory對象返回不唯一的實例對象
/// </summary>
private static Dictionary<Type,object> _cachedObjects = null;
private static readonly object _lock=new object();
private Dictionary<Type, object> CachedObjects
{
get
{
lock (_lock)
{
if (_cachedObjects==null)
{
_cachedObjects=new Dictionary<Type, object>();
}
return _cachedObjects;
}
}
}
//...省略部分代碼...
public object AcquireObject<TInstance>() where TInstance:class,new()
{
var type = typeof(TInstance);
if (CachedObjects.ContainsKey(type))
{
return CachedObjects[type];
}
lock (_lock)
{
var instance=new TInstance();
CachedObjects.Add(type, instance);
return CachedObjects[type];
}
}
}
上述代碼中,我們需要定義一個全局的字典,用來存儲所有的單例,值得註意的是,CachedObjects 字典是一個 static 類型,這表明這是一個共用的字典,不會因為不同的SingletonObjectFactory對象返回不唯一的實例對象。
還有一點,單例模式最好考慮一下多線程併發問題,雖然這是一個 『偽』 需求,畢竟Unity 3D是個單線程應用程式,但 uMVVM 框架還是考慮了多線程併發的問題,使用 lock 關鍵字,它必須是一個 static 類型,保證 lock 了同一個對象。
Transient Factory
Transient Factory 是最容易實現的工廠,不用考慮多線程併發問題,也不用考慮Pool,對每一次請求返回一個不同的對象:
public class TransientObjectFactory : IObjectFactory
{
//...省略部分代碼...
public object AcquireObject<TInstance>() where TInstance : class, new()
{
var instance = new TInstance();
return instance;
}
}
Pool Factory
Pool Factory 相對來說是比較複雜的工廠,它對 Transient Factory 進行了升級——創建實例前先去Pool中看看是否有未被使用的對象,有的話,那麼直接取出返回,如果沒有則向Pool中添加一個。
Pool的實現有兩種形式,一種是內置了諸多對象,還有一種是初始時是一個空的池,然後再往裡面添加對象。第一種效率更高,直接從池裡面拿,而第二種更省記憶體空間,類似於懶載入,uMVVM 的對象池技術使用第二種模式。
public class PoolObjectFactory : IObjectFactory
{
/// <summary>
/// 封裝的PoolData
/// </summary>
private class PoolData
{
public bool InUse { get; set; }
public object Obj { get; set; }
}
private readonly List<PoolData> _pool;
private readonly int _max;
/// <summary>
/// 如果超過了容器大小,是否限制
/// </summary>
private readonly bool _limit;
public PoolObjectFactory(int max, bool limit)
{
_max = max;
_limit = limit;
_pool = new List<PoolData>();
}
private PoolData GetPoolData(object obj)
{
lock (_pool)
{
for (var i = 0; i < _pool.Count; i++)
{
var p = _pool[i];
if (p.Obj == obj)
{
return p;
}
}
}
return null;
}
/// <summary>
/// 獲取對象池中的真正對象
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private object GetObject(Type type)
{
lock (_pool)
{
if (_pool.Count > 0)
{
if (_pool[0].Obj.GetType() != type)
{
throw new Exception(string.Format("the Pool Factory only for Type :{0}", _pool[0].Obj.GetType().Name));
}
}
for (var i = 0; i < _pool.Count; i++)
{
var p = _pool[i];
if (!p.InUse)
{
p.InUse = true;
return p.Obj;
}
}
if (_pool.Count >= _max && _limit)
{
throw new Exception("max limit is arrived.");
}
object obj = Activator.CreateInstance(type, false);
var p1 = new PoolData
{
InUse = true,
Obj = obj
};
_pool.Add(p1);
return obj;
}
}
private void PutObject(object obj)
{
var p = GetPoolData(obj);
if (p != null)
{
p.InUse = false;
}
}
public object AcquireObject(Type type)
{
return GetObject(type);
}
public void ReleaseObject(object obj)
{
if (_pool.Count > _max)
{
if (obj is IDisposable)
{
((IDisposable)obj).Dispose();
}
var p = GetPoolData(obj);
lock (_pool)
{
_pool.Remove(p);
}
return;
}
PutObject(obj);
}
}
上述的代碼通過構造函數的 max 決定Pool的大小,limit 參數表示超過Pool容量時,是否可以再繼續往Pool中添加數據。方法 GetObject 是最核心的方法,邏輯非常簡單,獲取對象之前先判斷Pool中是否有未被使用的對象,如果有,則返回,如果沒有,則根據 limit 參數再決定是否可以往Pool中添加數據。
小結
工廠模式是最常見的設計模式,根據工廠的類型可以獲取不同形式的數據對象,比如單例數據、臨時數據、亦或是對象池數據。這一章的工廠模式很重要,也是對下一篇對象的註入『Inject』做準備,故稱之為理念先行。
源代碼托管在Github上,點擊此瞭解