前一篇中,我們設計了GridViewHandler及FormViewHandler,讓商業邏輯可以由主程式中抽離,放置於外部來動態選擇要載入那些商業邏輯,就該例而言,這個設計除了將原本該
置於Data Layout的商業邏輯與UI扯上關聯外,其實並無其它設計較為不當之處,而將商業邏輯與UI扯上關聯這點,其實也是為了讓範例更加簡單易懂而特意設計的,要將這種設計
移置Data Layout裡也很簡單。
The Framework Designing (3) – Configurable Application
文/黃忠成
re-designing or not?
前一篇中,我們設計了GridViewHandler及FormViewHandler,讓商業邏輯可以由主程式中抽離,放置於外部來動態選擇要載入那些商業邏輯,就該例而言,這個設計除了將原本該
置於Data Layout的商業邏輯與UI扯上關聯外,其實並無其它設計較為不當之處,而將商業邏輯與UI扯上關聯這點,其實也是為了讓範例更加簡單易懂而特意設計的,要將這種設計
移置Data Layout裡也很簡單。
但,這個設計有一個設想未周全之處,那就是每個Element、也就是每個Table都只能掛載一個Handler,這點在開發初期並不會造成困擾,但在中後期當同系列的產品變多時,
就會造成如圖1的困擾。
圖1
如圖1所示,Product A、B、C屬於同一系列的產品,透過Handler來客製屬於該客戶的商業邏輯,但由於前篇所設計的延展點受限於每個Table只能掛載一個Handler,所以在產品線展開時,
就會造成Handler中包含過多的重複程式碼,以圖1來說,每個Handler都擁有設定Company Name預設值的能力,但因為不同的產品有著不同數量的預設值需求,所以此處為了適應,
只好將Company Name預設值的能力重複出現在之後延展的Handler裡,最好的設計應該如圖2。
圖2
圖2中假設當一個Table可以掛載多個Handler後的情況,在這種設計下,每個Handler都有各自的任務,可以透過組合來完成某個客戶的需求,這也是具延展性應用程式的主要設計架構。
圖2將原本GridViewHandler、FormViewHandler設計上的缺點揭露無遺,這使得我們必須思考是否要重新設計GridViewHandler及FormViewHandler,這也是當設計Framework或
系統架構時設計師最常遇到的問題,如果這個缺點在設計之初即察覺到,那麼只要重新修改設計概念即可,但有些時候這類問題會出現在產品開發的中後期,此時要更改原設計所需
付出的成本很大,因為得逐一調整開發完成的部分。
當修改原設計須付出的代價是無法或難以承受之時,此時就得往外科手術的方向著手,以本例而言,雖然每個Table只能掛載一個Handler,但這並不構成我們無法在一個Table掛載
Handler的限制,因為只要將掛載的的這一個Handler設計成可以呼叫多個子Handler即可,簡略的說就是將延伸點往下放到Handler上,如圖3所示。
圖3
在圖3中,原本CustomerExt是掛載至GridViewHandler的單一Handler,但如果這樣做就會造成圖1的問題,所以圖3中加入了一個SaveExtDispatcher,其掛載至GridViewHandler,
並將呼叫下放至其動態掛載的CustomerExt1、CustomerExt2上,這種做法就可以達到圖2的架構,這是Dispatcher樣式的應用。
乍看之下,這樣的設計要達到圖2的架構似乎很簡單,不過裡頭潛在一個問題,讓我們先思考SaveExtDispatcher的設計。在SaveExtDispatcher中,會動態的由一個組態檔讀入要
掛載的Handler,並將來自GridViewHandler的呼叫一一下放給這些Handler,實作上,SaveExtDispatcher一定會擁有類似於GridViewHandler中讀取組態檔及動態載入的程式碼,
一切都沒有問題,直到將SaveExtDispacther掛到GridViewHandler中時,組態檔便會把問題點出來。
<appSettings>
<addkey="Customers"value="SaveExtDispatcher.CustomerExtDispatcher, SaveExtDispatcher"/>
</appSettings>
|
看出問題了嗎? 遷就於原本架構,每個Table都必須要有一個獨立的SaveExtDispatcher,所以,[會動態的由一個組態檔讀入要掛載的Handler,並將來自GridViewHandler的呼叫
一一下放給這些Handler]這件事會出現在每一個Table所掛載的獨立SaveExtDispatcher中。
解決這個問題的方法很簡單,就直接把會重複的部分提出來寫成共用的Library即可,本例中就是DataHandlerDispacther.dll。
DataHandlerDispatcher.cs (in DataHandlerDispatcher project)
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Reflection;
using SimpleFramework;
namespace DataHandlerDispatcher
{
public abstract class DataHandlerDispatcher:IDataHandler
{
private List _innerHandlers = null;
protected abstract string GetConfigurationFileName();
private void Initialize(string configurationFile)
{
string appPath = AppDomain.CurrentDomain.BaseDirectory + "\\" + configurationFile;
if(!File.Exists(appPath))
return;
XDocument doc = XDocument.Load(appPath,LoadOptions.None);
var result = from s1 in doc.Descendants("handler") select s1;
_innerHandlers = new List();
foreach (var item in result)
{
if (item.Attribute("type") != null)
{
string[] partAssem = item.Attribute("type").Value.Split(',');
IDataHandler _instance = TypeLoader.CreateInstance(partAssem[1],
partAssem[0], "SimpleFramework.IDataHandler") as IDataHandler;
_innerHandlers.Add(_instance);
}
}
}
public void BeforeSave(System.Collections.Specialized.IOrderedDictionary values)
{
if (_innerHandlers == null)
Initialize(GetConfigurationFileName());
if (_innerHandlers != null)
{
foreach (var item in _innerHandlers)
item.BeforeSave(values);
}
}
}
}
|
接著只要讓SaveExtDispatcher繼承自此類別即可。
SaveExtDispatcher.cs (in SaveExtDispatcher project)
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DataHandlerDispatcher;
namespace SaveExtDispatcher
{
public class CustomerExtDispatcher:DataHandlerDispatcher.DataHandlerDispatcher
{
protected override string GetConfigurationFileName()
{
return "SaveExtDispatcher.config";
}
}
}
|
SaveExtDispatcher會讀取執行目錄下的SaveExtDispatcher.config,本例中如下所示。
SaveExtDispatcher.config(in web/winform project)
|
<?xmlversion="1.0"encoding="utf-8"?>
<handlers>
<handlertype="SaveExt.CustomerExt, SaveExt"/>
<!--
<handlertype="SaveExt2.CustomerExt, SaveExt2"/>
--!>
</handlers>
|
最後只要將SaveExtDispatcher掛到GridViewHandler/FormViewHandler即可,你可以於CreateStep_1目錄中找到整個完整範例。
writing configuration section handler
當然,前節的做法是不得已下的產物,改寫架構才是根本的解決之道,問題的根源是前例就簡使用了只能夠指定單值的appSetting,透過.NET Framework的
Configuration Framework,我們可以自訂一個專屬的Section來儲存Handlers的設定,如下所示。
app.config
|
<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroupname="simpleFramework">
<sectionname="dataHandlers"
type="SimpleFramework.Configuration.DataHandlerConfigurationHandler, SimpleFramework"
allowLocation="true" allowDefinition="Everywhere"/>
</sectionGroup>
</configSections>
<connectionStrings>
<addname="WindowsFormsApplication1.Properties.Settings.NorthwindConnectionString"
connectionString="Data Source=127.0.0.1;Initial Catalog=Northwind;Integrated Security=True"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<simpleFramework>
<dataHandlers>
<items>
<dataItemkey="Customers">
<handlers>
<handlertype="SaveExt.CustomerExt, SaveExt"/>
</handlers>
</dataItem>
</items>
</dataHandlers>
</simpleFramework>
<appSettings>
</appSettings>
</configuration>
|
DataHandlerConfigurationHandler的原始碼如下。
DataExtensionConfigurationHandler.cs(in SimpleFramework)
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
namespace SimpleFramework.Configuration
{
public class DataHandlerConfigurationHandler:ConfigurationSection
{
[ConfigurationProperty("items", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(DataHandlerSectionElementCollection),
AddItemName = "dataItem")]
public DataHandlerSectionElementCollection Items
{
get
{
return (DataHandlerSectionElementCollection)this["items"];
}
}
public static List GetHandlers(string key)
{
DataHandlerConfigurationHandler handler = (DataHandlerConfigurationHandler)System.Configuration.ConfigurationManager.GetSection(
"simpleFramework/dataHandlers");
foreach (DataHandlerSectionElement item in handler.Items)
{
if (item.Key == key && item.Handlers.Count > 0)
return item.Handlers.GetHandlers();
}
return null;
}
}
public class DataHandlerSectionElement : ConfigurationElement
{
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return (string)this["key"];
}
}
[ConfigurationProperty("handlers", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(DataHandlerElementCollection), AddItemName = "handler")]
public DataHandlerElementCollection Handlers
{
get
{
return (DataHandlerElementCollection)this["handlers"];
}
}
}
public class DataHandlerSectionElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new DataHandlerSectionElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((DataHandlerSectionElement)element).Key;
}
}
public class DataHandlerElement : ConfigurationElement
{
[ConfigurationProperty("type", IsRequired = true, IsKey = true)]
public string Type
{
get
{
return (string)this["type"];
}
}
public IDataHandler GetHandler()
{
if (!string.IsNullOrEmpty(Type))
{
string[] partAssem = Type.Split(',');
return TypeLoader.CreateInstance(partAssem[1], partAssem[0],
"SimpleFramework.IDataHandler") as IDataHandler;
}
return null;
}
}
public class DataHandlerElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new DataHandlerElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((DataHandlerElement)element).Type;
}
public List GetHandlers()
{
List result = new List();
foreach (DataHandlerElement item in this)
result.Add(item.GetHandler());
return result;
}
}
}
|
隨著DataExtensionConfigurationHandler的加入,GridViewHandler與FormViewHandler也要做些修改來適應。
GridViewHandler.cs
|
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Configuration;
namespace SimpleFramework.WinForms
{
public class GridViewHandler
{
private List _instances;
private DataGridView _view;
public void Initialize(string key, DataGridView view)
{
view.RowValidating += new DataGridViewCellCancelEventHandler(view_RowValidating);
_view = view;
_instances =
SimpleFramework.Configuration.DataHandlerConfigurationHandler.GetHandlers(key);
}
void view_RowValidating(object sender, DataGridViewCellCancelEventArgs e)
{
OrderedDictionary values = new OrderedDictionary();
foreach (DataGridViewCell item in _view.Rows[e.RowIndex].Cells)
{
if(item.Value is DBNull)
values.Add(_view.Columns[item.ColumnIndex].DataPropertyName, null);
else
values.Add(_view.Columns[item.ColumnIndex].DataPropertyName, item.Value);
}
foreach (var item in _instances)
item.BeforeSave(values);
foreach (DataGridViewCell item in _view.Rows[e.RowIndex].Cells)
item.Value = values[_view.Columns[item.ColumnIndex].DataPropertyName];
}
}
}
|
FormViewExt.cs
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using System.Configuration;
using System.Reflection;
using SimpleFramework;
namespace SimpleFramework.Web
{
public class FormViewHandler
{
private List _instances;
public void Initialize(string key, FormView view)
{
_instances =
SimpleFramework.Configuration.DataHandlerConfigurationHandler.GetHandlers(key);
view.ItemInserting += new FormViewInsertEventHandler(view_ItemInserting);
view.ItemUpdating += new FormViewUpdateEventHandler(view_ItemUpdating);
}
void view_ItemInserting(object sender, FormViewInsertEventArgs e)
{
foreach (var item in _instances)
item.BeforeSave(e.Values);
}
void view_ItemUpdating(object sender, FormViewUpdateEventArgs e)
{
foreach (var item in _instances)
item.BeforeSave(e.NewValues);
}
}
}
|
CreateStep_2中可找到此例的原始碼。
using dispatcher pattern
照上節的設計,GridViewHandler/FormViewHandler都要做出修改,那有沒有更簡單的方法呢?有的,只要應用先前用過的Dispatcher pattern即可。
DataExtensionConfigurationHandler.cs(in SimpleFramework)
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
namespace SimpleFramework.Configuration
{
public class HandlerDispatcher : IDataHandler
{
private List _innerHandlers = null;
public HandlerDispatcher(List handlers)
{
_innerHandlers = handlers;
}
public void BeforeSave(System.Collections.Specialized.IOrderedDictionary values)
{
foreach (var item in _innerHandlers)
item.BeforeSave(values);
}
}
public class DataHandlerConfigurationHandler:ConfigurationSection
{
[ConfigurationProperty("items", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(DataHandlerSectionElementCollection), AddItemName = "dataItem")]
public DataHandlerSectionElementCollection Items
{
get
{
return (DataHandlerSectionElementCollection)this["items"];
}
}
public static IDataHandler GetHandler(string key)
{
DataHandlerConfigurationHandler handler =
(DataHandlerConfigurationHandler)System.Configuration.ConfigurationManager.GetSection(
"simpleFramework/dataHandlers");
foreach (DataHandlerSectionElement item in handler.Items)
{
if (item.Key == key && item.Handlers.Count > 0)
return item.Handlers.GetHandler();
}
return null;
}
}
public class DataHandlerSectionElement : ConfigurationElement
{
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return (string)this["key"];
}
}
[ConfigurationProperty("handlers", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(DataHandlerElementCollection), AddItemName = "handler")]
public DataHandlerElementCollection Handlers
{
get
{
return (DataHandlerElementCollection)this["handlers"];
}
}
}
public class DataHandlerSectionElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new DataHandlerSectionElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((DataHandlerSectionElement)element).Key;
}
}
public class DataHandlerElement : ConfigurationElement
{
[ConfigurationProperty("type", IsRequired = true, IsKey = true)]
public string Type
{
get
{
return (string)this["type"];
}
}
public IDataHandler GetHandler()
{
if (!string.IsNullOrEmpty(Type))
{
string[] partAssem = Type.Split(',');
return TypeLoader.CreateInstance(partAssem[1], partAssem[0],
"SimpleFramework.IDataHandler") as IDataHandler;
}
return null;
}
}
public class DataHandlerElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new DataHandlerElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((DataHandlerElement)element).Type;
}
public IDataHandler GetHandler()
{
List result = new List();
foreach (DataHandlerElement item in this)
result.Add(item.GetHandler());
return new HandlerDispatcher(result);
}
}
}
|
整個架構如圖4。
你可以在CreateExt_Step4找到完整的範例程式碼。
configuration providers
當提出Simple Framework這種可抽換式的架構時,我常聽到的回應多半不是這些東西怎麼實作出來,而是組態檔所放置的位置,例如不想放在app.config/web.config裡,
想放在例如Database、另一個Server、或是由Web Service取得等等的需求,但實際上要達到這些需求並不難,比起設計一整個可抽換式架構來說,這簡直就是小兒科,
本文末尾將Simple Framework修改成如圖5所示。
圖5
簡單的說,將讀取組態檔的部分應用抽換式架構,平移至一個外部的Assembly中,形成可抽換式的組態檔來源。
DataExtensionConfigurationHandler.cs(in SimpleFramework)
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
namespace SimpleFramework.Configuration
{
public interface IDataHandlerConfigurationProvider
{
IDataHandler GetHandler(string providerConnectionString, string key);
}
public class HandlerDispatcher : IDataHandler
{
private List _innerHandlers = null;
public HandlerDispatcher(List handlers)
{
_innerHandlers = handlers;
}
public void BeforeSave(System.Collections.Specialized.IOrderedDictionary values)
{
foreach (var item in _innerHandlers)
item.BeforeSave(values);
}
}
public class DataHandlerConfigurationHandler:ConfigurationSection
{
[ConfigurationProperty("items", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(DataHandlerSectionElementCollection),
AddItemName = "dataItem")]
public DataHandlerSectionElementCollection Items
{
get
{
return (DataHandlerSectionElementCollection)this["items"];
}
}
[ConfigurationProperty("provider")]
public string Provider
{
get
{
return (string)this["provider"];
}
}
[ConfigurationProperty("providerConnectionString")]
public string ProviderConnectionString
{
get
{
return (string)this["providerConnectionString"];
}
}
public static IDataHandler GetHandler(string key)
{
DataHandlerConfigurationHandler handler =
(DataHandlerConfigurationHandler)System.Configuration.ConfigurationManager.GetSection(
"simpleFramework/dataHandlers");
if (!string.IsNullOrEmpty(handler.Provider))
{
string[] partAssem = handler.Provider.Split(',');
IDataHandlerConfigurationProvider provider =
(IDataHandlerConfigurationProvider)TypeLoader.CreateInstance(partAssem[1], partAssem[0],
"SimpleFramework.Configuration.IDataHandlerConfigurationProvider");
return provider.GetHandler(handler.ProviderConnectionString, key);
}
foreach (DataHandlerSectionElement item in handler.Items)
{
if (item.Key == key && item.Handlers.Count > 0)
return item.Handlers.GetHandler();
}
return null;
}
}
public class DataHandlerSectionElement : ConfigurationElement
{
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return (string)this["key"];
}
}
[ConfigurationProperty("handlers", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(DataHandlerElementCollection),
AddItemName = "handler")]
public DataHandlerElementCollection Handlers
{
get
{
return (DataHandlerElementCollection)this["handlers"];
}
}
}
public class DataHandlerSectionElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new DataHandlerSectionElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((DataHandlerSectionElement)element).Key;
}
}
public class DataHandlerElement : ConfigurationElement
{
[ConfigurationProperty("type", IsRequired = true, IsKey = true)]
public string Type
{
get
{
return (string)this["type"];
}
}
public IDataHandler GetHandler()
{
if (!string.IsNullOrEmpty(Type))
{
string[] partAssem = Type.Split(',');
return TypeLoader.CreateInstance(partAssem[1], partAssem[0], "SimpleFramework.IDataHandler") as IDataHandler;
}
return null;
}
}
public class DataHandlerElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new DataHandlerElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((DataHandlerElement)element).Type;
}
public IDataHandler GetHandler()
{
List result = new List();
foreach (DataHandlerElement item in this)
result.Add(item.GetHandler());
return new HandlerDispatcher(result);
}
}
}
|
CreateExt_Step5目錄中就是應用此架構的範例,你可於其中找到XmlConfigurationProvider Project,其將會由另一個XML檔案中讀取組態檔。
XmlProvider.cs(in XmlConfigurationProvider project)
|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Reflection;
using System.IO;
using SimpleFramework;
using SimpleFramework.Configuration;
namespace XmlConfigurationProvider
{
public class XmlProvider:IDataHandlerConfigurationProvider
{
public SimpleFramework.IDataHandler GetHandler(string providerConnectionString, string key)
{
XDocument doc = XDocument.Load(AppDomain.CurrentDomain.BaseDirectory+"\\"+providerConnectionString);
var keyRoot = (from s1 in doc.Descendants("dataItem") where s1.Attribute("key").Value == key select s1).FirstOrDefault();
if (keyRoot != null)
{
var items = keyRoot.Descendants("handlers").FirstOrDefault().Descendants("handler");
List list = new List();
foreach (var item in items)
{
string[] partAssem = item.Attribute("type").Value.Split(',');
list.Add((IDataHandler)TypeLoader.CreateInstance(partAssem[1], partAssem[0], "SimpleFramework.IDataHandler"));
}
return new HandlerDispatcher(list);
}
return null;
}
}
}
|
以下為web.config的組態內容。
<?xmlversion="1.0"?>
<!--
For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<configSections>
<sectionGroupname="simpleFramework">
<sectionname="dataHandlers"type="SimpleFramework.Configuration.DataHandlerConfigurationHandler, SimpleFramework"
allowLocation="true" allowDefinition="Everywhere"/>
</sectionGroup>
</configSections>
<connectionStrings>
<addname="NorthwindConnectionString"
connectionString=
"Data Source=127.0.0.1;Initial Catalog=Northwind;Integrated Security=True"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<simpleFramework>
<dataHandlers
provider="XmlConfigurationProvider.XmlProvider, XmlConfigurationProvider"providerConnectionString="handler.config"/>
</simpleFramework>
<system.web>
<compilationdebug="true"targetFramework="4.0"/>
</system.web>
</configuration>
|
handler.config的內容如下:
<?xmlversion="1.0"encoding="utf-8"?>
<items>
<dataItemkey="Customers">
<handlers>
<handlertype="SaveExt.CustomerExt, SaveExt"/>
</handlers>
</dataItem>
</items>
|
本文後記
說實在的,當你掌握了動態載入Assembly及設計組態檔的技巧後,要設計一個簡單的可抽換式架構就不難了,難得是當這些元件載入後彼此間的互動,
這又是另一個層次的問題了,未來本系列文章會持續地討論這些。
範例下載: http://code6421.myweb.hinet.net/Framework/Arch_2.zip