外观模式被用来把一组复杂的类包装在一个比较简单的封装接口中。
随着类的不断发展,可能会存在一些复杂的子系统,并且每个子系统有着自己复杂的接口。外观模式允许通过给这些子系统提供一个简化了的接口来降低这一复杂性。在降低复杂性的同时,用户也可以自己选择直接使用底层的方法。
在C#中ADO.NET的组件有着非常复杂的数据应用,我们这里的例子就是对ADO.NET组件中的类进行封装,提供一个比较简单的对数据库操作的接口。
我们有一个数据问价,存储着不同商店货物的价格,格式如下:
Stop and Shop, Apples, 0.27
Stop and Shop, Oranges, 0.36
Stop and Shop, Hamburger, 1.98
Stop and Shop, Butter, 2.39
Stop and Shop, Milk, 1.98
Stop and Shop, Cola, 2.65
Stop and Shop, Green beans, 2.29
Village Market, Apples, 0.29
Village Market, Oranges, 0.29
Village Market, Hamburger, 2.45
在我们的程序中,要实现的功能是,选择一种货物,然后显示不同商店的价格,用户界面如图:
我们的程序封装了一个处理创建、连接以及使用数据的新外观类。
我们先创建一个代表了到数据库连接的抽象DBase类,该类封装了连接的简历、表的打开和SQL查询:
using System;
using System.Data ;
using System.Data.SqlClient ;
using System.Data.OleDb ;
namespace Facade
{
/// <summary>
/// Summary description for DBase.
/// </summary>
public abstract class DBase {
protected OleDbConnection conn;
private void openConnection() {
if (conn.State == ConnectionState.Closed){
conn.Open ();
}
}
//------
private void closeConnection() {
if (conn.State == ConnectionState.Open ){
conn.Close ();
}
}
//------
public DataTable openTable (string tableName) {
OleDbDataAdapter adapter = new OleDbDataAdapter ();
DataTable dtable = null;
string query = "Select * from " + tableName;
adapter.SelectCommand = new OleDbCommand (query, conn);
DataSet dset = new DataSet ("mydata");
try {
openConnection();
adapter.Fill (dset);
dtable = dset.Tables [0];
}
catch(Exception e) {
Console.WriteLine (e.Message );
}
return dtable;
}
//------
public DataTable openQuery(string query) {
OleDbDataAdapter dsCmd = new OleDbDataAdapter ();
DataSet dset = new DataSet (); //create a dataset
DataTable dtable = null; //declare a data table
try {
//create the command
dsCmd.SelectCommand = new OleDbCommand(query, conn);
openConnection(); //open the connection
//fill the dataset
dsCmd.Fill(dset, "mine");
//get the table
dtable = dset.Tables[0];
closeConnection(); //always close it
return dtable; //and return it
}
catch (Exception e) {
Console.WriteLine (e.Message);
return null;
}
}
//------
public void openConnection(string connectionString) {
conn = new OleDbConnection(connectionString);
}
//------
public OleDbConnection getConnection() {
return conn;
}
}
}
我们根据需要根据使用的不同的数据库生成这个类的一些派生类,比如Access数据库的派生类版本:
using System;
using System.Data ;
using System.Data.SqlClient ;
using System.Data.OleDb ;
namespace Facade
{
/// <summary>
/// Summary description for AxsDatabase.
/// </summary>
public class AxsDatabase :DBase
{
public AxsDatabase(string dbName) {
string connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + dbName;
openConnection(connectionString);
}
}
}
SQL Server的数据库的派生类版本:
using System;
using System.Data ;
using System.Data.SqlClient ;
using System.Data.OleDb ;
namespace Facade
{
/// <summary>
/// Summary description for SQLServerDatabase.
/// </summary>
public class SQLServerDatabase:DBase {
string connectionString;
//-----
public SQLServerDatabase(String dbName) {
connectionString = "Persist Security Info = False;" +
"Initial Catalog =" + dbName + ";" +
"Data Source = myDataServer;User ID = myName;" +
"password=";
openConnection(connectionString);
}
//-----
public SQLServerDatabase(string dbName, string serverName, string userid, string pwd) {
connectionString = "Persist Security Info = False;" +
"Initial Catalog =" + dbName + ";" +
"Data Source =" + serverName + ";" +
"User ID =" + userid + ";" +
"password=" + pwd;
openConnection(connectionString);
}
}
}
这两个不同的派生类提供了不同的数据库连接字符串。
用到的另外一个类是DBTable类,封装了打开、加载以及更新单个数据表的操作。我们通过派生这个类可以实现商品类,商店类,价格类。
DBTable:
using System;
using System.Data ;
using System.Data.SqlClient ;
using System.Data.OleDb ;
using System.Collections ;
namespace Facade
{
/// <summary>
/// Summary description for DBTable.
/// </summary>
public class DBTable {
protected DBase db;
protected string tableName;
private bool filled, opened;
private DataTable dtable;
private int rowIndex;
private Hashtable names;
private string columnName;
private DataRow row;
private OleDbConnection conn;
private int index;
//-----
public DBTable(DBase datab, string tb_Name) {
db = datab;
tableName = tb_Name;
filled =false;
opened = false;
names = new Hashtable();
}
//-----
public void createTable() {
try {
dtable = new DataTable(tableName);
dtable.Clear();
}
catch (Exception e) {
Console.WriteLine (e.Message );
}
}
//-----
public bool hasMoreElements() {
if(opened)
return (rowIndex < dtable.Rows.Count) ;
else
return false;
}
//-----
public virtual string getValue(string cname) {
//returns the next name in the table
//assumes that openTable has already been called
if (opened) {
DataRow row = dtable.Rows[rowIndex++];
return row[cname].ToString().Trim ();
}
else
return "";
}
//-----
public int getKey(string nm, string keyname){
DataRow row;
int key;
if(! filled)
return (int)names[ nm];
else {
string query = "select * from " + tableName + " where " + columnName + "=\'" + nm + "\'";
dtable = db.openQuery(query);
row = dtable.Rows[0];
key = Convert.ToInt32 (row[keyname].ToString());
return key;
}
}
//-----
public virtual void makeTable(string cName) {
columnName = cName;
//stores current hash table values in data table
DataSet dset = new DataSet(tableName); //create the data set
dtable = new DataTable(tableName); //and a datatable
dset.Tables.Add(dtable); //add to collection
conn = db.getConnection();
openConn(); //open the connection
OleDbDataAdapter adcmd = new OleDbDataAdapter();
//open the table
adcmd.SelectCommand =
new OleDbCommand("Select * from " + tableName, conn);
OleDbCommandBuilder olecb = new OleDbCommandBuilder(adcmd);
adcmd.TableMappings.Add("Table", tableName);
//load current data into the local table copy
adcmd.Fill(dset, tableName);
//get the Enumerator from the Hashtable
IEnumerator ienum = names.Keys.GetEnumerator();
//move through the table, adding the names to new rows
while (ienum.MoveNext()) {
string name = (string)ienum.Current;
row = dtable.NewRow(); //get new rows
row[columnName] = name;
dtable.Rows.Add(row); //add into table
}
//Now update the database with this table
try {
adcmd.Update(dset);
closeConn();
filled = true;
}
catch (Exception e) {
Console.WriteLine (e.Message);
}
}
//-----
private void closeConn() {
if( conn.State == ConnectionState.Open) {
conn.Close();
}
}
//-----
private void openConn() {
if(conn.State == ConnectionState.Closed ) {
conn.Open();
}
}
//-----
public void addTableValue(string nm) {
//accumulates names in hash table
try {
names.Add(nm, index++);
}
catch (ArgumentException) {}
//do not allow duplicate names to be added
}
public void openTable() {
dtable = db.openTable(tableName);
rowIndex = 0;
if(dtable != null)
opened = true;
}
//-----
public void delete() {
//deletes entire table
conn = db.getConnection();
openConn();
if (conn.State == ConnectionState.Open ) {
OleDbCommand adcmd =
new OleDbCommand("Delete * from " + tableName, conn);
try{
adcmd.ExecuteNonQuery();
closeConn();
}
catch (Exception e) {
Console.WriteLine (e.Message);
}
}
}
}
}
然后我们就可以派生这个类生成商店类,商品类和价格类。
using System;
namespace Facade
{
/// <summary>
/// Summary description for Stores.
/// </summary>
public class Stores :DBTable {
public Stores(DBase db):base(db, "Stores"){
}
//-----
public void makeTable() {
base.makeTable ("Storename");
}
}
}
using System;
namespace Facade
{
/// <summary>
/// Summary description for Foods.
/// </summary>
public class Foods: DBTable {
public Foods(DBase db):base(db, "Foods"){
}
//-----
public void makeTable() {
base.makeTable ("Foodname");
}
//-----
public string getValue() {
return base.getValue ("FoodName");
}
}
}
Price类的构建有点特殊,因为Price类包含的其他两个表的引用,所以需要添加一个其他的表示在Price表格中一行的对象:
using System;
namespace Facade
{
/// <summary>
/// Summary description for StoreFoodPrice.
/// </summary>
public class StoreFoodPrice {
private int storeKey, foodKey;
private float foodPrice;
//-----
public StoreFoodPrice(int sKey, int fKey, float fPrice) {
storeKey = sKey;
foodKey = fKey;
foodPrice = fPrice;
}
//-----
public int getStore() {
return storeKey;
}
//-----
public int getFood() {
return foodKey;
}
//-----
public float getPrice() {
return foodPrice;
}
}
}
这样Price类中添加一行数据,就实际需要添加StoreFoodPrice 类的一个实例。
using System;
using System.Data ;
using System.Data.SqlClient ;
using System.Data.OleDb ;
using System.Collections ;
namespace Facade
{
/// <summary>
/// Summary description for Prices.
/// </summary>
public class Prices : DBTable {
private ArrayList priceList;
public Prices(DBase db) : base(db, "Prices") {
priceList = new ArrayList ();
}
//-----
public void makeTable() {
//stores current array list values in data table
OleDbConnection adc = new OleDbConnection();
DataSet dset = new DataSet(tableName);
DataTable dtable = new DataTable(tableName);
dset.Tables.Add(dtable);
adc = db.getConnection();
if (adc.State == ConnectionState.Closed)
adc.Open();
OleDbDataAdapter adcmd = new OleDbDataAdapter();
//fill in price table
adcmd.SelectCommand =
new OleDbCommand("Select * from " + tableName, adc);
OleDbCommandBuilder custCB = new OleDbCommandBuilder(adcmd);
adcmd.TableMappings.Add("Table", tableName);
adcmd.Fill(dset, tableName);
IEnumerator ienum = priceList.GetEnumerator();
//add new price entries
while (ienum.MoveNext() ) {
StoreFoodPrice fprice = (StoreFoodPrice)ienum.Current;
DataRow row = dtable.NewRow();
row["foodkey"] = fprice.getFood();
row["storekey"] = fprice.getStore();
row["price"] = fprice.getPrice();
dtable.Rows.Add(row); //add to table
}
adcmd.Update(dset); //send back to database
adc.Close();
}
//-----
public DataTable getPrices(string food) {
string query=
"SELECT Stores.StoreName, " +
"Foods.Foodname, Prices.Price " +
"FROM (Prices INNER JOIN Foods ON " +
"Prices.Foodkey = Foods.Foodkey) " +
"INNER JOIN Stores ON Prices.StoreKey = Stores.StoreKey " +
"WHERE(((Foods.Foodname) = \'" + food + "\')) " +
"ORDER BY Prices.Price";
return db.openQuery(query);
}
//-----
public void addRow(int storeKey, int foodKey, float price) {
priceList.Add (new StoreFoodPrice (storeKey, foodKey, price));
}
}
}
这三个相应的类都把本地数据更新到了数据库中。
现在所有的派生类都设计好了,就需要编写一个从数据文件中加载表的类了,该类第一次构建Stores和Foods这两个数据表,接着再次读取数据文件构造Prices这个表。
using System;
using CsharpPats;
namespace Facade
{
/// <summary>
/// Summary description for DataLoader.
/// </summary>
public class DataLoader {
private csFile vfile;
private Stores store;
private Foods fods;
private Prices price;
private DBase db;
//-----
public DataLoader(DBase datab) {
db = datab;
store = new Stores(db);
fods = new Foods (db);
price = new Prices(db);
}
//-----
public void load(string dataFile) {
string sline;
int storekey, foodkey;
StringTokenizer tok;
//delete current table contents
store.delete();
fods.delete();
price.delete();
//now read in new ones
vfile = new csFile(dataFile);
vfile.OpenForRead();
sline = vfile.readLine();
while (sline != null){
tok = new StringTokenizer(sline, ",");
store.addTableValue(tok.nextToken()); //store name
fods.addTableValue(tok.nextToken()); //food name
sline = vfile.readLine();
}
vfile.close();
//construct store and food tables
store.makeTable();
fods.makeTable();
vfile.OpenForRead();
sline = vfile.readLine();
while (sline != null) {
//get the gets and add to storefoodprice objects
tok = new StringTokenizer(sline, ",");
storekey = store.getKey(tok.nextToken(), "Storekey");
foodkey = fods.getKey(tok.nextToken(), "Foodkey");
price.addRow(storekey, foodkey, Convert.ToSingle (tok.nextToken()));
sline = vfile.readLine();
}
//add all to price table
price.makeTable();
vfile.close();
}
}
}
这个类中调用了三个表类的makeTable()方法,把数据更新到了数据库。
最终调用的主程序:
private DBase db;
private Stores shops;
private Prices prc;
//-----------
private void init() {
db = new AxsDatabase("Groceries.mdb");
shops = new Stores(db);
prc = new Prices(db);
loadFoodTable();
ToolTip tips = new ToolTip();
tips.SetToolTip(btLoad, "Reload data from groceries.txt file");
}
public Form1()
{
InitializeComponent();
init();
}
private void loadFoodTable() {
Foods fods =new Foods(db);
fods.openTable();
while (fods.hasMoreElements()){
lsFoods.Items.Add(fods.getValue());
}
}
private void btLoad_Click(object sender, System.EventArgs e) {
lsFoods.Items.Clear();
Cursor.Current = Cursors.WaitCursor;
DataLoader dload = new DataLoader(db);
dload.load("groceries.txt");
loadFoodTable();
Cursor.Current = Cursors.Default;
}
private void lsFoods_SelectedIndexChanged(object sender, System.EventArgs e) {
string food = lsFoods.Text;
DataTable dtable = prc.getPrices(food);
lsPrices.Items.Clear();
foreach (DataRow rw in dtable.Rows) {
lsPrices.Items.Add(rw["StoreName"].ToString().Trim() +
"\t" + rw["Price"].ToString());
}
}
外观模式帮助客户端避开复杂的子系统组件,并未普通用户提供了一个比较简单的编程接口。不过,其并不阻止高级用户在需要的时候直接使用较底层的更加复杂的类。外观模式允许在无需要求客户端代码变更的情况下对底层子系统做出修改,这降低了编译依赖。