设计模式之访问者模式、例子分析
1. 定义
访问者模式( Visitor):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
2. 使用前提
这个模式是很复杂的模式,一般情况下是用不上的,但是如果需要用那就是真的要用了
虽然蛮复杂,但也挺有意思的
首先,使用访问者模式的前提是,数据结构必须稳定的,不能更改,否则这个模式就需要修改很多类,违反了开放-封闭原则,原因是在访问者类(这个类是用来写具体逻辑或者说算法的类)里面,有针对数据结构的类实现的所有方法,如果数据结构相关类改变了,那么访问者类及其继承者都要变化
比方说,人类只有两个性别(不考虑变性以及具有医学争议的性别),男性和女性,那么男性和女性就是稳定的数据结构,是不会变化的,而访问者抽象类就必须有两个方法,即针对男性的方法(FocusonMen)以及针对女性的方法(FocusonWomen),刚好一个方法对应男性(里面写针对男性所需要的逻辑),一个方法对应女性(里面写针对女性所需要的逻辑),数据结构稳定所以其访问者实现的两个方法也是稳定的
访问者模式的目的,是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。反之,如果这样的系统的数据结构对象易于变化,经常要有新的数据对象增加进来,就不适合使用访问者模式
那其实访问者模式的优点就是增加新的操作很容易,因为增加新的操作就意味着增加一个新的访问者。访问者模式将有关的行为集中到一个访问者对象中
3. 访问者模式结构图与代码模板
结构图:
模板代码:
4. 举例与分析
题目:
针对人类的男性和女性,用访问者模式的思想,实现下面的话:
男人成功时,背后多半有一个伟大的女人。女人成功时,背后大多有一个不成功的男人。
男人失败时,闷头喝酒,谁也不用劝。女人失败时,眼泪汪汪,谁也劝不了。
男人恋爱时,凡事不懂也要装懂。女人恋爱时,遇事懂也装作不懂。
分析题目:
- 人类的男性和女性是不会变化的,人类只有男性和女性,符合使用访问者模式的前提
- 分析这些话,会发现除了男人女人外,还有其“成功”、“失败、“恋爱”三种状态,这些状态男性女性都有,可以当做访问者模式里的访问者类处理
- 另外,这些话里面还有着最终结果,即“背后多半有个成功的女人”等,可以当做是访问者类的内部逻辑是打印这句话
- 我们发现这些话是有顺序的,男性后就是女性,所以我们可以构造一个对象结构类,里面一次调用其男女方法(可以当做算法的顺序来理解)
结构图:
代码:
- 构建抽象人类
abstract class Person
{
public abstract void Apply(State state);//与状态联系起来,即与访问者联系起来
}
- 继承于抽象人类的具体性别,男性、女性
//男性
class Men : Person
{
public override void Apply(State state)
{
state.GetMenState(this);//调用访问者即状态类的方法,并把自身当做参数传入
}
}
//女性
class Women : Person
{
public override void Apply(State state)
{
state.GetWomenState(this);//调用访问者即状态类的方法,并把自身当做参数传入
}
}
此处,运用了双分派的技术,首先在客户程序中将具体状态作为参数传递给“男人”类完成了一次分派,然后“男人”类调用作为参数的“具体状态”中的方法“男人反应”,同时将自己(this)作为参数传递进去。这便完成了第二次分派
双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。‘接受’方法就是一个双分派的操作,它得到执行的操作不仅决定于‘状态’类的具体状态,还决定于它访问的‘人’的类别。
- 抽象状态类
//实现数据结构类即人类,其具体实现类-男人、女人的2种方法
abstract class State
{
public abstract void GetMenState(Men men);//构造男性方法
public abstract void GetWomenState(Women women);//构造女性方法
}
- 继承于抽象状态类的具体状态类
//具体状态类实现其抽象类的方法,该方法体是所需要实现的逻辑
class Success : State
{
public override void GetMenState(Men men)
{
Console.WriteLine("{0}{1}时,背后多半有一个伟大的女人", men.GetType().Name, this.GetType().Name);
}
public override void GetWomenState(Women women)
{
Console.WriteLine("{0}{1}时,背后大多有一个不成功的男人", women.GetType().Name, this.GetType().Name);
}
}
class Fail : State
{
public override void GetMenState(Men men)
{
Console.WriteLine("{0}{1}时,闷头喝酒,谁也不用劝", men.GetType().Name, this.GetType().Name);
}
public override void GetWomenState(Women women)
{
Console.WriteLine("{0}{1}时,眼泪汪汪,谁也劝不了", women.GetType().Name, this.GetType().Name);
}
}
class Love : State
{
public override void GetMenState(Men men)
{
Console.WriteLine("{0}{1}时,凡事不懂也要装懂", men.GetType().Name, this.GetType().Name);
}
public override void GetWomenState(Women women)
{
Console.WriteLine("{0}{1}时,遇事懂也装作不懂", women.GetType().Name, this.GetType().Name);
}
}
- 构造对象结构类
class ObjectStructure
{
private List<Person> People = new List<Person>();
public void Add(Person person)
{
People.Add(person);
}
public void Remove(Person person)
{
People.Remove(person);
}
//为了体现调用的顺序
public void Use(State state)
{
foreach (var item in People)
{
item.Apply(state);
}
}
}
- 客户端代码
class Program
{
static void Main(string[] args)
{
//创建对象结构类的实例并创建男性、女性的实例
ObjectStructure structure = new ObjectStructure();
Men men = new Men();
Women women = new Women();
//创建成功、失败、恋爱状态的实例
Success success = new Success();
Fail fail = new Fail();
Love love = new Love();
//往对象结构类的实例中添加男性、女性的实例
structure.Add(men);
structure.Add(women);
//调用对象结构类的方法来控制状态实例调用男性、女性实例
structure.Use(success);
structure.Use(fail);
structure.Use(love);
Console.Read();
}
}
代码结果:
5. 访问者模式总结
为什么用男女性别来举例分析访问者模式呢?
你想呀,如果人类的性别不止是男和女,而是可有多种性别,那就意味‘状态’类中的抽象方法就不可能稳定了,每加一种类别,就需要在状态类和它的所有下属类中都增加一个方法,这就不符合开放-封闭原则。
也就是说,访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化
访问者模式的目的是什么?
访问者模式的目的是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。反之,如果这样的系统的数据结构对象易于变化,经常要有新的数据对象增加进来,就不适合使用访问者模式
其实访问者模式的优点就是增加新的操作很容易,因为增加新的操作就意味着增加一个新的访可者。
访问者模式将有关的行为集中到一个访问者对象中,通常ConcreteVisitor可以单独开发,不必跟ConcreteElementA或ConcreteElementB 写在一起。正因为这样,ConcreteVisitor能提高ConcreteElement之间的独立性,如果把一个处理动作设计成ConcreteElementA和ConcreteElementB类的方法,每次想新增“处理”以扩充功能时就得去修改ConcreteElementA和ConcreteElementB了。对于水面题目来说,如果在‘男人’和女人’类中加了对‘成功’、‘失败’等状态的判断,就会造成处理方法和数据结构的紧耦合
访问者的缺点其实也就是使增加新的数据结构变得困难了
所以GoF四人中的一个作者就说过:‘大多时候你并不需要访问者模式,但当一旦你需要访问者模式时,那就是真的需要它了。’事实上,我们很难找到数据结构不变化的情况,所以用访问者模式的机会也就不太多了。这也就是为什么使用男人女人的例子,因为人类性别这样的数据结构是不会变化的
总的来说,访问者模式的能力和复杂性是把双刃剑,只有当你真正需要它的时候,才考虑使用它。有很多的程序员为了展示自己的面向对象的能力或是沉迷于模式当中,往往会误用这个模式,所以一定要好好理解它的适用性