技术图文:03 结构型设计模式(上)
结构型设计模式(上)
本教程主要介绍一系列用于如何将现有类或对象组合在一起形成更加强大结构的经验总结。
知识结构:
享元模式 – 实现对象的复用
Sunny 软件公司欲开发一个围棋软件,其界面效果如下图所示:

图2 围棋软件界面效果图
Sunny 软件公司开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。
如果将每一个棋子都作为一个独立的对象存储在内存中,将导致围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能是 Sunny 公司开发人员需要解决的一个问题。
为了节约存储空间,提高系统性能,Sunny 公司开发人员使用共享技术来设计围棋软件中的棋子,其基本结构如下图所示:

图3 围棋棋子结构图
Chessman
充当抽象享元类,WhiteChessman
和BlackChessman
是具体享元类,ChessmanFactory
是享元工厂类。
在实现该类时,使用了 单列模式 和 简单工厂模式,确保了工厂对象的唯一性,并提供工厂方法来向客户端返回共享对象。
将棋子的位置Coordinates
定义为棋子的一个外部状态,在需要时再进行设置,这样即解决了黑子和白子的共享,又解决了显示不同位置的问题。
完整代码如下:
(1)棋子位置类(坐标类):
public class Coordinates
{public int X { get; set; }public int Y { get; set; }public Coordinates(int x, int y){X = x;Y = y;}public override string ToString(){return X + "," + Y;}
}
(2)棋子类:
//棋子抽象类
public abstract class Chessman
{public abstract string Color { get; }public void Display(Coordinates coord){Console.WriteLine("棋子颜色:{0},位置:{1}", Color, coord);}
}//白色棋子实体类
public class WhiteChessman : Chessman
{public override string Color{get { return "白色"; }}
}//黑色棋子实体类
public class BlackChessman : Chessman
{public override string Color{get { return "黑色"; }}
}
(3)围棋棋子工厂类:
public class ChessmanFactory
{private static readonly ChessmanFactory Instance = new ChessmanFactory();private static Hashtable _ht; //使用HashTable来存储共享对象private ChessmanFactory(){_ht = new Hashtable();Chessman black = new BlackChessman();Chessman white = new WhiteChessman();_ht.Add("b", black);_ht.Add("w", white);}//返回共享工厂的唯一实例public static ChessmanFactory GetInstance(){return Instance;}//通过key获取存储在HashTable中的共享对象public Chessman GetChessman(string color){return _ht[color] as Chessman;}
}
(4)客户端:
static void Main(string[] args)
{ChessmanFactory factory = ChessmanFactory.GetInstance();Chessman black1 = factory.GetChessman("b");Chessman black2 = factory.GetChessman("b");Chessman black3 = factory.GetChessman("b");Console.WriteLine("判断两颗黑子是否相同:{0}", object.ReferenceEquals(black1, black2));Chessman white1 = factory.GetChessman("w");Chessman white2 = factory.GetChessman("w");Console.WriteLine("判断两颗白子是否相同:{0}", object.ReferenceEquals(white1, white2));black1.Display(new Coordinates(1, 2));black2.Display(new Coordinates(3, 4));black3.Display(new Coordinates(1, 3));white1.Display(new Coordinates(2, 5));white2.Display(new Coordinates(2, 4));
}
输出结果如下图所示:

图4 运行结果
从输出结果可以看出,虽然我们获取了三个黑子对象和两个白子对象,但是它们的内存地址相同,也就是说,它们实际上是同一个对象。
在每次调用display()
方法时,由于设置了不同的外部状态,所以显示在棋盘的不同位置。
若一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等问题。为了避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作,享元模式 因此诞生。
享元模式(Flyweight Pattern):
通过共享技术实现相同或相似对象的重用,存储这些对象的地方称为享元池(Flyweight Pool)。
享元模式 以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。
- 内部状态是存储在享元对象内部并且不会随着环境改变而改变的状态,内部状态可以共享。
- 外部状态是随环境改变而改变的、不可以共享的状态。通常由客户端保存,并在创建享元对象之后,需要使用时传入到享元对象内部。每个外部状态之间都是相互独立的。
我们可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。
享元模式 结构较为复杂,一般与 简单工厂模式 一起使用,其结构如下图所示:

图5 享元模式类图
Flyweight
(抽象享元类):通常是一个接口或抽象类,在抽象享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的属性,而外部状态通过注入的方式添加到享元类中。ConcreteFlyweight
(实体享元类):它实现/继承了抽象享元类,其实例称为享元对象。UnsharedConcreteFlyweight
(非共享实体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享实体享元类,当需要一个非共享实体享元类的对象时可以直接通过实例化创建。FlyweightFactory
(享元工厂类):用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的实体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合,可以结合 简单工厂模式 进行设计。当用户请求一个实体享元对象时,享元工厂提供一个存储在享元池中已经创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。在一个系统中,通常只有唯一一个享元工厂,因此可以使用 单例模式 进行享元工厂类的设计。
注意:享元模式 需要维护一个记录了系统已有的所有享元对象的列表,而这本身需要耗费资源。另外,为了使享元对象可以共享,需要将一些状态外部化,使得程序逻辑复杂化。因此,应当在有足够多的实例对象可供共享时才值得使用享元模式。
享元模式与字符串
在 C# 语言中,如果每次执行类似string str1 = “abcd”
的操作时都创建一个新的字符串对象将导致内存开销很大,因此如果第一次创建了内容为“abcd”的字符串对象str1
,下一次再创建相同的字符串对象str2
时会将它的引用指向str1
,不会重新分配内存空间,从而实现了“abcd”在内存中的共享。
见以下程序代码:
class Program
{static void Main(string[] args){string str1 = "abcd";string str2 = "abcd";string str3 = "ab" + "cd";string str4 = "ab";str4 += "cd";Console.WriteLine(object.ReferenceEquals(str1, str2));Console.WriteLine(object.ReferenceEquals(str1, str3));Console.WriteLine(object.ReferenceEquals(str1, str4));Console.WriteLine(str1 == str4);Console.WriteLine(str1.Equals(str4));str2 += "e";Console.WriteLine(object.ReferenceEquals(str1, str2));}
}

图6 程序运行结果
- 前两个输出语句均为
True
,说明str1
,str2
,str3
在内存中引用了相同的对象。 - 字符串
str4
的初值为“ab”,再对它进行操作str4 += “cd”
,此时虽然str4
的内容与str1
相同,但是由于str4
的初始值不同,在创建str4
时重新分配了内存,所以第三个输出语句结果为false
。 - 最后一个输出语句也为
false
,说明当对str2
进行修改时将创建一个新的对象,修改工作在新对象上完成,而原来引用的对象并没有发生任何改变,str1
仍然引用原有对象,而str2
引用新对象,str1
与str2
引用了两个完全不同的对象(Copy On Write)。
注:string
类型是一个特殊的引用类型,它的判断不同于其它引用类型去比较对象引用是否指向堆中同一实例,而是和值类型判断一致,比较对象内容是否一一相等。
外观模式 – 为外部调用提供统一入口
Sunny 软件公司欲开发一个可应用于多个系统的文件加密模块,该模块可以对文件中的数据进行加密并将加密之后的数据存储在一个新文件中,具体的流程包括三个部分,分别是读取源文件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。
这三个操作相对独立,为了实现代码的独立重用,让设计更符合 单一职责原则,这三个操作的业务代码封装在三个不同的类中。
通过分析,得到软件结构如下图所示:

图7 文件加密模块结构图
实现代码如下:
(1)FileReader
:充当文件读取的子系统类。
internal class FileReader
{public string Read(string fileNameSrc){StringBuilder sb = new StringBuilder();try{FileStream fs = new FileStream(fileNameSrc, FileMode.Open);int data;while ((data = fs.ReadByte()) != -1){sb.Append((char) data);}fs.Close();}catch (Exception e){Console.WriteLine("读取文件错误:" + e.Message);return null;}Console.WriteLine("读取文件,获取明文:" + sb);return sb.ToString();}
}
(2)FileWriter
:充当文件保存的子系统类。
internal class FileWriter
{public void Write(string encryptStr, string fileNameDes){try{FileStream fs = new FileStream(fileNameDes, FileMode.Create);byte[] str = Encoding.Default.GetBytes(encryptStr);fs.Write(str, 0, str.Length);fs.Flush();fs.Close();}catch (Exception e){string info = "保存文件错误:" + e.Message;Console.WriteLine(info);return;}Console.WriteLine("保存密文,写入文件。");}
}
(3)CipherMachine
:充当数据加密的子系统类。
internal class CipherMachine
{public string Encrypt(string plainText){Console.Write("数据加密,将明文转换为密文:");StringBuilder sb = new StringBuilder();char[] chars = plainText.ToCharArray();foreach (char ch in chars){sb.Append((ch%7));}Console.WriteLine(sb);return sb.ToString();}
}
(4)EncryptFacade
:充当加密外观类,为外部调用提供统一入口。
public class EncryptFacade
{ private readonly FileReader _reader;private readonly CipherMachine _cipher;private readonly FileWriter _writer;public EncryptFacade(){_reader = new FileReader();_cipher = new CipherMachine();_writer = new FileWriter();} public void FileEncrypt(string fileNameSrc, string fileNameDes){string plainStr = _reader.Read(fileNameSrc);if (string.IsNullOrEmpty(plainStr)){return;}string encryptStr = _cipher.Encrypt(plainStr);_writer.Write(encryptStr, fileNameDes);}
}
(5)客户端代码:
class Program
{static void Main(string[] args){EncryptFacade ef = new EncryptFacade();ef.FileEncrypt("src.txt", "des.txt");}
}
输出结果:

图8 输出结果
在本案例中,对文件 src.txt 中的数据进行加密,该文件内容为 “Hello world!”,加密之后将密文保存到另一个文件 des.txt 中,程序运行后保存在文件中的密文为“233364062325”。
在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,由于涉及到的类比较多,导致使用时代码较为复杂,此时,特别需要一个角色,由它来负责和多个业务类进行交互,而客户类只需与该类交互。
外观模式 通过引入一个外观类(Facade)来充当这个角色,为多个业务类的调用提供统一的入口,简化了类与类之间的交互。在外观模式中,那些需要交互的业务类被称为子系统(Subsystem)。
如果没有外观类,那么每个客户类需要和多个子系统之间进行复杂的交互,系统的耦合度将很大,如图 9(A)所示;而引入外观类之后,客户类只需要直接与外观类交互,客户类与子系统之间原有的复杂关系由外观类来实现,从而降低了系统的耦合度,如图 9(B)所示。

图9 外观模式示意图
外观模式 并不给系统增加任何新功能,它仅仅是简化调用接口。
外观模式(Facade Pattern):
又称为门面模式,为子系统中的一组接口提供一个统一的入口。
外观模式 是 迪米特法则 的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。
外观模式 的结构如下图所示:

图10 外观模式结构图
Facade
:在 Client 可以调用它的方法,Facade 知道相关的(一个或者多个) SubSystem 的功能和责任;在正常情况下,它将所有从 Client 发来的请求委派给相应的 SubSystem 处理。SubSystem
:在软件系统中可以有一个或者多个 SubSystem,每个 SubSystem 可以不是一个单独的类,而是一个类的集合,它实现 SubSystem 的功能;每一个 SubSystem 都可以被 Client 直接调用,或者被 Facade 调用;SubSystem 并不知道 Facade 的存在,对于 SubSystem 而言,Facade 仅仅是另外一个 Client 而已。
(1)子系统类
internal class SubSystemA
{public void MethodA(){//业务实现代码}
}internal class SubSystemB
{public void MethodB(){//业务实现代码}
}internal class SubSystemC
{public void MethodC(){//业务实现代码}
}
(2)外观类
public class Facade
{private SubSystemA _obj1 = new SubSystemA();private SubSystemB _obj2 = new SubSystemB();private SubSystemC _obj3 = new SubSystemC();public void Method(){_obj1.MethodA();_obj2.MethodB();_obj3.MethodC();}
}
(3)客户端
class Program
{static void Main(string[] args){Facade facade = new Facade();facade.Method();}
}
抽象外观类
如果在案例“文件加密模块”中需要更换一个加密类,不再使用原有的基于求模运算的加密类CipherMachine
,而改为基于移位运算的新加密类NewCipherMachine
,其代码如下:
internal string Encrypt(string plainText)
{Console.Write("数据加密,将明文转换为密文:");StringBuilder sb = new StringBuilder();int key = 10; //设置密钥,移位数为10char[] chars = plainText.ToCharArray();foreach (char ch in chars){int temp = Convert.ToInt32(ch);//小写字母移位if (ch >= 'a' && ch <= 'z'){temp += key%26;if (temp > 122) temp -= 26;if (temp < 97) temp += 26;}//大写字母移位if (ch >= 'A' && ch <= 'Z'){temp += key%26;if (temp > 90) temp -= 26;if (temp < 65) temp += 26;}sb.Append((char) temp));}Console.WriteLine(sb);return sb.ToString();
}
如果不增加新的外观类,只能通过修改原有外观类EncryptFacade
的源代码来实现加密类的更换,将原有的对CipherMachine
类型对象的引用改为对NewCipherMachine
类型对象的引用,这违背了 开闭原则,因此需要通过增加新的外观类来实现对子系统对象引用的改变。
如果增加一个新的外观类NewEncryptFacade
来与FileReader
、FileWriter
以及新增加的NewCipherMachine
类进行交互,虽然原有系统类库无须做任何修改,但是因为客户端代码中原来针对EncryptFacade
类进行编程,现在需要改为NewEncryptFacade
类,因此需要修改客户端源代码。
如何在不修改客户端代码的前提下使用新的外观类呢?

图11 引入抽象外观类之后的文件加密模块结构图
(1)抽象外观类
public abstract class AbstractEncryptFacade
{public abstract void FileEncrypt(string fileNameSrc, string fileNameDes);
}
(2)新增加的实体外观类
public class NewEncryptFacade : AbstractEncryptFacade
{private readonly FileReader _reader;private readonly NewCipherMachine _cipher;private readonly FileWriter _writer;public NewEncryptFacade(){_reader = new FileReader();_cipher = new NewCipherMachine();_writer = new FileWriter();}public override void FileEncrypt(string fileNameSrc, string fileNameDes){string plainStr = _reader.Read(fileNameSrc);if (string.IsNullOrEmpty(plainStr)){return;}string encryptStr = _cipher.Encrypt(plainStr);_writer.Write(encryptStr, fileNameDes);}
}
配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="facade" value="SunnyFacade.NewEncryptFacade"/></appSettings>
</configuration>
客户端
using System.Configuration;
using System.Reflection;class Program
{static void Main(string[] args){string facadeString = ConfigurationManager. AppSettings["facade"];AbstractEncryptFacade ef = Assembly.Load("SunnyFacade").CreateInstance(facadeString) as AbstractEncryptFacade;if (ef != null)ef.FileEncrypt("src.txt", "des.txt");}
}
输出结果如下:

图12 输出结果
原有外观类EncryptFacade
也需作为抽象外观类AbstractEncryptFacade
类的子类,更换具体外观类时只需修改配置文件,无须修改源代码,符合 开闭原则。
适配器模式 – 不兼容结构的协调
Sunny 软件公司在很久以前曾开发了一个算法库,里面包含了一些常用的算法,例如排序算法和查找算法,在进行各类软件开发时经常需要重用该算法库中的算法。
在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找,该系统的设计人员已经开发了一个成绩操作接口IScoreOperation
,在该接口中声明了排序方法Sort(int[])
和查找方法Search(int[], int)
,为了提高排序和查找的效率,开发人员决定重用算法库中的快速排序算法类QuickSort
和二分查找算法类BinarySearch
,其中QuickSort
的QuickExchangeSort(int[])
方法实现了快速排序,BinarySearch
的BinSearch (int[], int)
方法实现了二分查找。
由于某些原因,现在 Sunny 公司开发人员已经找不到该算法库的源代码,无法直接通过复制和粘贴操作来重用其中的代码;部分开发人员已经针对IScoreOperation
接口编程,如果再要求对该接口进行修改或要求大家直接使用QuickSort
类和BinarySearch
类将导致大量代码需要修改。
Sunny 软件公司开发人员面对这个没有源码的算法库,遇到一个幸福而又烦恼的问题:如何在既不修改现有接口又不需要任何算法库代码的基础上能够实现算法库的重用?

图13 需协调的两个系统的结构示意图
现在我们需要IScoreOperation
接口能够和已有算法库一起工作,让它们在同一个系统中能够兼容,最好的实现方法是增加一个适配器角色,通过适配器来协调这两个原本不兼容的结构。

图14 算法库重用结构图
IScoreOperation
接口充当抽象目标,QuickSort
和BinarySearch
类充当适配者,OperationAdapter
充当适配器。
(1)抽象成绩操作类:目标接口
public interface IScoreOperation
{void Sort(int[] array);int Search(int[] array, int key);
}
(2)快速排序类:适配者
public class QuickSort
{public void QuickExchangeSort<T>(T[] array) where T : IComparable<T>{QuickExchangeSort(array, 0, array.Length - 1);}private void QuickExchangeSort<T>(T[] array, int left, int right) where T : IComparable<T>{if (left < right){T current = array[left];int i = left;int j = right;while (i < j){while (array[j].CompareTo(current) > 0 && i < j)j--;while (array[i].CompareTo(current) <= 0 && i < j)i++;if (i < j){T temp = array[i];array[i] = array[j];array[j] = temp;j--;i++;}}array[left] = array[j];array[j] = current;if (left < j - 1) QuickExchangeSort(array, left, j - 1);if (right > j + 1) QuickExchangeSort(array, j + 1, right);}}
}
(3)二分查找类:适配者
public class BinarySearch
{public int BinSearch<T>(T[] array, T key)where T : IComparable<T>{int left = 0;int right = array.Length - 1;while (left <= right){int mid = (left + right)/2;if (array[mid].CompareTo(key) < 0)left = mid + 1;else if (array[mid].CompareTo(key) == 0)return mid;elseright = mid - 1;}return -1;}
}
(4)操作适配器:适配器
public class OperationAdapter : IScoreOperation
{private readonly QuickSort _sortObj;private readonly BinarySearch _searchObj;public OperationAdapter(){_sortObj = new QuickSort();_searchObj = new BinarySearch();}public void Sort(int[] array){_sortObj.QuickExchangeSort<int>(array);}public int Search(int[] array, int key){return _searchObj.BinSearch<int>(array, key);}
}
(5)配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="Adapter" value="SunnyAdapter.OperationAdapter"/></appSettings>
</configuration>
(6)客户端代码
using System.Configuration;
using System.Reflection;class Program
{static void Main(string[] args){Assembly assembly = Assembly.Load("SunnyAdapter");IScoreOperation scoureOperation = assembly.CreateInstance(ConfigurationManager.AppSettings["Adapter"])as IScoreOperation;if (scoureOperation == null)return;int[] scores = { 84, 76, 50, 69, 90, 91, 88, 96 };Console.WriteLine("成绩排序结果:");scoureOperation.Sort(scores); for (int i = 0; i < scores.Length; i++){Console.Write(scores[i]+",");}Console.WriteLine();Console.WriteLine("查找成绩90:");int score = scoureOperation.Search(scores, 90);if (score == -1)Console.WriteLine("没有找到成绩90。");elseConsole.WriteLine("找到成绩90。");Console.WriteLine("查找成绩92:");score = scoureOperation.Search(scores, 92);if (score == -1)Console.WriteLine("没有找到成绩92。");elseConsole.WriteLine("找到成绩92。");}
}

图15 运行结果
如果需要使用其它排序算法类和查找算法类,可以增加一个新的适配器类,使用新的适配器来适配新的算法,原有代码无须修改。
通过引入配置文件和反射机制,可以在不修改客户端代码的情况下使用新的适配器,无须修改源代码,符合“开闭原则”。
适配器模式(Adpter Pattern)
将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
在 适配器模式 中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。从而解决了接口不兼容的问题,使得原本没有任何关系的类可以协同工作。
根据适配器类与适配者类的关系不同,适配器模式可分为:
- 对象适配器(适配器与适配者之间是关联关系)
- 类适配器(适配器与适配者之间是继承或实现关系)

图16 对象适配器模式结构图
(1)目标接口
public interface ITarget
{void Request();
}
(2)适配者类
public class Adaptee
{public void SpecificRequest(){;}
}
(3)适配器类
public class Adapter : ITarget
{private readonly Adaptee _adaptee;public Adapter(Adaptee adaptee){_adaptee = adaptee;}public void Request(){_adaptee.SpecificRequest();}
}
在对象适配器中,客户端需要调用Request()
方法,而适配者类Adaptee
没有该方法,但是它所提供的SpecificRequest()
方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter
,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的Request()
方法中调用适配者的SpecificRequest()
方法。

图17 类适配器模式结构图
public class Adapter : Adaptee, ITarget
{public void Request(){base.SpecificRequest();}
}
适配器模式包含以下 3 个角色:
ITarget
(目标接口):定义客户所需接口。Adaptee
(适配者类):即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源码。Adapter
(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee
和ITarget
进行适配,它通过实现ITarget
并 关联/继承 一个Adaptee 对象/类 使二者产生联系。
由于Java、C#等语言不支持多重类继承,因此类适配器的使用受到很多限制,例如如果目标ITarget不是接口,而是一个类,就无法使用类适配器;此外,如果适配者Adaptee
为sealed
类,也无法使用类适配器。在C#等面向对象编程语言中,大部分情况下我们使用的是对象适配器,类适配器较少使用。
桥接模式 – 处理多维度变化
Sunny 软件公司欲开发一个跨平台图像浏览系统,要求该系统能够显示 BMP、JPG、GIF、PNG 等多种格式的文件,并且能够在 Windows、Linux、Unix 等多个操作系统上运行。
系统首先将各种格式的文件解析为像素矩阵(Matrix
),然后将像素矩阵显示在屏幕上,在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。
系统需具有较好的扩展性以支持新的文件格式和操作系统。
Sunny 软件公司的开发人员针对上述要求,提出了一个初始设计方案,其基本结构如下图所示:

图18 跨平台图像浏览器初始结构图
在上图的初始设计方案中,使用了一种多层继承结构。
Image
是抽象父类,而每一种类型的图像类作为其直接子类,不同的图像文件格式具有不同的解析方法,可以得到不同的像素矩阵;- 由于每一种图像又需要在不同的操作系统中显示,不同的操作系统在屏幕上显示像素矩阵有所差异,因此需要为不同的图像类再提供一组在不同操作系统显示的子类。
对该设计方案进行分析,发现存在如下两个主要问题:
【1】由于采用了多层继承结构,导致系统中类的个数急剧增加,在各种图像的操作系统实现层提供了12个实体类,加上各级抽象层的类,系统中类的总个数达到了17个,在该设计方案中,实体层的类的个数 = 所支持的图像文件格式数 × 所支持的操作系统数
。
【2】系统扩展麻烦,由于每一个实体类既包含图像文件格式信息,又包含操作系统信息,因此无论是增加新的图像文件格式还是增加新的操作系统,都需要增加大量的实体类。
如何解决这两个问题?
谈谈两种常见文具 “毛笔” 和 “蜡笔” 的区别。
假如我们需要大中小 3 种型号的画笔,能够绘制 12 种不同的颜色。如果使用蜡笔,需要准备 36支,如果使用毛笔,只需要提供 3 种型号的毛笔,外加 12 个颜料盒即可,涉及到的对象个数为 15,远小于36,却能实现与36支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有 12 种颜色,对应的蜡笔需增加 12支,而毛笔只需增加 1支。
为什么会这样呢?
通过分析可知:
- 蜡笔:颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,无论是对颜色还是对型号进行扩展都势必会影响另一个维度;
- 毛笔:颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。
如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,而毛笔很好地将二者解耦,使用起来非常灵活,扩展也更为方便。
我们通过分析得知,该系统也存在两个独立变化的维度:
- 图像文件格式(对应图像格式的解析)
- 操作系统(对应像素矩阵的显示)

图19 跨平台图像浏览器中存在的两个独立变化维度示意图
为了减少所需生成的子类数目,将这两个维度分离,使得它们可以独立变化,增加新的图像文件格式或者操作系统时都对另一个维度不造成任何影响。
Sunny 公司开发人员重构了系统的设计,如下图所示:

图20 跨平台图像浏览器重构后的结构图
完整代码如下:
//各种格式的文件最终都被转化为像素矩阵,
//不同的操作系统提供不同的方式显示像素矩阵。
public class Matrix
{//...
}
“实现类”层次结构:
public abstract class ImageImp
{public abstract void DoPaint(Matrix m);
}public class LinuxImp : ImageImp
{public override void DoPaint(Matrix m){Console.WriteLine("在Linux操作系统中显示图像。");}
}public class UnixImp : ImageImp
{public override void DoPaint(Matrix m){Console.WriteLine("在Unix操作系统中显示图像。");}
}public class WindowsImp : ImageImp
{public override void DoPaint(Matrix m){Console.WriteLine("在Windows操作系统中显示图像。");}
}
“抽象类”层次结构:
public abstract class Image
{protected ImageImp Imp;public void SetImageImp(ImageImp imp){this.Imp = imp;}public abstract void ParseFile(string fileName);
}public class BmpImage : Image
{public override void ParseFile(string fileName){Matrix m = new Matrix();Imp.DoPaint(m);Console.WriteLine(fileName + ",格式为BMP。");}
}public class GifImage : Image
{public override void ParseFile(string fileName){Matrix m = new Matrix();Imp.DoPaint(m);Console.WriteLine(fileName + ",格式为GIF。");}
}public class JpgImage:Image
{public override void ParseFile(string fileName){Matrix m = new Matrix();Imp.DoPaint(m);Console.WriteLine(fileName+ ",格式为JPG。");}
}public class PngImage : Image
{public override void ParseFile(string fileName){Matrix m = new Matrix();Imp.DoPaint(m);Console.WriteLine(fileName + ",格式为PNG。");}
}
配置文件代码:
<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="image" value="SunnyBridge.BmpImage"/><add key="os" value="SunnyBridge.LinuxImp"/></appSettings>
</configuration>
客户端代码:
using System.Reflection;
using System.Configuration;
class Program
{static void Main(string[] args){string image = ConfigurationManager.AppSettings["image"];string os = ConfigurationManager.AppSettings["os"];Assembly assembly = Assembly.Load("SunnyBridge");Image img = assembly.CreateInstance(image) as Image;ImageImp imageImp = assembly.CreateInstance(os) as ImageImp;if (img != null){img.SetImageImp(imageImp);img.ParseFile("光头强");}}
}
输出结果如下图所示:

图21 运行结果
如果需要更换图像文件格式或者更换操作系统,只需修改配置文件即可,在实际使用时,可以通过分析图像文件格式后缀名来确定具体的文件格式,在程序运行时获取操作系统信息来确定操作系统类型,无须使用配置文件。
当增加新的图像文件格式或者操作系统时,原有系统无需做任何修改,只需增加一个对应的实现类即可,系统具有较好的可扩展性,完全符合“开闭原则”。
桥接模式与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
桥接模式(Bridge Pattern):
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 用 抽象关联 取代了传统的 多继承;
- 将类之间的 静态继承关系 转换为 动态的对象组合关系;
桥接模式 结构如下图所示:

图22 桥接模式类图
Abstraction
(抽象类):定义了一个Implementor
类型的对象并可以维护该对象,它与Implementor
之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。RefinedAbstraction
(扩充抽象类):实现了在Abstraction
中声明的抽象业务方法,在RefinedAbstraction
中可以调用在Implementor
中定义的业务方法。Implementor
(实现类接口):通过关联关系,在Abstraction
中不仅拥有自己的方法,还可以调用到Implementor
中定义的方法,使用关联关系来替代继承关系。ConcreteImplementor
(实体实现类):在程序运行时,ConcreteImplementor
对象将替换其父类对象,提供给抽象类实体的业务操作方法。
通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。
对于毛笔而言:
- 型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;
- 颜色是其另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。
结构示意图如下图所示:

图23 桥接模式类图
桥接模式 中体现了“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等设计原则。熟悉该模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。
适配器模式与桥接模式的联用
- 桥接模式:用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;
- 适配器模式:初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式,解决两个已有接口间不兼容问题;
某系统的报表处理模块中,需要将报表显示和数据采集分开,系统可以有多种报表显示方式也可以有多种数据采集方式,如可以从文本文件中读取数据,也可以从数据库中读取数据,还可以从Excel文件中获取数据。如果需要从Excel文件中获取数据,则需要调用与Excel相关的API,而这个API是现有系统所不具备的,该API由厂商提供。
在设计过程中,由于存在报表显示和数据采集两个独立变化的维度,因此可以使用 桥接模式 进行初步设计;为了使用Excel相关的API来进行数据采集则需要使用适配器模式。系统的完整设计中需要将两个模式联用,如下图所示:

图24 桥接模式与适配器模式联用示意图
相关文章:

Linux抓包工具tcpdump详解
原文链接 tcpdump是一个用于截取网络分组,并输出分组内容的工具,简单说就是数据包抓包工具。tcpdump凭借强大的功能和灵活的截取策略,使其成为Linux系统下用于网络分析和问题排查的首选工具。 tcpdump提供了源代码,公开了接口&…

学习笔记TF065:TensorFlowOnSpark
2019独角兽企业重金招聘Python工程师标准>>> Hadoop生态大数据系统分为Yam、 HDFS、MapReduce计算框架。TensorFlow分布式相当于MapReduce计算框架,Kubernetes相当于Yam调度系统。TensorFlowOnSpark,利用远程直接内存访问(Remote Direct Memo…

HTML5培训好不好
HTML5培训好不好?这个问题,要看你选择的培训机构,想要学习HTML5技术,靠谱的培训机构非常重要,下面我们就来看看详细的介绍吧。 HTML5培训好不好?从前端开发的基础出发,学习使用HTML,CSS,JavaS…

技术图文:03 结构型设计模式(下)
结构型设计模式(下) 本教程主要介绍一系列用于如何将现有类或对象组合在一起形成更加强大结构的经验总结。 知识结构: 组合模式 – 树形结构的处理 Sunny 软件公司欲开发一个杀毒(AntiVirus)软件,该软件…

程序员必知8大排序3大查找(三)
前两篇 《程序员必知8大排序3大查找(一)》 《程序员必知8大排序3大查找(二)》 三种查找算法:顺序查找,二分法查找(折半查找),分块查找,散列表(以后谈…

MongoDB给数据库创建用户
转自http://www.imooc.com/article/18439 一.先以非授权的模式启动MongoDB非授权: linux/Mac : mongod -f /mongodb/etc/mongo.confwindows : mongod --config c:\mongodb\etc\mongo.conf 或者 net start mongodb (前提是mongo安装到了服务里面ÿ…

如何挑选一家好的软件测试培训机构
随着智能时代的发展,我们的手机APP等各种软件都变得越来越复杂化、规模化,软件测试这一步骤是必不可少的,这也造就了这个行业的兴起,越来越多的人想要学习软件测试技术,想要知道如何挑选一家好的软件测试培训机构?来看…

POJ 3177 判决素数个数
时间限制: 1000ms内存限制:65536kB描述输入两个整数X和Y,输出两者之间的素数个数(包括X和Y)。输入两个整数X和Y,X和Y的大小任意。输出输出一个整数,结果可以是0,或大于0的整数。样例输入1 100样例输出25&am…

数据结构与算法:22 精选练习50
精选练习50 马上就要期末考试或者考研了。为了大家复习的方便,我精选了有关数据结构与算法的50道选择题,大家可以抽空练习一下。公众号后台回复“答案”可以获取该50道题目的答案。 01、数据在计算机中的表示称为数据的______。 (A&#x…

极速理解设计模式系列:11.单例模式(Singleton Pattern)
单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。 三要点: 一、单例类只能有一个实例 二、单例类必须自行创建自身实例 三、单例类自行向整个系统提供实例 类图: 应用场景…

参加web前端培训要学哪些知识
IT行业,web前端技术是比较吃香的,也是工资待遇非常高的行业之一,如果想要做一名合格的web前端工程师,系统学习是非常重要的,那么参加web前端培训要学哪些知识呢?来看看下面的详细介绍。 参加web前端培训要学哪些知识?…

数据结构与算法:19 排序
19 排序 知识结构: 1. 排序的基本概念与术语 假设含有nnn个记录的序列为{r1,r2,⋯,rn}\lbrace r_1,r_2,\cdots,r_n \rbrace{r1,r2,⋯,rn},其相应的关键字分别为{k1,k2,⋯,kn}\lbrace k_1,k_2,\cdots,k_n \rbrace{k1,k2,⋯,kn},…

Objective-C 什么是类
Objective-C 什么是类 转自http://www.189works.com/article-31219-1.html 之前一直做C开发,最近2个多月转 Objective-C, 入门的时候,遇到了很多的困惑。现在过节,正是解决他们的好时机。 主要参考来自http://www.sealiesoftware.…

APP之红点提醒三个阶段
下面这个页面就是我们进入APP后的主界面。客户选项的红点上数字就是显示我们没有查看的客户总数量。 当我们切换到客户这个fragment时,会显示贷款客户数量与保险客户数量。 当我们随便点击入一个选项,假如进入到保险客户的这个activity里面,L…

零基础参加java培训的系统学习路线
零基础想要学习java技术,那么最好的选择就是参加java培训,进行系统的学习,以下就是小编为大家整理的零基础参加java培训的系统学习路线,希望能够帮助到正在学习java技术的零基础同学。 零基础参加java培训的系统学习路线&#…

在ASP.NET中跟踪和恢复大文件下载
在Web应用程序中处理大文件下载的问题一直出了名的困难,因此对于大多数站点来说,如果用户的下载被中断了,它们只能说悲哀降临到用户的身上了。但是我们现在不必这样了,因为你可以使自己的ASP.NET应用程序有能力支持可恢复…

ZeroMQ实例-使用ZeroMQ进行windows与linux之间的通信
1、本文包括 1)在windows下使用ZMQ 2)在windows环境下与Linux环境下进行网络通信 2、在Linux下使用ZMQ 之前写过一篇如何在Linux环境下使用ZMQ的文章 《ZeroMQ实例-使用ZMQ(ZeroMQ)进行局域网内网络通信》,这里就不再赘述。 3、在Windows环境…

线性代数:03 向量空间 -- 基本概念
本讲义是自己上课所用幻灯片,里面没有详细的推导过程(笔者板书推导)只以大纲的方式来展示课上的内容,以方便大家下来复习。 本章主要介绍向量空间的知识,与前两章一样本章也可以通过研究解线性方程组的解把所有知识点…

如何获得PMP认证证书
pmp证书是一项由美国项目管理协会发起的项目管理专业人士认证证书,它属于国际认证类证书,含金量是非常高的,那么如何获得PMP认证证书呢?来看看下面的详细介绍。 如何获得PMP证书? PMP证书的获取是需要参加PMP考试的。我国自1999年引进PM…

UITextField的详细使用
UItextField通常用于外部数据输入,以实现人机交互。下面以一个简单的登陆界面来讲解UItextField的详细使用。//用来显示“用户名”的labelUILabel* label1 [[UILabelalloc] initWithFrame:CGRectMake(15, 65, 70, 30)];label1.backgroundCol…

06-hibernate注解-一对多单向外键关联
一对多单向外键 1,一方持有多方的集合,一个班级有多个学生(一对多)。 2,OneToMany(cascade{CascadeType.ALL}, fetchFetchType.LAZY ) //级联关系,抓取策略:懒加载。 JoinColumn(name"c…

线性代数:03 向量空间 -- 矩阵的零空间,列空间,线性方程组解的结构
本讲义是自己上课所用幻灯片,里面没有详细的推导过程(笔者板书推导)只以大纲的方式来展示课上的内容,以方便大家下来复习。 本章主要介绍向量空间的知识,与前两章一样本章也可以通过研究解线性方程组的解把所有知识点…

学Python培训有什么用
Python在近几年的发展非常迅速,在互联网行业Python的薪资也越来越高,不少人开始准备学习Python技术,那么到底学Python培训有什么用呢?来看看下面的详细介绍。 学Python培训有什么用? 学习python可以提高工作效率,使用python&…

SQL压力测试用的语句和相关计数器
将数据库中所有表的所有的内容选一遍: IF object_id(tempdb..#temp) is not null BEGIN DROP TABLE #temp END DECLARE index int DECLARE count int DECLARE schemaname varchar(50) DECLARE tablename varchar(50) set index1 set count(select count(*) from s…

线性代数:04 特征值与特征向量 -- 特征值与特征向量
本讲义是自己上课所用幻灯片,里面没有详细的推导过程(笔者板书推导)只以大纲的方式来展示课上的内容,以方便大家下来复习。 本章主要介绍特征值与特征向量的知识,前一章我们介绍了线性变换可以把一个向量映射到另一个…

使用Silverlight2的WebClient下载远程图片
在Silverlight 2之前有一个Downloader对象,开发者一般使用Downloader下载图片和文体文件,这个对象在Silverlight 2中作为了一个特性被集成到WebClient类之中,你可以直接使用WebClient的OpenReadAsync方法加载远程图片的URI,然后使…

学习Web前端需要避免哪些错误
很多初学web前端的同学,在学习web前端的时候都会遇到一些错误,虽然有些错误与某一个具体的行为相关,但有些错误却是所有Web开发人员都需要面对的挑战。下面小编就整理一下学习Web前端需要避免哪些错误,希望能够给同学们带来帮助。…

【2012百度之星/资格赛】H:用户请求中的品牌 [后缀数组]
时间限制:1000ms内存限制:65536kB描述馅饼同学是一个在百度工作,做用户请求(query)分析的同学,他在用户请求中经常会遇到一些很奇葩的词汇。在比方说“johnsonjohnson”、“duckduck”,这些词汇虽然看起来是一些词汇的…

实战:使用Telnet排除网络故障
使用Telnet排除网络故障 如果员工告诉你,他的计算机不能访问网站。你需要断定是他的计算机系统出了问题还是IE浏览器中了恶意插件,或者是网络层面的问题。 如图2-108所示,通过Telnet 服务器的某个端口,就能断定是否访问该服务器的…

线性代数:04 特征值与特征向量 -- 矩阵的相似对角化
本讲义是自己上课所用幻灯片,里面没有详细的推导过程(笔者板书推导)只以大纲的方式来展示课上的内容,以方便大家下来复习。 本章主要介绍特征值与特征向量的知识,前一章我们介绍了线性变换可以把一个向量映射到另一个…