Asp.net设计模式笔记之一:理解设计模式
GOF设计模式著作中的23种设计模式可以分成三组:创建型(Creational),结构型(Structural),行为型(Behavioral)。下面来做详细的剖析。
创建型
创建型模式处理对象构造和引用。他们将对象实例的实例化责任从客户代码中抽象出来,从而让代码保持松散耦合,将创建复杂对象的责任放在一个地方,这遵循了单一责任原则和分离关注点原则。
下面是“创建型”分组中的模式:
1.Abstract Factory(抽象工厂)模式:提供一个接口来创建一组相关的对象。
2.Factory Method(工厂方法)模式:支持使用一个类来委托创建有效对象的责任。
3.Builder(生成器)模式:将对象本身的构造分离出来,从而能够构造对象的不同版本。
4.Prototype(原型)模式:能够从一个原型实例来复制或克隆类,而不是创建新实例。
5.Singleton(单例)模式:支持一个类只实例化一次,并只有一个可用来访问它的全局访问点。
结构型
结构型模式处理对象的组合与关系,以满足大型系统的需要。
下面是“结构型”分组中的模式:
1.Adapter(适配器)模式:使不兼容接口的类能够一起使用。
2.Bridge(桥接)模式:将抽象与其实现分离,允许实现和抽象彼此独立地改变。
3.Composite(组合)模式:可以像对待对象的单个实例那样来对待一组表示层次结构的对象。
4.Decorator(装饰)模式:能够动态包装一个类并扩展其行为。
5.Facade(门面)模式:提供一个简单的接口并控制对一组复杂接口和子系统的访问。
6.Flyweight(享元)模式:提供一种在许多小类之间高效共享数据的方式。
7.Proxy(代理)模式:为一个实例化成本很高的更复杂的类提供一个占位符。
行为型
行为型模式处理对象之间在责任和算法方面的通信。这个分组中的模式将复杂行为封装起来并将其从系统控制流中抽象出来,这样就使复杂系统更容易理解和维护。
下面是”行为型“分组中的模式:
1.Chain Of Responsibility(责任链)模式:允许将命令动态链接起来处理请求。
2.Command(命令)模式:将一个方法封装成一个对象,并将该命令的执行与它的调用者分离。
3.Interpreter(解释器)模式:指定如何执行某种语言中的语句。
4.Iterator(迭代器)模式:提供以形式化的方式来导航集合的方法。
5.Mediator(中介者)模式:定义一个对象,可以让其他两个对象进行通信而不必让它们知道彼此。
6.Memento(备忘录)模式:允许将对象恢复到以前的状态。
7.Observer(观察者)模式:定义一个或多个类在另一个类发生变化时接到报警。
8.State(状态)模式:允许对象通过委托给独立的,可改变的状态对象来改变自己的行为。
9.Strategy(策略)模式:能够将算法封装到一个类中并在运行时转换,以改变对象的行为。
10.Template Method(模板方法)模式:定义算法流程控制,但允许子类重写或实现执行步骤。
11.Vistor(访问者)模式:能够在类上执行新的功能而不影响类的结构。
上面介绍了众多的设计模式及其分组。但是如何来选择和运用呢?下面有一些需要注意的事项:
1.在不了解模式的情况下不能运用他们。
2.在设计的时候,要衡量是否有必要引入设计模式的复杂性。最好能衡量下实现某种模式所需的时间与该模式能够带来的效益。谨记KISS原则:保持简单浅显。
3.将问题泛化,以更抽象的方式识别正在处理的问题。设计模式是高层次的解决方案,试着把问题抽象,而且不要过于关注具体问题的细节。
4.了解具有类似性质的模式以及同组中的其他模式。以前已经使用过某个模式并不意味着在解决问题时它总是正确的模式选择。
5.封装变化的部分。了解应用程序中什么可能发生变化。如果知道某个特殊的报价折扣算法将随时间发生变化,那么寻找一种模式来帮助您在不影响应用程序其余部分的情况下改变该算法。
6.在选择好设计模式之后,确保在命名解决方案中的参与者时使用该模式的语言及领域语言。例如,如果正在使用策略模式为不同的快递公司计价提供解决方案,那么相应地为他们明明,如FedExShippingCostStrategy。通过组合使用模式的公共词汇表和领域语言,会让代码更具可读性,而且更能够让其他具备模式知识的开发者理解。
就设计模式而言,除了学习之外没有其他替代方法。对每种设计模式了解得越多,在运用他们时就会准备的更好。当遇到一个问题正在寻找解决方案时,扫描一下每种模式的目的,唤起自己的记忆。
一种很好地学习方法就是试着识别.net框架中的模式,比如:Asp.net Cache使用了Singleton模式,在创建新的Guid实例时使用了Factory Method模式,.Net 2 xml类使用Factory Method模式,而1.0版并没有使用。
下面我们以一个快速模式示例来进行讲解,以便于加深映像。
新建一个类库项目0617.DaemonPattern.Service,然后引用System.web程序集。
首先添加一个Product.cs的空类作为我们的Model:
1
2
3
4
|
public class Product { } |
然后添加ProductRepository.cs类作为我们的数据存储仓库,从这里我们可以从数据库获取数据实体对象:
1
2
3
4
5
6
7
8
9
|
public class ProductRepository { public IList<Product> GetAllProductsIn( int categoryId) { var products = new List<Product>(); //Database operation to populate products. return products; } } |
最后添加一个名称为ProductService.cs的类,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class ProductService { public ProductService() { this .productRepository = new ProductRepository(); } private ProductRepository productRepository; public IList<Product> GetAllProductsIn( int categoryId) { IList<Product> products; string storageKey = string .Format( "products_in_category_id_{0}" , categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(storageKey); if (products == null ) { products = productRepository.GetAllProductsIn(categoryId); HttpContext.Current.Cache.Insert(storageKey, products); } return products; } } |
从代码的逻辑,我们可以清楚的看到,ProductService通过ProductRepository仓库从数据库获取数据。
这个类库带来的问题有以下几点:
1.ProductService依赖于ProductRepository类。如果ProductRepository类中的API发生改变,就需要在ProductService类中进行修改。
2.代码不可测试。如果不让真正的ProductRepository类连接到真正的数据库,就不能测试ProductService的方法,因为这两个类之间存在着紧密耦合。另一个与测试有关的问题是,该代码依赖于使用Http上下文来缓存商品。很难测试这种与Http上下文紧密耦合的代码。
3.被迫使用Http上下文来缓存。在当前状态,若使用Velocity或Memcached之类的缓存存储提供者,则需要修改ProductService类以及所有其他使用缓存的类。Verlocity和Memcached都是分布式内存对象缓存系统,可以用来替代Asp.net的默认缓存机制。
随意,综上看来,代码耦合度过高,不易进行测试,同时也不易进行替换。
既然知道了存在的问题,那么就让我们来对其进行重构。
首先,考虑到ProductService类依赖于ProductRepository类的问题。在当前状态中,ProductService类非常脆弱,如果ProductRepository类的API改变,就需要修改ProductService类。这破坏了分离关注点和单一职责原则。
1.依赖倒置原则(依赖抽象而不要依赖具体)
可以通过依赖倒置原则来解耦ProductService类和ProductRepository类,让它们都依赖于抽象:接口。
在ProductRepository类上面右击,选择“重构”->“提取接口”选项,会自动给我们生成一个IProductRepository.cs类:
1
2
3
4
|
public interface IProductRepository { IList<Product> GetAllProductsIn( int categoryId); } |
修改现有的ProductRepository类,以实现新创建的接口,代码如下:
1
2
3
4
5
6
7
8
9
|
public class ProductRepository : IProductRepository { public IList<Product> GetAllProductsIn( int categoryId) { var products = new List<Product>(); //Database operation to populate products. return products; } } |
之后更新ProductService类,以确保它引用的是接口而非具体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class ProductService { public ProductService() { this .productRepository = new ProductRepository(); } private IProductRepository productRepository; public IList<Product> GetAllProductsIn( int categoryId) { IList<Product> products; string storageKey = string .Format( "products_in_category_id_{0}" , categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(storageKey); if (products == null ) { products = productRepository.GetAllProductsIn(categoryId); HttpContext.Current.Cache.Insert(storageKey, products); } return products; } } |
这样修改之后,ProductService类现在只依赖于抽象而不是具体的实现,这意味着ProductService类完全不知道任何实现,从而确保它不是那么容易的被破坏掉,而且代码在整体上说来对变化更有弹性。
但是,这里还有个问题,既是ProductService类仍然负责创建具体的实现。而且目前在没有有效的ProductRepository类的情况下不可能测试代码。所以这里我们需要引入另一个设计原则来解决这个问题:依赖注入原则。
由于ProductService类仍然与ProductRepository的具体实现绑定在了一起,通过依赖注入原则,我们可以将这一过程移到外部进行,具体方法就是通过该类的构造器将其注入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class ProductService { public ProductService(IProductRepository productRepository) { this .productRepository = productRepository; } private IProductRepository productRepository; public IList<Product> GetAllProductsIn( int categoryId) { IList<Product> products; string storageKey = string .Format( "products_in_category_id_{0}" , categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(storageKey); if (products == null ) { products = productRepository.GetAllProductsIn(categoryId); HttpContext.Current.Cache.Insert(storageKey, products); } return products; } } |
这样就可以在测试期间向ProductService类传递替代者,从而能够孤立地测试ProductService类。通过把获取依赖的责任从ProductService类中移除,能够确保ProductService类遵循单一职责原则:它现在只关心如何协调从缓存或资源库中检索数据,而不是创建具体的IProductRepository实现。
依赖注入有三种形式:构造器,方法以及属性。我们这里只是使用了构造器注入。
当然,现在的代码看上去基本没问题了,但是一旦替换缓存机制的话,将会是一个比较棘手的问题,因为基于Http上下文的缓存没有被封装,替换其需要对当前类进行修改。这破坏了开放封闭原则:对扩展开放,对修改关闭。
由于Adapter(适配器)模式主要用来将一个类的某个转换成一个兼容的接口,所以在当前的例子中,我们可以将HttpContext缓存API修改成想要使用的兼容API。然后可以使用依赖注入原则,通过一个接口将缓存API注入到ProductService类。
这里我们创建一个名为ICacheStorage的新街口,它包含有如下契约:
1
2
3
4
5
6
|
public interface ICacheStorage { void Remove( string key); void Store( string key, object data); T Retrieve<T>( string key); } |
在ProductService类中,我们就可以将其取代基于HttpContext的缓存实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class ProductService { public ProductService(IProductRepository productRepository,ICacheStorage cacheStroage) { this .productRepository = productRepository; this .cacheStroage = cacheStroage; } private IProductRepository productRepository; private ICacheStorage cacheStroage; public IList<Product> GetAllProductsIn( int categoryId) { IList<Product> products; string storageKey = string .Format( "products_in_category_id_{0}" , categoryId); products = cacheStroage.Retrieve<List<Product>>(storageKey); if (products == null ) { products = productRepository.GetAllProductsIn(categoryId); cacheStroage.Store(storageKey, products); } return products; } } |
而具体的缓存类我们可以继承自ICacheStorage来实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class HttpCacheAdapterStorage:ICacheStorage { public void Remove( string key) { if (HttpContext.Current.Cache[key] != null ) HttpContext.Current.Cache.Remove(key); } public void Store( string key, object data) { if (HttpContext.Current.Cache[key] != null ) HttpContext.Current.Cache.Remove(key); HttpContext.Current.Cache.Insert(key,data); } public T Retrieve<T>( string key) { if (HttpContext.Current.Cache[key]!= null ) return (T)HttpContext.Current.Cache[key]; return default (T); } } |
现在再回头看看,我们解决了开始列举的种种问题,使得代码更加容易测试,更易读,更易懂。
下面是Adapter(适配器)模式的UML图示:
从图中可以看出,客户有一个对抽象(Target)的引用。在这里,该抽象就是ICacheStorage接口。Adapter是Target接口的一个实现,它只是将Operation方法委托给Adaptee类,这里的Adapter类就是指我们的HttpCacheStorage类,而Adaptee类则是指HttpContext.Current.Cache提供的具体操作方法。
具体的描述如下:
这样,当我们切换到Memcached,抑或是MS Velocity的时候,只需要创建一个Adapter,让ProductService类与该缓存存储提供者通过公共的ICacheStorage接口交互即可。
从这里我们知道:
Adapter模式非常简单,它唯一的作用就是让具有不兼容接口的类能够在一起工作。
由于Adapter模式并不是唯一能够帮助处理缓存数据的模式,下面的章节将会研究Proxy设计模式如何来帮助解决缓存问题的。
在这里,我们还有最后一个问题没有解决,就是在当前设计中,为了使用ProductService类,总是不得不为构造器提供ICacheStorage实现,但是如果不希望缓存数据呢? 一种做法是提供一个null引用,但是这意味着需要检查空的ICacheStorage实现从而弄乱代码,更好的方式则是使用NullObject模式来处理这种特殊情况。
Null Object(空对象模式,有时也被称为特殊情况模式)也是一种极为简单的模式。当不希望指定或不能指定某个类的有效实例而且不希望到处传递null引用时,这个模式就有用武之地。Null对象的作用是代替null引用并实现相同的接口但是没有行为。
如果不希望ProductService类中缓存数据,Null Object模式可以派上用场:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class NullObjectCache:ICacheStorage { public void Remove( string key) { } public void Store( string key, object data) { } public T Retrieve<T>( string key) { return default (T); } } |
这样,当我们请求缓存数据的时候,它什么都不做而且总是向ProductService返回null值,确保不会缓存任何数据。
最后,总结一下:
三种设计模式分组。
依赖注入原则。
Adapter模式具体应用。
Null Object模式用于处理空对象。