设计模式入门

本内容将研究如何使用模式和设计原则来改进一段简单的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 模式的意图描述为:

将一个类的接口转换为客户期望的另一个接口







已禁用评论。