2010-07-28 11:08:36
摘要:适配器模式:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
// 类适配器: 基于继承
public interface ITarget
{
void F1();
void F2();
void Fc();
}
public class Adaptee
{
public void Fa()
{
//...
}
public void Fb()
{
//...
}
public void Fc()
{
//...
}
}
public class Adaptor : Adaptee, ITarget
{
public void F1()
{
base.Fa();
}
public void F2()
{
//...重新实现F2()...
}
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
// 对象适配器:基于组合
public interface ITarget
{
void F1();
void F2();
void Fc();
}
public class Adaptee
{
public void Fa()
{
//...
}
public void Fb()
{
//...
}
public void Fc()
{
//...
}
}
public class Adaptor : ITarget
{
private Adaptee adaptee;
public Adaptor(Adaptee adaptee)
{
this.adaptee = adaptee;
}
pub……
阅读全文
2010-07-24 15:18:34
摘要:装饰模式:动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
Component是定义一个对象接口,可以给这些对象动态地添加职责。ComcreteComponent是定义了一个具体的对象,也可以给这个对象添加一些职责。Decorator,装饰抽象类,继承了Component,从外部来扩展Component类的功能,但对于Component来说,是无需知道Decorator的存在的。至于ComponentDecorator就是具体的装饰对象,起到给Component添加职责的功能。
装饰模式是结构型模式的巅峰设计,之所以这么说,是因为它不仅使用了继承,而且还使用了组合。这是一种比较烧脑的设计模式。我们来看一下装饰模式的代码实现:
abstract class Component
{
public abstract void Operation();
}
class ConcreteComponent : Component
{
public override void Operation()
{
Console.WriteLine(具体对象的操作);
}
}
abstract class Decorator : Component //继承Component
{
protected Component component; //组合Component
public void SetComponent(Component component) //设置Component
{
this.component = component;
}
public override void Operation() //重写Operation(),实际执行的是Component的Operation()
{
if (component != null)
{
component.Operation();
}
}
}
class ConcreteDecoratorA : Decorator
{
private string adde……
阅读全文
2010-07-17 21:39:53
摘要:桥接模式,也叫作桥梁模式,英文是 Bridge Design Pattern。这个模式可以说是 23 种设计模式中最难理解的模式之一了。
当然,这其中“最纯正”的理解方式,当属 GoF 的《设计模式》一书中对桥接模式的定义。毕竟,这 23 种经典的设计模式,最初就是由这本书总结出来的。在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化。”
关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于“组合优于继承”设计原则。
桥接模式的应用举例
比如一款手机可以安装不同的应用,而手机又有多种不同的品牌。如果将手机按品牌分类,其结构图如下:
按软件分类实现结构图
由于实现的方式有多种,桥接模式的核心意图就是把这些实现独立出来,让它们各自地变化。这就使得每种实现的变化不会影响其他实现,从而达到应对变化的目的。
代码实现:
//手机软件
abstract class HandsetSoft
{
public abstract void Run();
}
//手机游戏
class HandsetGame : HandsetSoft
{
public override void Run()
{
Console.WriteLine(运行手机游戏);
}
}
//手机通讯录
class HandsetAddressList : HandsetSoft
{
public override void Run()
{
Console.WriteLine(运行手机通讯录);
}
}
//手机品牌
abstract class HandsetBrand
{
protected HandsetSoft soft;
//设置手机软件
public void S……
阅读全文
2010-07-13 13:13:36
摘要:代理模式是很常用的一种结构性模式,我们先来看一下它的描述和机构图:
代理模式:为其他对象提供一种代理以控制对这个对象的访问。
代理模式的原理与实现
在不改变原始类(或叫被代理类)的情况下,通过引入代理类来给原始类附加功能。一般情况下,我们让代理类和原始类实现同样的接口。但是,如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的。在这种情况下,我们可以通过让代理类继承原始类的方法来实现代理模式。
动态代理的原理与实现
静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。对于静态代理存在的问题,我们可以通过动态代理来解决。我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
代理模式的应用场景
业务系统的非功能性需求开发
代理模式最常用的一个应用场景就是,在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需要关注业务方面的开发。
代理模式在RPC中的应用
实际上,RPC 框架也可以看作一种代理模式,GoF 的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用 RPC 服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC 服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节。
代理模式在缓存中的应用
如果是基于框架来开发的话,那就可以在 AOP 切面中完成接口缓存的功能。
阅读全文
2010-07-08 20:09:14
摘要:原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。
//原型类
abstract class Prototype
{
private string _id;
public Prototype(string id)
{
_id = id;
}
public string Id
{
get { return _id; }
}
public abstract Prototype Clone();
}
//实现类
class ConcretePrototype : Prototype
{
public ConcretePrototype(string id) : base(id)
{
}
public override Prototype Clone()
{
return (Prototype)this.MemberwiseClone(); //浅拷贝
}
}
原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。一般情况下只有创建对象非常耗时,并且能控制共享数据带来的影响的情况下比较使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。
阅读全文
2010-07-03 19:56:08
摘要:建造者模式: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度使用。
如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
下面代码是一个组装小型汽车的实例,小汽车包括的核心配件基本一样,都有引擎、轮胎、车灯等,但不同的工厂工人组装小汽车的工作流程都不同。我们可以使用建造者模式封装不同工人对象创建的细节。
/// summary
/// 工人抽象
/// /summary
public abstract class AbstractBuilder
{
public abstract void Engine();
public abstract void Wheels();
public abstract void Light();
public abstract Car Car();
}
/// summary
/// 比亚迪工人
/// /summary
public class BuilderBYD : AbstractBuilder
{
private Engine _Engine = null;
private Wheels _Wheels = null;
private Light _Light = null;
public override void Engine()
{
this._E……
阅读全文
2010-07-01 12:19:39
摘要:抽象工厂模式的应用场景比较特殊,没有前两种常用。
在简单工厂和工厂方法中,类只有一种分类方式。比如,在规则配置解析那个例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。但是,如果类有两种分类方式,比如,我们既可以按照配置文件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应下面这 6 个 parser 类。
针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
针对这种特殊的场景,如果还是继续用工厂方法来实现的话,我们要针对每个 parser 都编写一个工厂类,也就是要编写 6 个工厂类。如果我们未来还需要增加针对业务配置的解析器(比如 IBizConfigParser),那就要再对应地增加 3 个工厂类。而我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?
抽象工厂就是针对这种非常特殊的场景而诞生的。我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。这样就可以有效地减少工厂类的个数。具体的代码实现如下所示:
public interface IConfigParserFactory
{
IRuleConfigParser CreateRuleParser();
ISystemConfigParser CreateSystemParser();
//此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonRuleConfigParserFactory : IConfigParserFactory
{
public override IRuleConfigParser CreateRuleParser() {
retur……
阅读全文
2010-06-30 21:17:11
摘要:一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。不过,在 GoF 的《设计模式》一书中,它将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见。
简单工厂
在下面这段代码中,我们根据配置文件的后缀(json、xml、yaml),选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),将存储在文件中的配置解析成内存对象 RuleConfig。
public class RuleConfigSource
{
public RuleConfig Load(string ruleConfigFilePath)
{
string ext = GetFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = null;
if (ext == json)
{
parser = new JsonRuleConfigParser();
}
else if (ext == xml)
{
parser = new XmlRuleConfigParser();
}
else if (ext == yaml)
{
parser = new YamlRuleConfigParser();
}
else
{
throw new InvalidRuleConfigException(Rule config file format is not supported: + ruleConfigFilePath);
}
string configText = ;
//从ruleConfigFilePath文件中读取配置文本到configText中
RuleConfig rul……
阅读全文
2010-06-27 18:34:22
摘要:单例模式:保证一个类仅有一个实例,并提供一个该实例的全局访问点。
如何实现一个单例
要实现一个单例,我们需要关注的点无外乎下面几个:
构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
考虑对象创建时的线程安全问题;
考虑是否支持延迟加载;
考虑 GetInstance() 性能是否高(是否加锁)。
1. 饿汉式
饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到Singleton的时候,再创建实例),从名字中我们也可以看出这一点。具体的代码实现如下所示:
public sealed class Singleton //sealed为了阻止派生,因派生可以会增加实例
{
//在第一次引用类的任何成员时创建实例。公共语言运行库负责处理变量初始化
private static readonly Singleton instance = new Singleton();
private Singleton() { }
public static Singleton GetInstance()
{
return instance;
}
}
有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。
不过我认为如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
2. 懒汉式
有饿汉式,对应的就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。具体的代码实现如下所示:
public class Singleton
{
private static volatile Singleton instance;
……
阅读全文
2010-06-24 13:36:56
摘要:重构概述
重构的目的:为什么重构(why)?
对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是我们学习的经典设计思想、原则、模式、编程规范等理论知识的练兵场。
重构的对象:重构什么(what)?
按照重构的规模,我们可以将重构大致分为大规模高层次的重构和小规模低层次的重构。大规模高层次重构包括对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式。小规模低层次的重构包括规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等编程细节问题,主要是针对类、函数级别的重构。小规模低层次的重构更多的是利用编码规范这一理论知识。
重构的时机:什么时候重构(when)?
我们一定要建立持续重构意识,把重构作为开发必不可少的部分,融入到日常开发中,而不是等到代码出现很大问题的时候,再大刀阔斧地重构。
重构的方法:如何重构(how)?
大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做。
单元测试
什么是单元测试?
单元测试是代码层面的测试,由研发自己来编写,用于测试“自己”编写的代码的逻辑的正确性。单元测试顾名思义是测试一个“单元”,有别于集成测试,这个“单元”一般是类或函数,而不是模块或者系统。
为什么要写单元测试?
写单元测试的过程本身就是代码 Code Review 和重构的过程,能有效地发现代码中的 bug 和代码设计上的问题。除此之外,单元测试还是对集成测试的有力补充,还能帮助我们快速熟悉代码,是 TDD 可落地执行的改进方案。
如何编写单元测试?
写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻译成代码。我们可以利用一些测试框架来简化单元测试的编写。除此之外,对于单元测试,我们需要建立以下正确的认知:
编写单元测试尽管繁琐,但并不是太耗时;
我们可以稍微放低对单元测试代码质量的要求;
覆盖率作为衡量单元测试质量的唯一标准是不合理的;
单元测试不要依赖被测代码的具体实现逻辑;
单元测试框……
阅读全文