敏捷软件开发 – VISITOR模式
你需要向类层次结构中增加新的方法,但是增加起来会很费劲或者会破坏现有设计。这是一个很常见的问题。
假设你有一个Modem对象的层次结构。基类中具有对于所有调制解调器来说公共的通用方法。派生类代表正对许多不同调制解调器厂商和类型的驱动程序。同样假设有一个需求,要向该层次结构中增加一个新方法,名为configureForUnix。这个方法壶队调制解调器进行配置,使之可以工作与UNIX操作系统中。在每个调制解调器派生类中,该方法的实现都不相同,因为每个不同的调制解调器在UNIX中都有自己独特的配制方法和行为特征。
糟糕的是,增加configureForUnix方法其实回避了非常讨厌的一组问题。对于Windows怎么办?对于MacOS怎么办?对于Linux又怎么办?我们真的必须针对所使用的每一种新操作系统都要向Modem层次结构中增加一个新方法吗?这种做法显然是丑陋的。我们将永远无法封闭Modem接口。每当出现一种新操作系统时,我们就必须更改该接口并重新部署所有的调试解调器软件。
VISITOR系列模式允许在不更改类层次结构的情况下向其中增加新方法。
- VISITOR模式;
- ACYCLIC VISITOR模式;
- DECORATOR模式;
- EXTENSION OBJECT模式;
VISITOR模式

为了把调制解调器配置可以在UNIX中使用。程序员创建了UnixModemConfig类的一个实例,并把它传给Modem的Accept函数。接着,相应的Modem派生对象会调用UnixModemConfig的接口IModemVisitor的Visit。如果这个派生对象是一个Hayes,那么就会调用Visit(Hayes)。这个调用会被分发到UnixModemConfig的Visit(Hayes)函数,接着该函数把Hayes调制解调器配置为可以在UNIX中使用。
构建了这个结构后,就可以通过增加新的IModemVisitor派生类来增加新的操作系统配置函数,而完全不用对Modem层次结构进行修改。所以,VISITORm模式使用IModemVisitor的派生类替代了Modem层次结构中的方法。
这个双重分发涉及了两个多态分发。第一个分发是Accept函数。该分发辨别出所调用的Accept函数所属对象的类型。第二个分发(由辨别出的Accept方法调用的Visit方法)辨别出要执行的特定函数。
ACYCLIC VISITOR模式
请注意,访问者层次结构的基类中对于被访问层次结构中的每一个派生类都有一个对于函数。因此,就有一个依赖环把所有被访问的派生类绑定在一起。这样,就很难实现对访问者结构的增量编译,并且也很难向被访问层次结构中增加新的派生类。

访问者派生类同样派生自访问者(IVisitor)接口。对于被访问层次结构的每个派生类,都有一个对于的访问者接口。被访问派生类中的Accept函数把Visitor基类转型为适当的访问者接口。如果转型成功,该方法就调用想要的Visit函数。
这种做法解除了依赖环,并且更易于增加被访问的派生类以及进行增量编译。糟糕的是,它同样也使得解决方案更加复杂了。更糟糕的是,转型花费的时间依赖于被访问层次结构的宽度和深度,所以很难进行测定。所以ACYCLLIC VISITOR模式不适用于硬实时系统。该模式的复杂性可能同样会使它不适于其他的系统。但是,对于那些被访问的层次结构不稳定,并且增量编译比较重要的系统来说,该模式是一个不错的选择。
DECORATOR模式
再假设我们有一个具有很多使用者的应用程序。每个使用者都可以坐在他的计算机前,要求系统使用该计算机的调制解调器呼叫另一台计算机。有些用户希望听到拨号声,有些用户则希望他们的调制解调器保持安静。
我们可以通过在代码中每一处对调制解调器拨号的地方询问使用者的偏好来实现这一点。
...
Modem m = user.Modem;
if(user.WantsLoudDial())
{
m.Volume = 11;
}
m.Dial();
...
这段代码会幽灵般成百上千次的遍布于应用程序中。这种做法是要避免的。
另一种做法是在调制解调器对象内部设置一个标志,让Dial方法检测这个标志并相应地设置音量。
public Class HayesModem : Modem
{
private bool wantsLoudDial = false;
public void Dial()
{
if(wantsLoudDial)
{
Volume = 11;
}
}
}
这样做虽然好了一些,但是仍然必须在Modem每个派生类中重复这段代码。Modem新派生类的编写者必须要记着复制这段代码。依赖于程序员的记忆力是相当冒险的事情。
也可以使用TEMPLATE METHOD模式来解决问题。
public abstract class Modem
{
private bool wantsLoudDial = false;
public void Dial()
{
if(wantsLoudDial)
{
Volume = 11;
}
DialForReal();
}
protected abstract void DialForReal();
}
虽然这样更好了一些,但是为什么使用者突然的想法就应该以这种方式影响到Modem呢?Modem为什么应该知道大声拨号呢?每当使用者提出一些其他的古怪要求时,就必须对它进行更改吗?我们想分离那些由于不同的原因而改变的东西。我们同样也可以使用SRP,因为大声拨号的需要和调制解调器的内在功能没有任何关系,所以也不应该成为调制解调器的一部分。
DECORATOR模式通过创建一个名为LoudDialModem的全新类来解决这个问题。LoudDialModem派生自Modem,并且委托给一个它包含的Modem实例。它捕获对Dial函数的调用并在委托前把音量调高。

public interface Modem
{
void Dial(string pno);
int SpeakerVolume { get; set; }
string PhoneNumber { get; }
}
public class HayesModem : Modem
{
private string phoneNumber;
private int speakerVolume;
public void Dial(string pno)
{
phoneNumber = pno;
}
public int SpeakerVolumn
{
get { return speakerVolume; }
set { speakerVolume = value; }
}
public string PhoneNumber
{
get { return phoneNumber; }
}
}
public class LoudDialModem : Modem
{
private Modem itsModem;
public void Dial(Modem m)
{
itsModem = m;
}
public void Dial(string pno)
{
itsModem.SpeakerVolume = 10;
phoneNumber = pno;
}
public int SpeakerVolumn
{
get { return speakerVolume; }
set { speakerVolume = value; }
}
public string PhoneNumber
{
get { return phoneNumber; }
}
}
EXTENSION OBJECT模式

public abstract class Part
{
Hashtable extensions = new Hashtable();
public abstract string PartNumber { get; }
public abstract string Description { get; }
public void AddExtension(string extensionType, PartExtension extension)
{
extensions[extensionType] = extension;
}
public PartExtension GetExtension(string extensionType)
{
PartExtension pe = extensions[extensionType] as PartExtension;
if (pe == null)
{
pe = new BadPartExtension();
}
return pe;
}
public interface PartExtension { }
}
public class PiecePart : Part
{
private string partNumber;
private string description;
private double cost;
public PiecePart(string partNumber, string description, double cost)
{
this.partNumber = partNumber;
this.description = description;
this.cost = cost;
AddExtension("CSV", new CsvPiecePartExtension(this));
AddExtension("XML", new XmlPiecePartExtension(this));
}
public override string PartNumber
{
get { return this.partNumber; }
}
public override string Description
{
get { return this.description; }
}
public double Cost
{
get { return cost; }
}
}
public class Assembly : Part
{
private IList parts = new ArrayList();
private string partNumber;
private string description;
public Assembly(string partNumber, string description)
{
this.partNumber = partNumber;
this.description = description;
AddExtension("CSV", new CsvPiecePartExtension(this));
AddExtension("XML", new XmlPiecePartExtension(this));
}
public void Add(Part part)
{
parts.Add(part);
}
public IList Parts { get { return parts; } }
public override string PartNumber
{
get { return this.partNumber; }
}
public override string Description
{
get { return this.description; }
}
}
public abstract class XmlPartExtension : PartExtension
{
private static XmlDocument document = new XmlDocument();
public abstract XmlElement XmlElement { get; }
protected XmlElement NewElement(string name)
{
return document.CreateElement(name);
}
protected XmlElement NewTextElement(string name, string text)
{
XmlElement element = document.CreateElement(name);
XmlText xmlText = document.CreateTextNode(text);
element.AppendChild(xmlText);
return element;
}
}
public class XmlPiecePartExtension : XmlPartExtension
{
private PiecePart piecePart;
public XmlPiecePartExtension(PiecePart part)
{
piecePart = part;
}
public override XmlElement XmlElement
{
get
{
XmlElement e = NewElement("PiecePart");
e.AppendChild(NewTextElement("PartNumber", piecePart.PartNumber));
e.AppendChild(NewTextElement("Description", piecePart.Description));
e.AppendChild(NewTextElement("Cost", piecePart.Cost));
return e;
}
}
}
public class XmlAssemblyExtension : XmlPartExtension
{
private Assembly assembly;
public XmlAssemblyExtension(Assembly assembly)
{
this.assembly = assembly;
}
public override XmlElement XmlElement
{
get
{
XmlElement e = NewElement("Assembly");
e.AppendChild(NewTextElement("PartNumber", assembly.PartNumber));
e.AppendChild(NewTextElement("Description", assembly.Description));
XmlElement parts = NewElement("Parts");
foreach (Part part in assembly.Parts)
{
XmlPartExtension xpe = part.GetExtension("XML") as XmlPartExtension;
parts.AppendChild(xpe.XmlElement);
}
e.AppendChild(parts);
return e;
}
}
}
public interface CsvPartExtension : PartExtension
{
string CsvText { get; }
}
public class CsvPiecePartExtension : CsvPartExtension
{
private PiecePart piecePart;
public CsvPiecePartExtension(PiecePart part)
{
this.piecePart = part;
}
public string CsvText
{
get
{
StringBuilder b = new StringBuilder("PiecePart,");
b.Append(piecePart.PartNumber);
b.Append(",");
b.Append(piecePart.Description);
b.Append(",");
b.Append(piecePart.Cost);
return b.ToString();
}
}
}
public class CsvAssemblyExtension : CsvPartExtension
{
private Assembly assembly;
public CsvAssemblyExtension(Assembly assemlby)
{
this.assembly = assembly;
}
public string CsvText
{
get
{
StringBuilder b = new StringBuilder("Assembly,");
b.Append(piecePart.PartNumber);
b.Append(",");
b.Append(piecePart.Description);
foreach (Part part in assembly.Parts)
{
CsvPartExtension cpe = part.GetExtension("CSV");
b.Append(",{");
b.Append(cpe.CsvText);
b.Append("}");
}
return b.ToString();
}
}
}
结论
VISITOR系列模式给我们提供了许多无需更改一个层次结构中的类即可修改其行为的方法。因此,它们有助于我们保持OCP。此外,它们也提供了用来分离不同种类的功能的机制,从而使类不会和很多其他的功能混杂在一起。这样有助于我们保持CCP。也应该可以清楚地看出,VISITOR系列模式的结构中也是用了LSP和DIP。
VISITOR模式是有诱惑力的。在它们面前很容易会失去自制力。如果它们有用就去使用它们,但是请对它们的必要性保持谨慎的态度。通常,可以使用VISITOR模式解决的问题往往也可以使用更简单的方法解决。
摘录自:[美]RobertC.Martin、MicahMartin著,邓辉、孙鸣译 敏捷软件开发原则、模式与实践(C#版修订版) [M]、人民邮电出版社,2013、405-432、

浙公网安备 33010602011771号