IExtenderProviderで実装するCommandパターン

普段よく使っている設計パターンをさらしてみる。

GoFデザインパターンにCommandパターンというのがある。「C#デザインパターン」の説明によると

Commandは、特定のアクションに対する要求をオブジェクトの中に封じ込め、このオブジェクトを公開された
パブリックなインターフェースに与えます。このパターンを使うと、実際に実行されるアクションについて
何も知らなくても要求を出せる機能をクライアントに与えられます。

要するにアクションの呼び出し元と実際のアクションを分離するのが目的という事かな。
UIの各イベントと実行されるアクションを切り離すのによく利用すると思う。あとアクションが単一のクラスにカプセル化される為、Undoの実装なんかも容易になる。(何をしたかを自身がわかってるから)

コマンドクラス

一番単純なCommandパターンとしては、まず「ICommand」というインターフェースを定義する。

public interface ICommand {
    void Execute();
}

このインターフェースは「Execute」というメソッドのみ。このメソッドでアクションを実行する。

とりあえず、このインターフェースを実装した「ExitCommand」というクラスを定義する。

public class ExitCommand : ICommand {
    public ExitCommand() {
    }

    public void Execute() {
        System.Windows.Forms.Application.Exit();
    }
}

コマンドが実行されると、アプリケーションを終了する。

次はこのコマンドをUIコンポーネントに関連付ける。例えばボタンに関連付ける場合、「Button」クラスを継承して「CommandButton」クラスを定義する。

public class CommandButton : System.Windows.Forms.Button {
    private ICommand command;
    
    public ICommand Command {
        get { return command; }
        set { command = value; }
    }
    
    public CommandButton() { }
    
    override void OnClick(EventArgs e) {
        base.OnClick(e);
        
        if(Command != null) Command.Execute();
    }
}

このクラスは「ICommand」のインスタンスを保持し、ボタンがクリックされるとコマンドを実行する。

あとはこの「CommandButton」をUIデザイナからフォームに貼り付けて、フォームのコンストラクタで「ExitCommand」のインスタンスを割り当てるだけ。

public partial class MainForm : System.Windows.Forms.Form {
    public MainForm() {
        InitializeComponent();
        
        btnExit.Command = new ExitCommand();
    }
}

とこんな感じでやるわけだが、これにはいくつかの問題がある。

  1. コマンドを関連付けたいUIコンポーネントの数だけこういったクラスを作る必要がある
  2. コマンドとUIコンポーネントの関連付けをフォームがやっている
  3. コマンドの種類とインスタンス化の方法をフォームが知っている必要がある

要はフォームが余計な事をやりすぎているということだ。コマンドの数が増えてくるとこの部分のコードが異臭を放ってくる。

これを解決するために、コマンドとUIコンポーネントの関連付けを行うクラスを作る。
このクラスはUIコンポーネントにコマンドを関連付け、UIコンポーネントで特定のイベントが発生すると関連付けられたコマンドを実行する。UIコンポーネントとコマンドの橋渡し(仲介)を行う。こういうのをMediator(仲介者)と呼ぶ。

こういうクラスは「IExtenderProvider」インターフェースを使うと効果的に実装できる。「IExtenderProvider」はUIコンポーネントを継承しなくても、UIコンポーネントに対してプロパティを追加することができる。

これを使えば、既存のUIコンポーネントを直接拡張しなくてもコマンドとの関連付けを行うプロパティを定義できるわけだ。といっても、UIデザイナ上でプロパティにコマンドのインスタンスを直接割り当てる事はできないので、コマンドにコマンド名というラベルを付けて、そのコマンド名をUIコンポーネントに関連付ける事にする。

まずはMediatorクラスを実装する前にコマンドにコマンド名をラベル付けするコマンドのファクトリクラスを作る。

コマンドのファクトリクラス

public interface ICommandFactory {
    ICommand GetCommand(string name);
}

コマンド名を渡すとそれに割り当てられたコマンドのインスタンスを返すだけ。

そしてこのインターフェースを実装するクラス

public class CommandFactory : ICommandFactory {
    public CommandFactory() { }

    public ICommand GetCommand(string name) {
        if(name == "exit") return new ExitCommand();

        return null;
    }
}

ここでは単純に「exit」というコマンド名がきたら、「ExitCommand」のインスタンスを返すだけ。

Mediatorクラス

UIデザイナ上に配置するために「Component」クラスを継承して、「IExtenderProvider」インターフェースを実装する。

using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.ComponentModel;

[ProvideProperty("CommandName", typeof(Component))]
public class CommandExecuteMediator : Component, IExtenderProvider {
    private ICommandFactory commandFactory;
    /// <summary>
    /// コンポーネントとコマンド名のマップ
    /// </summary>
    private Dictionary<Component, string> commandNames = new Dictionary<Component, string>();

    public ICommandFactory CommandFactory {
        private get { return commandFactory; }
        set { commandFactory = value; }
    }

    public CommandExecuteMediator() { }
    public CommandExecuteMediator(IContainer container) {
        container.Add(this);
    }

    public bool CanExtend(object extendee) {
        // 自分自身は無視
        if(extendee is CommandExecuteMediator) return false;

        return extendee is Button;
    }

    public void SetCommandName(Component component, string commandName) {
        if(commandNames.ContainsKey(component)) commandNames.Remove(component);

        commandNames.Add(component, commandName);

        if(component is Button) {
            ((Button)component).Click -= new EventHandler(CommandExecuteMediator_Click);
            ((Button)component).Click += new EventHandler(CommandExecuteMediator_Click);
        }
    }

    [DefaultValue("")]
    [Category("コマンド"), Description("コンポーネントに割り当てるコマンド名です。")]
    public string GetCommandName(Component component) {
        return commandNames.ContainsKey(component) ? commandNames[component] : string.Empty;
    }

    private void CommandExecuteMediator_Click(object sender, EventArgs e) {
        string commandName = GetCommandName((Component)sender);

        if(!string.IsNullOrEmpty(commandName)) {
            ICommand command = CommandFactory.GetCommand(commandName);

            if(command != null) command.Execute();
        }
    }
}

「IExtenderProvider」インターフェースは「CanExtend」メソッドを実装するだけ。
このメソッドで拡張するUIコンポーネントをふるいにかける。ここでは「Button」クラスでだけtrueを返すようにする。

拡張するプロパティは「ProvideProperty」属性で指定し、プロパティ名の頭に「Get」「Set」を付加した名前のメソッドを定義する必要がある。ここでは「SetCommandName」メソッドで「Button」の「Click」イベントにイベントハンドラを追加し、関連付けられたコマンドを実行し、「GetCommandName」メソッドでUIコンポーネントに関連付けられたコマンド名を返す。UIコンポーネントとコマンド名の関連付けは「Dictionary」に格納する。

あとはこの「CommandExecuteMediator」をUIデザイナからフォームに貼り付けて、フォームのコンストラクタで「CommandFactory」のインスタンスを割り当てる。

public partial class MainForm : System.Windows.Forms.Form {
    public MainForm() {
        InitializeComponent();
        
        commandExecuteMediator.CommandFactory = new CommandFactory();
    }
}

フォームに適当にボタンを配置して、ボタンのプロパティを表示すると「CommandName」というプロパティが追加されている。

ここに「exit」と設定する。

これでアプリケーションを実行してボタンをクリックすると「ExitCommand」が実行されるようになる。こんな感じで「IExtenderProvider」インターフェースを使うと、既存のUIコンポーネントの振る舞いを全く変えずに新しい機能を追加する事ができる。

もちろんこれだけではまだまだ不十分で、コマンドのインスタンス化とラベル付けは、全てSpring.NETなどのDIコンテナに任せる。

Spring.NETを使う

コマンドをオブジェクト定義ファイルで定義する。

<?xml version="1.0" encoding="utf-8"?>
<objects xmlns="http://www.springframework.net">
    <object name="exit" singleton="false" type="ExitCommand" />
</objects>

そして「CommandFactory」はコマンドのインスタンスをDIコンテナから取得するように変える。

public class CommandFactory : ICommandFactory, Spring.Context.IApplicationConetxtAware {
    private IApplicationContext context;
    
    public IApplicationContext ApplicationContext {
        get { return context; }
        set { context = value; }
    }
    
    public CommandFactory() { }

    public ICommand GetCommand(string name) {
        return ApplicationContext.ContainsObject(name) ?
            ApplicationContext.GetObject(name) as ICommand : null;
    }
}

これでコマンドのインスタンス化を外に追いやることができた。こうすることで、コマンドに対していろんなオブジェクトをプロパティ経由で渡す事ができる。

このやり方は結構使えると思うんだけど、どうでしょう?