设计模式入门
本内容将研究如何使用模式和设计原则来改进一段简单的asp.net 代码。其实,这段代码比较常见,或者已经看过或者是用过N次了。
以下内容高手略过。。。欢迎吐槽。
首先来看一段在电子商务应用程序中典型使用的代码部分。代码的功能是获取指定类别中的商品列表。
代码已经共享在GIT 上。 htt://git.oschina.net/
Product.cs 表示数据中的一个商品数据,ProductRepository.cs 表示从数据库中获取商品时间,ProductService.cs 类是一个服务类,用于向下从ProductRepository 中获取数据,向上 提供 Product 数据。
Product.cs
public class Product { public int Id { get; set; } public string Name { get; set; } // 更多其他属性 }
ProductRepository.cs
public class ProductRepository { public IList<Product> GetProductsByCategoryId(int categoryId) { IList<Product> products = new List<Product>(); // TODO. 从数据库中获取商品 return products; } }
ProductService.cs
public class ProductService { protected const string CACHE_KEY = "products.all.{0}"; protected ProductRepository _repository; public ProductService() { this._repository = new ProductRepository(); } public IList<Product> GetProductsByCategoryId(int categoryId) { IList<Product> products; string cache_key = string.Format(CACHE_KEY, categoryId); products = (List<Product>)HttpContext.Current.Cache.Get(cache_key); if (products == null) { products = _repository.GetProductsByCategoryId(categoryId); HttpContext.Current.Cache.Insert(cache_key, products); } return products; } }
ProductService 的GetProductsByCategoryId 方法很简单,就是负责从缓存中获取商品数据,如果缓存中的数据为空,那就从 ProductRepository 中获取并且保存带缓存中 。
如果当前的项目到此为止那是没有什么问题的。当然,我们的重点不再这里。我们重新审查下代码,有什么问题呢?
- ProductService 类 依赖于 ProductRepository 类。如果ProductRepository 类中的API 发生 改变,ProductService 类中 引用的API 也需要相应改变。
- 代码不可测试。如果ProductRepository 类 不与真正的数据库链接,就不能测试 ProductService 类 中的方法,因为这两个类之间存在紧密耦合。还有一个与测试有关的问题是,ProductService 类依赖使用 HttpContext.Current.Cache 来缓存数据,很难测试这种与 HTTP 上下文紧密耦合的代码。
- 被迫使用 HTTP 上下文来缓存。在当前状态下,如果使用其他缓存提供者,比如 memcached 或者是 velocity , 则需要修改 ProductService 类以及其他使用了缓存的类。
当前代码位置:http://git.oschina.net/PassingWind/SampleCode/tree/PartternsStart1/
一,使用设计原则进行重构代码
ProductService 类 依赖于 ProductRepository 类 的问题。如果修改了 ProductRepository 类中的API ,ProductService 类也要做相应修改。这破坏了分离关注点和单一责任原则。
1,依赖倒置原则
依赖抽象而不要依赖具体。
这里可以实施依赖倒置原则来解耦 ProductService 类 和 ProductRepository 类,让他们都依赖于抽象——【接口】。
将 ProductService 类 和 ProductRepository 类 都重构下,提取出接口,IProductRepository 接口和 IProductService 接口。并 实现接口。
修改 ProductService 类 ,改为引用 接口而不是具体实现的类。
public class ProductService : IProductService
{
protected const string CACHE_KEY = "products.all.{0}";
//protected ProductRepository _repository;
protected IProductRepository _repository;
public ProductService()
{
this._repository = new ProductRepository();
}
public IList<Product> GetProductsByCategoryId(int categoryId)
{
IList<Product> products;
string cache_key = string.Format(CACHE_KEY, categoryId);
products = (List<Product>)HttpContext.Current.Cache.Get(cache_key);
if (products == null)
{
products = _repository.GetProductsByCategoryId(categoryId);
HttpContext.Current.Cache.Insert(cache_key, products);
}
return products;
}
}
通过引入接口有什么作用呢? ProductService 类 现在之依赖于抽象而不是具体的实现,意味着 ProductService 类 完全不知道任何实现,从而确定它不会那么容易被破坏,而且代码基在整体上对变化更有弹性。
但是,这里还有一个小问题, ProductService 类 仍然负责创建具体的实现,而且在目前还没有有效的 ProductRepository 类的情况下,无法测试代码。
当前代码位置:http://git.oschina.net/PassingWind/SampleCode/tree/PartternsStart2/
2,依赖注入原则
ProductService 类 仍然与 ProductRepository 类的具体实现关联在一起,因为目前 ProductService 类 目前的任务是创建实例。这一点可以从 ProductService 类的构造方法中可以看出来。
依赖注入 (Dependency Injection ) 可以将创建 ProductRepository 类的具体实现 的责任移到 ProductService 类 外面去,并且通过 ProductService 类 的构造器将其注入。
public class ProductService : IProductService
{
protected const string CACHE_KEY = "products.all.{0}";
//protected ProductRepository _repository;
protected IProductRepository _repository;
public ProductService(IProductRepository repository)
{
//this._repository = new ProductRepository();
this._repository = repository;
}
public IList<Product> GetProductsByCategoryId(int categoryId)
{
IList<Product> products;
string cache_key = string.Format(CACHE_KEY, categoryId);
products = (List<Product>)HttpContext.Current.Cache.Get(cache_key);
if (products == null)
{
products = _repository.GetProductsByCategoryId(categoryId);
HttpContext.Current.Cache.Insert(cache_key, products);
}
return products;
}
}
这样就可以在测试期间 向 ProductService 类 中传递 代替者,从而能孤立的测试 ProductService 类 。通过把获取依赖的责任从 ProductService 类中移除,使 ProductService 类 从而遵循单一责任原则。现在它只关心如何从缓存或者是资源库中获取数据,而不是创建具体的 IProductRepository 类 的实现。
最后要做的事情是 解决 缓存需求对 HTTPContext 的依赖。 可以使用一种简单的设计模式提供的服务。
二,依据 Adapter 模式进行重构
因为没有 http context 类的源代码,因此不能向 ProductRepository 类那样做。但是,这种问题以前就被解决过很多次了,因此有个设计模式可以解决这个问题。
适配器模式 (Adapter) 主要将一个类的某个接口转换成一个兼容的接口,这样就能运用该模式。将 httpcontext cache API 修改成想要的使用的API,然后,可以使用依赖注入原则,通过一个接口将缓存API 注入到 ProductService 类 中。
创建一个 ICacheStorage 的新接口,并且包含以下契约。
public interface ICacheStorage { void Remove(string key); object Get(string key); void Insert(string key, object value); }
接下来,更新 ProductService 类。
public class ProductService : IProductService { protected const string CACHE_KEY = "products.all.{0}"; //protected ProductRepository _repository; protected IProductRepository _repository; protected ICacheStorage _cache; public ProductService(IProductRepository repository, ICacheStorage cache) { //this._repository = new ProductRepository(); this._repository = repository; this._cache = cache; } public IList<Product> GetProductsByCategoryId(int categoryId) { IList<Product> products; string cache_key = string.Format(CACHE_KEY, categoryId); //products = (List<Product>)HttpContext.Current.Cache.Get(cache_key); products = (List<Product>)_cache.Get(cache_key); if (products == null) { products = _repository.GetProductsByCategoryId(categoryId); //HttpContext.Current.Cache.Insert(cache_key, products); _cache.Insert(cache_key, products); } return products; } }
现在的问题是 httpcontext cache 不能隐式的实现新的 ICacheStorage 接口。那么 Adapter 模式怎么有效解决这个问题呢?
GOF将 Adapter 模式的意图描述为:
将一个类的接口转换为客户期望的另一个接口
已禁用评论。