求知 文章 文库 Lib 视频 iProcess 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
设计模式(五)
 
火龙果软件    发布于 2014-3-5
 

设计模式之命令模式(十三)

一、引出模式

在前面的模式中,我们已经组装好了一台电脑,现在要做的是开机。是的,开机!对用户们来说开机只不过是按个电源按钮,跟喝水一样简单!但对于我们搞技术的,就不一样了,可这其中又发生了什么不为人知的事呢?自己百度去!

我们先简单将以下流程,不做深入讲解。

首先加载电源,然后设备自检,接下来装在操作系统,最后电脑就启动了。可是谁来完成这些过程?如何完成的呢?

总不能让用户做这些吧,其实真正完成这些功能的是主板。那客户和主板又是怎么联系的呢?现实中,使用连接线将按钮连接到主板上,这样当用户按下按钮时,就相当与发命令给主板,让主板去完成后续工作。

想想,在这里有没有什么问题?

我们把这种情形放到软件开发中看看。客户端只是想要发出命令,不关心命令的执行者是谁,也不关心执行者是怎么完成的,有时同一个请求可能需要执行不同的操作,那怎么办?

二、认识模式

1.模式定义

将请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求进行排队或记录请求日志,以及支持可撤销的操作。

2.解决思路

我们来试着用命令模式解决上述开机的过程。

当客户按下按钮时,按钮本身并不知道如何处理,于是我们通过连接线,将按钮和主板连接起来,让主板去完成真正启动机器的功能。

在这里,我们通过引入按钮和连接线,来让发出命令的客户和命令的真正实现者——主板完全解耦,客户操作的始终是按钮,按钮后面的事客户就不管了,因为客户只知道只要按下按钮就能开机了,中间做了什么客户是不关心的,客户只求结果,不问过程。

在命令模式中,会定义一个命令的接口,用来约束所有的命令对象,然后提供具体的命令实现,每个命令实现对象是对客户端某个请求的封装,对应于机箱上的按钮,一个机箱可以有很多按钮,也就相当于有很多个具体的命令实现对象。

在命令模式中,命令对象是不知道如何处理命令的,他会转调命令接受者对象来真正执行命令。就像刚才例子中,按钮是不知道如何处理的,按钮是吧这个请求转发给主板,主板来执行,这个主板就相当于命令模式汇总的接受者。

在命令模式中,命令对象和接收者的关系并不是与生俱来的,需要有一个装配者对两者进行关联,命令模式中的Client对象可以实现这样的功能,在电脑中,有了按钮,有了主板,那还需要有根连接线将按钮和主板连接起来才行,这根连接线就充当着Client对象的角色。

命令模式中,还会提供一个Invoker对象来持有命令对象。比如,机箱上会有多个按钮,这个机箱就相当于Invoker对象,这样我们客户就可以通过Invoker也就是机箱来按下按钮来执行相应的命令。

3.模式结构原型

Command:定义命令的接口,声明执行的方法。

ConreteCommand:命令接口实现对象,是“虚”的实现;通常会持有接受者,并调用接受者的功能来完成命令要执行的操作。

Receiver:接受者,真正执行命令的对象。任何类都能成为接受者,只要它能够实现命令要求实现的相应的功能。

Invoker:要求命令对象执行请求,通常会持有命苦对象,可以持有很多命令对象。这个是客户端真正触发命令并要求执行相应操作的定法,也就是说这才是使用命令对象的入口。

Client:创建具体的命令对象,并且设置命令对象的接受者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接受者,可以将它称为装配者,真正使用命令的客户端是从Invoker来触发执行的。

4.模式原型示例代码

class Program
{
static void Main(string[] args)
{
Client client = new Client();
client.Run();
Console.ReadKey();
}
}

/// <summary>
/// 示意,负责创建命令对象,并设定它的接受者
/// </summary>
public class Client
{
public void Run()
{
Receiver receiver = new Receiver();
//创建命令对象,设定它的接收者
Command command = new ConcreteCommand(receiver);
//创建Invoker,把命令对象设置进去
Invoker invoker = new Invoker(command);
invoker.RunCommand();
}
}

/// <summary>
/// 命令接口,声明执行的操作
/// </summary>
public interface Command
{
/// <summary>
/// 执行命令对应的操作
/// </summary>
void Execute();
}

/// <summary>
/// 具体的命令实现对象
/// </summary>
public class ConcreteCommand : Command
{
/// <summary>
/// 持有相应的接受者对象
/// </summary>
private Receiver receiver = null;

/// <summary>
/// 示意,命令对象可以有自己的状态
/// </summary>
private string state = null;

/// <summary>
/// 构造方法,传入相应的接受者对象
/// </summary>
/// <param name="receiver">相应的接受者对象</param>
public ConcreteCommand(Receiver receiver)
{
this.receiver = receiver;
}

public void Execute()
{
//通常会转调接受者对象的相应方法,让接受者来真正执行功能
receiver.Action();
}
}

/// <summary>
/// 接收者对象
/// </summary>
public class Receiver
{
/// <summary>
/// 示意方法,真正执行命令相应的操作
/// </summary>
public void Action()
{
Console.WriteLine("命令执行了");
}
}

/// <summary>
/// 调用者(机箱)
/// </summary>
public class Invoker
{
/// <summary>
/// 持有命令对象
/// </summary>
private Command command;

/// <summary>
/// 设置调用者持有的命令对象
/// </summary>
/// <param name="command">命令对象</param>
public Invoker(Command command)
{
this.command = command;
}

/// <summary>
/// 示意方法,要求命令执行请求
/// </summary>
public void RunCommand()
{
command.Execute();
}
}

5.电脑开机示例示例代码

class Program
{
static void Main(string[] args)
{
//把命令和真正的实现组合起来,相当于在组装机器,
MainBoardApi mainBoardApi=new GigaMainBoard();

//把机箱上按钮的连接线插接到主板上。
Command command=new OpenCommand(mainBoardApi);

//真正的客户端测试

//为机箱上的按钮设置对应的命令,让按钮知道该干什么
Box box=new Box();
box.SetOpenCommand(command);

//然后模拟按下机箱上的按钮
box.OpenButtonPressed();

Console.Read();
}
}

/// <summary>
/// 命令接口,声明执行的操作
/// </summary>
public interface Command
{
/// <summary>
/// 执行命令对应的操作
/// </summary>
void Execute();
}

/// <summary>
/// 持有开机命令的真正实现,通过调用接收者的方法来实现命令
/// </summary>
public class OpenCommand : Command
{
/// <summary>
/// 持有真正实现命令的接收者——主板对象
/// </summary>
private MainBoardApi mainBoard = null;

/// <summary>
/// 构造方法,传入主板对象
/// </summary>
/// <param name="mainBoard">主板对象</param>
public OpenCommand(MainBoardApi mainBoard)
{
this.mainBoard = mainBoard;
}

public void Execute()
{
//对于命令对象,根本不知道如何开机,会转调主板对象
//让主板去完成开机的功能
this.mainBoard.Open();
}
}

/// <summary>
/// 主板的接口
/// </summary>
public interface MainBoardApi
{

/// <summary>
/// 主板具有能开机的功能
/// </summary>
void Open();
}

/// <summary>
/// 技嘉主板类,开机命令的真正实现者,在Command模式中充当Receiver
/// </summary>
public class GigaMainBoard : MainBoardApi
{

/// <summary>
/// 真正的开机命令的实现
/// </summary>
public void Open()
{
Console.WriteLine("技嘉主板现在正在开机,请等候");
Console.WriteLine("接通电源......");
Console.WriteLine("设备检查......");
Console.WriteLine("装载系统......");
Console.WriteLine("机器正常运转起来......");
Console.WriteLine("机器已经正常打开,请等候");
}
}

/// <summary>
/// 微星主板类,开机命令的真正实现者,在Command模式中充当Receiver
/// </summary>
public class MsiMainBoard : MainBoardApi
{
/// <summary>
/// 真正的开机命令的实现
/// </summary>
public void Open()
{
Console.WriteLine("微星主板现在正在开机,请等候");
Console.WriteLine("接通电源......");
Console.WriteLine("设备检查......");
Console.WriteLine("装载系统......");
Console.WriteLine("机器正常运转起来......");
Console.WriteLine("机器已经正常打开,请等候");
}
}


/// <summary>
/// 机箱对象,本身有按钮,持有按钮对应的命令对象
/// </summary>
public class Box
{
/// <summary>
/// 开机命令对象
/// </summary>
private Command openCommand;

/// <summary>
/// 设置开机命令对象
/// </summary>
/// <param name="command">开机命令对象</param>
public void SetOpenCommand(Command command)
{
this.openCommand = command;
}

/// <summary>
/// 提供给客户使用,接受并相应用户请求,相当于按钮被按下触发的方法
/// </summary>
public void OpenButtonPressed()
{
//按下按钮,执行命令
openCommand.Execute();
}
}

三、理解模式

1.命令模式的关键

命令模式的关键之处就是把请求封装称为对象,也就是命令对象,并定义统一的执行操作的接口,这个命令对象被存储、转发、记录、处理、撤销等,整个命令模式都是围绕这个对象进行的。

2.命令模式的组装和调用

命令模式中经常会有一个命令的装配者,用它来维护命令的“虚”实现和真实实现之间的关系。如果是超级智能的命令,也就是说命令对自己完全实现好了,不需要接受者,那就是命令模式的退化,不需要接受者,自然也不需要装配者。

实际开发中,Client和Invoker是可以融合在一起的,有客户在使用命令模式的时候,先进行命令对象和接受者的组装,组装完成后,就可以调用命令执行请求。

3.命令的接受者

接受者是可以是任意的类,只要这个对象知道如何真正的执行命令,执行时是从Command的实现类里面转调过来的。

一个接受者对象可以处理多个命令对象,接受者和命令之间没有约定的对象关系。

4.智能命令

在标准的命令模式中,命令的实现类是没有真正实现命令要求的功能的,真正执行命令的是接受者。

如果命令的实现对象比较智能,自己就能实现命令要求的功能,就不需要调用接受者,这种情况称为智能命令。

5.发起请求的对象和真正处理的对象是解耦的

请求有谁来处理?如何处理?发起请求的对象是不知道的,也就是发情请求的对象和真正实现的对象是解耦的。

6.参数化配置

所谓的命令模式的参数化配置,指的是:可以用不同的命令对象,去参数化配置客户端的请求。

如前面的表述,按下按钮你是不知道是开机,关机还是重启的,那就要看参数化配置是哪一个具体的按钮对象。

示例代码:

class Program
{
static void Main(string[] args)
{
//把命令和真正的实现组合起来,相当于在组装机器,
MainBoardApi mainBoardApi = new GigaMainBoard();

//创建开机命令
Command command = new OpenCommand(mainBoardApi);

//创建重启机器的命令
ResetCommand resetCommand = new ResetCommand(mainBoardApi);

//真正的客户端测试

//为机箱上的按钮设置对应的命令,让按钮知道该干什么
Box box = new Box();

//先正确配置,就是开机按钮对开机命令,重启按钮对重启命令
box.SetOpenCommand(command);
box.SetResetCommand(resetCommand);

//然后模拟按下机箱上的按钮
Console.WriteLine("正确配置下------------------------->");
Console.WriteLine(">>>按下开机按钮:>>>");

box.OpenButtonPressed();
Console.WriteLine(">>>按下重启按钮:>>>");
box.ResetButtonPressed();
Console.Read();
}
}

/// <summary>
/// 命令接口,声明执行的操作
/// </summary>
public interface Command
{
/// <summary>
/// 执行命令对应的操作
/// </summary>
void Execute();
}

/// <summary>
/// 持有开机命令的真正实现,通过调用接收者的方法来实现命令
/// </summary>
public class OpenCommand : Command
{
/// <summary>
/// 持有真正实现命令的接收者——主板对象
/// </summary>
private MainBoardApi mainBoard = null;

/// <summary>
/// 构造方法,传入主板对象
/// </summary>
/// <param name="mainBoard">主板对象</param>
public OpenCommand(MainBoardApi mainBoard)
{
this.mainBoard = mainBoard;
}

public void Execute()
{
//对于命令对象,根本不知道如何开机,会转调主板对象
//让主板去完成开机的功能
this.mainBoard.Open();
}
}

/// <summary>
/// 重启机器命令的实现,实现Command接口,
/// 持有重启机器命令的真正实现,通过调用接收者的方法来实现命令
/// </summary>
public class ResetCommand : Command
{

/// <summary>
/// 持有真正实现命令的接收者——主板对象
/// </summary>
private MainBoardApi mainBoard = null;

/// <summary>
/// 构造方法,传入主板对象
/// </summary>
/// <param name="mainBoard">主板对象</param>
public ResetCommand(MainBoardApi mainBoard)
{
this.mainBoard = mainBoard;
}


public void Execute()
{
//对于命令对象,根本不知道如何重启机器,会转调主板对象
//让主板去完成重启机器的功能
this.mainBoard.Reset();
}
}


/// <summary>
/// 主板的接口
/// </summary>
public interface MainBoardApi
{

/// <summary>
/// 主板具有能开机的功能
/// </summary>
void Open();

/// <summary>
/// 主板具有实现重启的功能
/// </summary>
void Reset();
}

/// <summary>
/// 技嘉主板类,开机命令的真正实现者,在Command模式中充当Receiver
/// </summary>
public class GigaMainBoard : MainBoardApi
{

/// <summary>
/// 真正的开机命令的实现
/// </summary>
public void Open()
{
Console.WriteLine("技嘉主板现在正在开机,请等候");
Console.WriteLine("接通电源......");
Console.WriteLine("设备检查......");
Console.WriteLine("装载系统......");
Console.WriteLine("机器正常运转起来......");
Console.WriteLine("机器已经正常打开,请等候");
}

/// <summary>
/// 真正的重新启动机器命令的实现
/// </summary>
public void Reset()
{
Console.WriteLine("微星主板现在正在重新启动机器,请等候");
Console.WriteLine("机器已经正常打开,请等候");
}
}

/// <summary>
/// 微星主板类,开机命令的真正实现者,在Command模式中充当Receiver
/// </summary>
public class MsiMainBoard : MainBoardApi
{
/// <summary>
/// 真正的开机命令的实现
/// </summary>
public void Open()
{
Console.WriteLine("微星主板现在正在开机,请等候");
Console.WriteLine("接通电源......");
Console.WriteLine("设备检查......");
Console.WriteLine("装载系统......");
Console.WriteLine("机器正常运转起来......");
Console.WriteLine("机器已经正常打开,请等候");
}

/// <summary>
/// 真正的重新启动机器命令的实现
/// </summary>
public void Reset()
{
Console.WriteLine("微星主板现在正在重新启动机器,请等候");
Console.WriteLine("机器已经正常打开,请等候");
}
}


/// <summary>
/// 机箱对象,本身有按钮,持有按钮对应的命令对象
/// </summary>
public class Box
{
/// <summary>
/// 开机命令对象
/// </summary>
private Command openCommand;

/// <summary>
/// 设置开机命令对象
/// </summary>
/// <param name="command">开机命令对象</param>
public void SetOpenCommand(Command command)
{
this.openCommand = command;
}

/// <summary>
/// 提供给客户使用,接受并相应用户请求,相当于按钮被按下触发的方法
/// </summary>
public void OpenButtonPressed()
{
//按下按钮,执行命令
openCommand.Execute();
}

/// <summary>
/// 重启机器命令对象
/// </summary>
private Command resetCommand;

/// <summary>
/// 设置重启机器命令对象
/// </summary>
/// <param name="command"></param>
public void SetResetCommand(Command command)
{
this.resetCommand = command;
}

/// <summary>
/// 提供给客户使用,接受并相应用户请求,相当于重启按钮被按下触发的方法
/// </summary>
public void ResetButtonPressed()
{
//按下按钮,执行命令
resetCommand.Execute();
}
}

7.可撤销的操作

可撤销的操作意思是:放弃该操作,回到未执行操作前的状态。

有两种基本的思路来实现可撤销的操作,一种是补偿式又称反操作式,比如被撤销的操作是+,那撤销的操作就是-。

另一种是存储恢复式,就是把操作前的状态记录下来,然后要撤销操作时直接恢复回去。

在这里我们演示第一种可撤销操作,剩下一种等到备忘录模式时在讲。

做一个计算机功能,只需要实现加减运算,还要让这个计算器支持可撤销的

示例代码:

class Program
{
static void Main(string[] args)
{

//1:组装命令和接收者
//创建接收者
OperationApi operation = new Operation();

//创建命令对象,并组装命令和接收者
AddCommand addCmd = new AddCommand(operation, 5);
SubCommand substractCmd = new SubCommand(operation, 3);

//2:把命令设置到持有者,就是计算器里面
Calculator calculator = new Calculator();
calculator.SetAddCommand(addCmd);
calculator.SetSubCommand(substractCmd);

//3:模拟按下按钮,测试一下
calculator.AddPressed();
Console.WriteLine("一次加法运算后的结果为:" + operation.GetResult());
calculator.SubPressed();
Console.WriteLine("一次减法运算后的结果为:" + operation.GetResult());

//测试撤消
calculator.UndoPressed();
Console.WriteLine("撤销一次后的结果为:" + operation.GetResult());
calculator.UndoPressed();
Console.WriteLine("再撤销一次后的结果为:" + operation.GetResult());

//测试恢复
calculator.RedoPressed();
Console.WriteLine("恢复操作一次后的结果为:" + operation.GetResult());
calculator.RedoPressed();
Console.WriteLine("再恢复操作一次后的结果为:" + operation.GetResult());

Console.Read();
}
}

/// <summary>
/// 命令接口,声明执行的操作,支持可撤销操作
/// </summary>
public interface Command
{
/// <summary>
/// 执行命令对应的操作
/// </summary>
void Execute();

/// <summary>
/// 执行撤销命令对应的操作
/// </summary>
void Undo();
}

/// <summary>
/// 具体的加法命令实现对象
/// </summary>
public class AddCommand : Command
{
/// <summary>
/// 持有具体执行计算的对象
/// </summary>
private OperationApi operationApi = null;

/// <summary>
/// 操作的数据,也就是要加上的数据
/// </summary>
private int num;

/// <summary>
/// 构造方法,传入具体执行计算的对象
/// </summary>
/// <param name="operationApi"></param>
/// <param name="num"></param>
public AddCommand(OperationApi operationApi, int num)
{
this.operationApi = operationApi;
this.num = num;
}

public void Execute()
{
////转调接收者去真正执行功能,这个命令是做加法
operationApi.Add(num);
}

public void Undo()
{
//转调接收者去真正执行功能
//命令本身是做加法,那么撤销的时候就是做减法了
operationApi.Sub(num);
}
}

/// <summary>
/// 具体的减法命令实现对象
/// </summary>
public class SubCommand : Command
{
/// <summary>
/// 持有具体执行计算的对象
/// </summary>
private OperationApi operationApi = null;

/// <summary>
/// 操作的数据,也就是要加上的数据
/// </summary>
private int num;

/// <summary>
/// 构造方法,传入具体执行计算的对象
/// </summary>
/// <param name="operationApi"></param>
/// <param name="num"></param>
public SubCommand(OperationApi operationApi, int num)
{
this.operationApi = operationApi;
this.num = num;
}

public void Execute()
{
//转调接收者去真正执行功能,这个命令是做减法
operationApi.Sub(num);
}

public void Undo()
{
//转调接收者去真正执行功能
//命令本身是做减法,那么撤销的时候就是做加法了
operationApi.Add(num);
}
}

/// <summary>
/// 操作运算的接口
/// </summary>
public interface OperationApi
{
/// <summary>
/// 获取计算完成后的结果
/// </summary>
/// <returns></returns>
int GetResult();

/// <summary>
/// 设置计算开始的初始值
/// </summary>
/// <param name="result"></param>
void SetResult(int result);

/// <summary>
/// 执行加法
/// </summary>
/// <param name="num"></param>
void Add(int num);

/// <summary>
/// 执行减法
/// </summary>
/// <param name="num"></param>
void Sub(int num);
}

/// <summary>
/// 运算类,真正实现加减法运算
/// </summary>
public class Operation : OperationApi
{
/// <summary>
/// 记录运算的结果
/// </summary>
private int result;

public int GetResult()
{
return result;
}

/// <summary>
/// 设置值
/// </summary>
/// <param name="result"></param>
public void SetResult(int result)
{
this.result = result;
}

/// <summary>
/// 实现加法功能
/// </summary>
/// <param name="num"></param>
public void Add(int num)
{
result += num;
}

/// <summary>
/// 实现减法功能
/// </summary>
/// <param name="num"></param>
public void Sub(int num)
{
result -= num;
}
}

/// <summary>
/// 计算器类,计算器上有加法按钮、减法按钮,还有撤销和恢复的按钮
/// </summary>
public class Calculator
{
/// <summary>
/// 命令的操作的历史记录,在撤销时候用
/// </summary>
private List<Command> undoCmds = new List<Command>();

/// <summary>
/// 命令被撤销的历史记录,在恢复时候用
/// </summary>
private List<Command> redoCmds = new List<Command>();

/// <summary>
/// 持有执行加法的命令对象
/// </summary>
private Command addCommand = null;

/// <summary>
/// 持有执行减法的命令对象
/// </summary>
private Command subCommand = null;

/// <summary>
/// 设置执行加法的命令对象
/// </summary>
/// <param name="addCommand"></param>
public void SetAddCommand(Command addCommand)
{
this.addCommand = addCommand;
}

/// <summary>
/// 设置执行减法的命令对象
/// </summary>
/// <param name="subCommand"></param>
public void SetSubCommand(Command subCommand)
{
this.subCommand = subCommand;
}

/// <summary>
/// 加法按钮
/// </summary>
public void AddPressed()
{
this.addCommand.Execute();
//把操作记录到历史记录里面
undoCmds.Add(this.addCommand);
}

/// <summary>
/// 减法按钮
/// </summary>
public void SubPressed()
{
this.subCommand.Execute();
//把操作记录到历史记录里面
undoCmds.Add(this.subCommand);
}

/// <summary>
/// 撤销按钮
/// </summary>
public void UndoPressed()
{
if (this.undoCmds.Count > 0)
{
//取出最后一个命令来撤销
Command cmd = this.undoCmds.Last();
cmd.Undo();
//如果还有恢复的功能,那就把这个命令记录到恢复的历史记录里面
this.redoCmds.Add(cmd);
//然后把最后一个命令删除掉,
this.undoCmds.Remove(cmd);
}
else
{
Console.WriteLine("很抱歉,没有可撤销的命令");
}
}

/// <summary>
/// 恢复按钮
/// </summary>
public void RedoPressed()
{
if (this.redoCmds.Count > 0)
{
//取出最后一个命令来重做
Command cmd = this.redoCmds.Last();
cmd.Execute();
//把这个命令记录到可撤销的历史记录里面
this.undoCmds.Add(cmd);
//然后把最后一个命令删除掉
this.redoCmds.Remove(cmd);
}
else
{
Console.WriteLine("很抱歉,没有可恢复的命令");
}
}
}

8.宏命令

宏命令就是包含多个命令的命令,是一个命令的组合。命令命令模式也是能实现的。

9.队列请求

所谓队列请求,就是对命令对象进行排队,组成工作队列,然后一次取出命令对象来执行。

10.日志请求

日志请求,就是将请求的历史记录保存下来,一般是采用永久存储的方式。如果运行请求过程中,系统崩溃了,那么当系统再次运行时,就可以从保存的历史记录中获取日志请求,并重新执行命令。

11.命令模式的优点

更松散的耦合

命令模式使得发起命令的对象——客户端,和命令的执行者对象完全解耦。

更动态的控制

命令模式将请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,使得系统更加灵活。

更自然的复合命令

命令模式中的命令对象能够很容易的组合成符合命令,如前面的宏命令。

12.何时选用命令模式

如果需要抽象出需要执行的动作,并参数化这些对象,可以使用命令模式。将这些需要执行的动作抽象成为命令,然后实现命令的参数化配置。

如果需要在不同的时刻指定、排列和执行请求,可以选用命令模式。

如果需要支持取消操作,可以选用命令模式,通过管理命令对象,很容易实现命令的恢复和重做功能。

如果需要支持系统崩溃时,重启后能将系统的操作功能重新执行一遍,可以选用命令模式。

在需要事务的系统中,可以选用命令模式。

13.命令模式的本质

命令模式的本质就是“封装请求”。命令模式的关键就是把请求封装称为命令对象,然后就可以对这个对象进行一系列的处理。

设计模式之组合模式(十四)

一、引出模式

在软件开发中,我们经常会遇到树型目录的功能,比如:管理商品的目录

如果让你来实现这个功能,你会怎么做呢?

我们先来分析分析:商品类别树上的节点有三类,根节点、树枝节点和叶子节点,在进一步根节点和树枝节点都是可以包含其他节点的,我们就叫它容器节点。这样,商品类别树就分为了容器节点和叶子节点,我们将它们分别实现成为对象。

代码示例:

class Program
{
static void Main(string[] args)
{
//定义所有的组合对象
Composite root = new Composite("服装");
Composite c1 = new Composite("男装");
Composite c2 = new Composite("女装");
//定义所有的叶子对象
Leaf leaf1 = new Leaf("衬衣");
Leaf leaf2 = new Leaf("夹克");
Leaf leaf3 = new Leaf("裙子");
Leaf leaf4 = new Leaf("套装");
//按照树的结构来组合组合对象和叶子对象
root.AddComposite(c1);
root.AddComposite(c2);

c1.AddLeaf(leaf1);
c1.AddLeaf(leaf2);

c2.AddLeaf(leaf3);
c2.AddLeaf(leaf4);

//调用根对象的输出功能来输出整棵树
root.PrintStruct("");

Console.ReadKey();
}
}

/// <summary>
/// 叶子对象
/// </summary>
public class Leaf
{
/// <summary>
/// 叶子对象的名字
/// </summary>
private string name = null;

/// <summary>
/// 构造方法,传入叶子对象的名字
/// </summary>
/// <param name="name">叶子对象的名字</param>
public Leaf(string name)
{
this.name = name;
}

/// <summary>
/// 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
/// </summary>
/// <param name="preStr">前缀,主要是按照层级拼接的空格,实现向后缩进</param>
public void PrintStruct(string preStr)
{
Console.WriteLine(preStr + "-" + name);
}
}

/// <summary>
/// 组合对象,可以包含其它组合对象或者叶子对象
/// </summary>
public class Composite
{
/// <summary>
/// 用来记录包含的其它组合对象
/// </summary>
private List<Composite> childComposite = new List<Composite>();

/// <summary>
/// 用来记录包含的其它叶子对象
/// </summary>
private List<Leaf> childLeaf = new List<Leaf>();

/// <summary>
/// 组合对象的名字
/// </summary>
private string name = null;

/// <summary>
/// 构造方法,传入组合对象的名字
/// </summary>
/// <param name="name"></param>
public Composite(string name)
{
this.name = name;
}

/// <summary>
/// 向组合对象加入被它包含的其它组合对象
/// </summary>
/// <param name="c"></param>
public void AddComposite(Composite c)
{
this.childComposite.Add(c);
}

/// <summary>
/// 向组合对象加入被它包含的叶子对象
/// </summary>
/// <param name="leaf"></param>
public void AddLeaf(Leaf leaf)
{
this.childLeaf.Add(leaf);
}

/// <summary>
/// 输出组合对象自身的结构
/// </summary>
/// <param name="preStr"></param>
public void PrintStruct(String preStr)
{
//先把自己输出去
Console.WriteLine(preStr + "+" + this.name);
//然后添加一个空格,表示向后缩进一个空格,输出自己包含的叶子对象
preStr += " ";
foreach (Leaf leaf in childLeaf)
{
leaf.PrintStruct(preStr);
}

//输出当前对象的子对象了
foreach (Composite c in childComposite)
{
////递归输出每个子对象
c.PrintStruct(preStr);
}
}
}

功能上已经实现好了,但有何问题呢?

区分了组合对象和叶子对象,并进行有区别的对待,比如在Composite和Client里面,都需要区别对待这两种对象,这就是个问题。

对于这种具有整体与部分关系,并能组合成树型结构的对象结构,如何才能够以一个统一的方式来进行操作呢?

二、认识模式

1.模式定义

将对象组合成为属性结构以表示“整体-部分”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

2.解决思路

上述例子中,要区分组合对象和叶子对象,就是因为没有把组合对象和叶子对象统一起来。

组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来,用户使用时,始终是在操作组件对象,而不用再区分是在操作组合对象还是叶子对象。

3.模式图示

Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。

Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其它的子节点对象。

Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。

Client:客户端,通过组件接口来操作组合结构里面的组件对象。

4.模式原型示例代码

class Program
{
static void Main(string[] args)
{
//定义多个Composite对象
Component root = new Composite();
Component c1 = new Composite();
Component c2 = new Composite();
//定义多个叶子对象
Component leaf1 = new Leaf();
Component leaf2 = new Leaf();
Component leaf3 = new Leaf();

//组和成为树形的对象结构
root.AddChild(c1);
root.AddChild(c2);
root.AddChild(leaf1);

c1.AddChild(leaf2);
c2.AddChild(leaf3);

//操作Component对象
Component o = root.GetChildren(1);
Console.WriteLine(o);
}

/// <summary>
/// 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
/// </summary>
public abstract class Component
{
/// <summary>
/// 示意方法,子组件对象可能有的功能方法
/// </summary>
public abstract void SomeOperation();

/// <summary>
/// 向组合对象中加入组件对象
/// </summary>
/// <param name="component"></param>
public virtual void AddChild(Component component)
{
}

/// <summary>
/// 从组合对象中移出某个组件对象
/// </summary>
/// <param name="component"></param>
public virtual void RemoveChild(Component component)
{

}

/// <summary>
/// 返回某个索引对应的组件对象
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public virtual Component GetChildren(int index)
{

}
}

/// <summary>
/// 叶子对象,叶子对象不再包含其它子对象
/// </summary>
public class Leaf : Component
{
/// <summary>
/// 示意方法,叶子对象可能有自己的功能方法
/// </summary>
public override void SomeOperation()
{
// do something
}
}

/// <summary>
/// 组合对象,通常需要存储子对象,定义有子部件的部件行为,
/// 并实现在Component里面定义的与子部件有关的操作
/// </summary>
public class Composite : Component
{
/// <summary>
/// 用来存储组合对象中包含的子组件对象
/// </summary>
private List<Component> childComponents = null;

/// <summary>
/// 示意方法,通常在里面需要实现递归的调用
/// </summary>
public override void SomeOperation()
{
if (childComponents != null)
{
foreach (Component c in childComponents)
{
//递归的进行子组件相应方法的调用
c.SomeOperation();
}
}
}

public override void AddChild(Component component)
{
//延迟初始化
if (childComponents == null)
{
childComponents = new List<Component>();
}
childComponents.Add(component);
}

public override void RemoveChild(Component component)
{
if (childComponents != null)
{
childComponents.Remove(component);
}
}

public override Component GetChildren(int index)
{
if (childComponents != null)
{
if (index >= 0 && index < childComponents.Count)
{
return childComponents[index];
}
}
return null;
}
}
}

5.商品分类目录实例代码

class Program
{
static void Main(string[] args)
{
//定义所有的组合对象
Component root = new Composite("服装");
Component c1 = new Composite("男装");
Component c2 = new Composite("女装");
//定义所有的叶子对象
Component leaf1 = new Leaf("衬衣");
Component leaf2 = new Leaf("夹克");
Component leaf3 = new Leaf("裙子");
Component leaf4 = new Leaf("套装");
//按照树的结构来组合组合对象和叶子对象
root.AddChild(c1);
root.AddChild(c2);


c1.AddChild(leaf1);
c1.AddChild(leaf2);

c2.AddChild(leaf3);
c2.AddChild(leaf4);

//调用根对象的输出功能来输出整棵树
root.PrintStruct("");

Console.ReadKey();
}

public abstract class Component
{
public abstract void PrintStruct(string preStr);

public virtual void AddChild(Component component)
{
}

public virtual void RemoveChild(Component component)
{
}

public virtual Component GetChildren(int index)
{
return null;
}
}

public class Leaf : Component
{

private string name = null;

public Leaf(string name)
{
this.name = name;
}

public override void PrintStruct(string preStr)
{
Console.WriteLine(preStr + "-" + name);
}
}

public class Composite : Component
{
private string name = null;
private List<Component> childComponents = null;

public Composite(string name)
{
this.name = name;
}

public override void PrintStruct(string preStr)
{
Console.WriteLine(preStr + "+" + name);
if (childComponents != null)
{
preStr += " ";
foreach (var c in childComponents)
{
c.PrintStruct(preStr);
}
}
}

public override void AddChild(Component component)
{
if (childComponents == null)
{
childComponents = new List<Component>();
}
childComponents.Add(component);

}

public override void RemoveChild(Component component)
{
if (childComponents == null)
{
childComponents = new List<Component>();
}
childComponents.Remove(component);
}

public override Component GetChildren(int index)
{
if (childComponents != null)
{
if (index > 0 && index < childComponents.Count)
{
return childComponents[index];
}
}
return null;
}
}
}

三、理解模式

1.组合模式的目的

组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作

实现这个目标的关键之处,是设计一个抽象的组件类,让它可以代表组合对象和叶子对象。

2.对象树

组合模式会组合出树型结构,组成这个树型结构所使用的多个组件对象,就自然形成的对象树。

所有可以使用对象树来描述或操作的功能,都可以考虑组合模式。比如读取XML,或对语句进行语法解析等。

3.组合模式中的递归

组合模式中的递归,是对象本身的递归,是对象的组合方式,从设计上来讲是递归关联,是对象关联关系的一种。

4.安全性和透明性

在组合模式中,把组件对象分为两种:一种是可以包含子组件Composite对象;另一种不能包含其他组件对象的叶子对象。

Composite对象就像是一个容器,可以包含其他的Composite对或叶子对象。有了容器,就要对容器进行维护和管理。

这就产生了这样一个问题:在组合模式的类层次结构中,到底哪一些类里面定义这些管理子组件的操作,是应该在Component中声明这些操作呢,还是在Composite中声明这些操作?

这就需要仔细思考,在不同的实现中,进行安全性和透明性的权衡选择。

这里所说的安全性是指:从客户使用组合模式上看是否更安全。如果是安全的,那么不会有发生误操作的可能,能访问的方法都是被支持的功能。

这里所说的透明性是指:从客户使用组合模式上,是否需要区分到底是组合对象还是叶子对象。如果是透明的,那就是不再区分,对于客户而言,都是组件对象,具体的类型对于客户而言是透明的,是客户无需要关心的。

1.透明性的实现

如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无需关心具体的组件类型,这种实现方式就是透明性的实现。事实上,前面示例的实现方式都是这种实现方式。

但是透明性的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的,比如:增加、删除子组件对象。而客户不知道这些区别,对客户是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的。

组合模式的透明性实现,通常的方式是:在Component中声明管理子组件的操作,并在Component中为这些方法提供缺省的实现,如果是有子对象不支持的功能,缺省的实现可以是抛出一个例外,来表示不支持这个功能。

2.安全性的实现

如果把管理子组件的操作定义在Composite中,那么客户在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了,因为压根就没有这样的功能,这种实现方式是安全的。

但是这样一来,客户端在使用的时候,就必须区分到底使用的是Composite对象,还是叶子对象,不同对象的功能是不一样的。也就是说,这种实现方式,对客户而言就不是透明的了。

5.组合模式的优缺点

定义了包含基本对象和组合对象的类层次结构

在组合模式中,基本对象可以被组合成更复杂的组合对象,而组合对象又可以组合成更复杂的组合对象,可以不断地递归组合下去,从而构成一个统一的组合对象的类层次结构

统一了组合对象和叶子对象

在组合模式中,可以把叶子对象当作特殊的组合对象看待,为它们定义统一的父类,从而把组合对象和叶子对象的行为统一起来

简化了客户端调用

组合模式通过统一组合对象和叶子对象,使得客户端在使用它们的时候,就不需要再去区分它们,客户不关心使用的到底是什么类型的对象,这就大大简化了客户端的使用

更容易扩展

由于客户端是统一的面对Component来操作,因此,新定义的Composite或Leaf子类能够很容易的与已有的结构一起工作,而客户端不需要为增添了新的组件类而改变

很难限制组合中的组件类型

容易增加新的组件也会带来一些问题,比如很难限制组合中的组件类型。这在需要检测组件类型的时候,使得我们不能依靠编译期的类型约束来完成,必须在运行期间动态检测。

6.何时选用组合模式

建议在如下情况中,选用组合模式:

如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也简单

如果你希望统一的使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能

7.组合模式的本质

组合模式的本质:统一叶子对象和组合对象。

组合模式通过把叶子对象当成特殊的组合对象看待,从而对叶子对象和组合对象一视同仁,统统当成了Component对象,有机的统一了叶子对象和组合对象。

正是因为统一了叶子对象和组合对象,在将对象构建成树形结构的时候,才不需要做区分,反正是组件对象里面包含其它的组件对象,如此递归下去;也才使得对于树形结构的操作变得简单,不管对象类型,统一操作。

 
相关文章

UML建模之时序图
UML状态图
区分UML类图中的几种关系
UML建模之活动图介绍
 
相关文档

UML统一建模语言参考手册
网上商城UML图
UML建模示例:JPetStor
UML序列图编写规范
 
相关课程

UML与面向对象分析设计
UML + 嵌入式系统分析设计
业务建模与业务分析
基于UML和EA进行系统分析设计
 
分享到
 
 


如何向妻子解释OOD
OOAD与UML笔记
UML类图与类的关系详解
UML统一建模语言初学
总结一下领域模型的验证
基于 UML 的业务建模


面向对象的分析设计
基于UML的面向对象分析设计
UML + 嵌入式系统分析设计
关系数据库面向OOAD设计
业务建模与业务架构
使用用例进行需求管理


某航空IT部门 业务分析与业务建模
联想 业务需求分析与建模
北京航管科技 EA工具与架构设计
使用EA和UML进行嵌入式系统分析
全球最大的茶业集团 UML系统分析
华为 基于EA的嵌入式系统建模
水资源服务商 基于EA进行UML建模
更多...