[Architecture] Singleton Pool

摘要:[Architecture] Singleton Pool

動機 :

在開發與資料庫溝通的系統時,因為建立資料庫連線是比較昂貴的。
所以ADO.NET在背後幫開發人員,實做了 ConnectionPool的機制。
將系統內建立的資料庫連線做快取,當系統要使用時就直接使用快取連線,避免了每次都建立新資料庫連線的花費。
並且實際上在使用ADO.NET時,開發人員對於背後的ConnectionPool機制其實是無感的。
要讓開發人員無感,可是又能完成快取的功能,這真的要花一點工夫去設計。

 

本文介紹一個『Singleton Pool模式』。
定義物件之間的職責跟互動,用來建置類似ConnectionPool功能的物件池功能,並且提供開發人員無感的使用界面。
為自己做個紀錄,也希望能幫助到有需要的開發人員。

 

 

結構 :

 

『Singleton Pool模式』是Flyweight Pattern的延伸,有興趣的開發人員可以找相關文章做參考。
模式的結構如下:

 

主要的參與者有:
NativeConnection
-實際提供功能的物件。

 

 

ReferenceRecord
-快取NativeConnection,並且紀錄有多少客戶端正在使用中。

 

 

ConnectionPool
-建立ReferenceRecord用來快取物件及記錄使用者。
-當有人要使用NativeConnection,可是系統內沒有快取的時候,建構快取。
-當有人要使用NativeConnection,可是系統內已有快取的時候,回傳快取。
-當沒有人使用NativeConnection,可是系統內已有快取的時候,解構快取。

 

 

Connection
-合成NativeConnection功能提供外部使用。
-將NativeConnection的建構、解構,交由ConnectionPool去處理。

 

 

透過下面的圖片說明,可以了解相關物件之間的互動流程。

 

 

 

 

 

 

 

 

實做 :

 

範列下載 :

範例的程式碼較多,實做說明請參照範例程式內容。
SingletonPoolSample點此下載

 

範列實做 :

 

範例內容實做一個模擬的ConnectionPool,它會快取實際連線到資料庫的物件,並且會將內部執行訊息列印到Console上。
透過這個範例,可以清楚的了解如何實做以及執行效果。

 

首先在專案裡實做一個虛擬的NativeConnection,用來模擬實際連線到資料庫的功能以及將執行訊息列印到Console上。

 


public class NativeConnection : IDisposable
{
    // Fields
    private readonly string _connectionString = null;


    // Constructor
    public NativeConnection(string connectionString)
    {
        #region Require

        if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentNullException();

        #endregion
        _connectionString = connectionString;
        Console.WriteLine(string.Format("Connect to database.[{0}]", _connectionString));
    }

    public void Dispose()
    {
        Console.WriteLine(string.Format("Disconnection to database.[{0}]", _connectionString));
    }


    // Methods  
    public void Query()
    {
        Console.WriteLine(string.Format("Query.[{0}]", _connectionString));
    }
}

 

再來建立ReferenceRecord,用來快取NativeConnection,並且紀錄誰使用了NativeConnection。

 


internal sealed class ReferenceRecord<TReferenceKey, TReferenceItem>
{
    // Fields
    private readonly List<Guid> _consumerIdList = new List<Guid>();


    // Constructor
    public ReferenceRecord(TReferenceKey referenceKey, TReferenceItem referenceItem)
    {
        #region Require

        if (referenceKey == null) throw new ArgumentNullException();
        if (referenceItem == null) throw new ArgumentNullException();

        #endregion
        this.ReferenceKey = referenceKey;
        this.ReferenceItem = referenceItem;
    }


    // Properties
    public TReferenceKey ReferenceKey { get; private set; }

    public TReferenceItem ReferenceItem { get; private set; }


    // Methods   
    public void Register(Guid consumerId)
    {
        #region Require

        if (consumerId == Guid.Empty) throw new ArgumentNullException();

        #endregion
        if (_consumerIdList.Contains(consumerId) == false)
        {
            _consumerIdList.Add(consumerId);
        }
    }

    public void Unregister(Guid consumerId)
    {
        #region Require

        if (consumerId == Guid.Empty) throw new ArgumentNullException();

        #endregion
        if (_consumerIdList.Contains(consumerId) == true)
        {
            _consumerIdList.Remove(consumerId);
        }
    }

    public bool NoConsumerRegistered()
    {
        if (_consumerIdList.Count <= 0)
        {
            return true;
        }
        return false;
    }
}

 

接著實做ConnectionPool,用來將整個『Singleton Pool模式』的流程做封裝。

 


public abstract class ReferencePool<TReferenceKey, TReferenceItem>
{
    // Fields
    private readonly List<ReferenceRecord<TReferenceKey, TReferenceItem>> _referenceRecordCollection = new List<ReferenceRecord<TReferenceKey, TReferenceItem>>();


    // Methods   
    public virtual TReferenceItem Create(Guid consumerId, TReferenceKey referenceKey)
    {
        #region Require

        if (consumerId == Guid.Empty) throw new ArgumentNullException();
        if (referenceKey == null) throw new ArgumentNullException();

        #endregion     
      
        // Return Existing ReferenceItem
        foreach (ReferenceRecord<TReferenceKey, TReferenceItem> referenceRecord in _referenceRecordCollection)
        {
            if (this.CompareReferenceKey(referenceKey, referenceRecord.ReferenceKey) == true)
            {
                referenceRecord.Register(consumerId);
                return referenceRecord.ReferenceItem;
            }                
        }

        // Return New  ReferenceItem
        TReferenceItem referenceItem = this.CreateReferenceItem(referenceKey);
        if (referenceItem == null) throw new InvalidOperationException("CreateReferenceItem failed.");

        ReferenceRecord<TReferenceKey, TReferenceItem> newReferenceRecord = new ReferenceRecord<TReferenceKey, TReferenceItem>(referenceKey, referenceItem);            
        _referenceRecordCollection.Add(newReferenceRecord);
        newReferenceRecord.Register(consumerId);

        return referenceItem;
    }

    public virtual void Release(Guid consumerId, TReferenceKey referenceKey)
    {
        #region Require

        if (consumerId == Guid.Empty) throw new ArgumentNullException();
        if (referenceKey == null) throw new ArgumentNullException();

        #endregion

        // Release Existing ReferenceItem
        ReferenceRecord<TReferenceKey, TReferenceItem> existingReferenceRecord = null;
        foreach (ReferenceRecord<TReferenceKey, TReferenceItem> referenceRecord in _referenceRecordCollection)
        {
            if (this.CompareReferenceKey(referenceKey, referenceRecord.ReferenceKey) == true)
            {
                existingReferenceRecord = referenceRecord;
                break;
            }
        }

        if (existingReferenceRecord != null)
        {
            existingReferenceRecord.Unregister(consumerId);
            if (existingReferenceRecord.NoConsumerRegistered() == true)
            {
                _referenceRecordCollection.Remove(existingReferenceRecord);
                this.ReleaseReferenceItem(existingReferenceRecord.ReferenceItem);                    
            }
        }
    }


    protected abstract TReferenceItem CreateReferenceItem(TReferenceKey referenceKey);

    protected abstract void ReleaseReferenceItem(TReferenceItem referenceItem);

    protected abstract bool CompareReferenceKey(TReferenceKey referenceKeyA, TReferenceKey referenceKeyB);
}

public class ConnectionPool : ReferencePool<string, NativeConnection>
{
    // Methods  
    protected override NativeConnection CreateReferenceItem(string connectionString)
    {
        #region Require

        if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentNullException();

        #endregion
        return new NativeConnection(connectionString);
    }

    protected override void ReleaseReferenceItem(NativeConnection connection)
    {
        #region Require

        if (connection == null) throw new ArgumentNullException();

        #endregion     
        connection.Dispose();
    }

    protected override bool CompareReferenceKey(string connectionStringA, string connectionStringB)
    {
        #region Require

        if (string.IsNullOrEmpty(connectionStringA) == true) throw new ArgumentNullException();
        if (string.IsNullOrEmpty(connectionStringB) == true) throw new ArgumentNullException();

        #endregion
        return connectionStringA == connectionStringB;
    }
}

 

剩下就是Connection了,這個物件合成NativeConnection物件提供外部使用。並且在建構、解構的函式內,調用Singleton的ConnectionPool,來完成『Singleton Pool模式』的功能。

 


public class Connection : IDisposable
{
    // Singleton
    private static ConnectionPool _poolInstance = null;

    private static ConnectionPool PoolInstance
    {
        get
        {
            if (_poolInstance == null)
            {
                _poolInstance = new ConnectionPool();
            }
            return _poolInstance;
        }
    }


    // Fields
    private readonly Guid _consumerId = Guid.Empty;

    private readonly string _connectionString = null;

    private readonly ConnectionPool _connectionPool = null;

    private readonly NativeConnection _nativeConnection = null;        


    // Constructor
    public Connection(string connectionString) : this(connectionString, Connection.PoolInstance) { }

    public Connection(string connectionString, ConnectionPool connectionPool)
    {
        #region Require

        if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentNullException();
        if (connectionPool == null) throw new ArgumentNullException();

        #endregion

        // Arguments
        _consumerId = Guid.NewGuid();
        _connectionString = connectionString;
        _connectionPool = connectionPool;

        // Create
        _nativeConnection = _connectionPool.Create(_consumerId, _connectionString);
        if (_nativeConnection == null) throw new InvalidOperationException("Create NativeConnection failed.");
    }

    public void Dispose()
    {
        // Release
        _connectionPool.Release(_consumerId, _connectionString);
    }


    // Methods  
    public void Query()
    {
        // Query
        _nativeConnection.Query();
    }        
}

 

最後我們加上測試的程式以及執行的結果。
由程式碼可以看到,雖然建立了三個Connection來使用,
可是因為物件套用『Singleton Pool模式』的緣故,NativeConnection的建構、解構是依照ConnectionString的數量(兩個)來執行。

 


class Program
{
    static void Main(string[] args)
    {
        Connection connectionA = new Connection("XXX Database");
        Connection connectionB = new Connection("YYY Database");
        Connection connectionC = new Connection("XXX Database");

        connectionA.Query();
        connectionB.Query();
        connectionC.Query();

        connectionA.Dispose();
        connectionB.Dispose();
        connectionC.Dispose();

        Console.ReadLine();
    }
}

 

 

 

後記 :

 

如果在一些不大適合使用Singleton的系統內,也可以採用下列的模式。
增加一個ConnectionManager,來套用『Singleton Pool模式』的功能。

 


public class ConnectionManager
{
    // Fields
    private readonly ConnectionPool _connectionPool = new ConnectionPool();


    // Methods   
    public Connection Create(string connectionString)
    {
        #region Require

        if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentNullException();

        #endregion            
        return new Connection(connectionString, _connectionPool);
    }
}

class Program
{
    static void Main(string[] args)
    {
        ConnectionManager connectionManager = new ConnectionManager();

        Connection connectionA = connectionManager.Create("XXX Database");
        Connection connectionB = connectionManager.Create("YYY Database");
        Connection connectionC = connectionManager.Create("XXX Database");

        connectionA.Query();
        connectionB.Query();
        connectionC.Query();

        connectionA.Dispose();
        connectionB.Dispose();
        connectionC.Dispose();

        Console.ReadLine();
    }
}
期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。