C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2008/09/23 23:08

とても似ているメソッドの共通化

ロギングのような別の関心事に関する共通処理ならばアスペクト指向という手もある。
が、ここではそういうケースではなく、あくまでも同一の関心事であるケースについて考えてみようと思う。
同一の関心事であるケースとは、コアロジックに関係する事前処理・事後処理のことである。

基本的には、リンク先に書かれている方法で充分だ。しかし、各処理が複雑な場合、一つのクラスに大量のプライベートメソッドが出てくる。
これは共通の事前処理・事後処理があるかどうかは無関係な話だが、そうすると各処理をクラス化した方がよくなるわけだ。

各処理クラスは、共通の基本クラスから派生する。
そして事前処理・事後処理は基本クラスで実装し、テンプレートメソッドパターンを適用することで、派生クラスにはコアロジックの実装に専念させる。

引数、戻り値、例外の扱い方に色々な方法があるが、僕がお勧めする方法を例を交えて 1 つだけ紹介しようと思う。

まず、次のような 2 つのクラスがあるとする。(メソッドの中身は省略)

HogeService クラス
public class HogeService
{
    public void Open();
    public void Close();

    public void AddFuga(Fuga target);
    public IList<Fuga> GetFugas();
}

Fuga クラス
public class Fuga
{
    public int Id { get; }
}

HogeService クラスの AddFuga(Fuga target) や GetFugas() を使用する場合、次の制約がある。
  • 事前処理として、Open を呼び出さなければならない
  • 事後処理として、Close を呼び出さなければならない
このクラスを若干使いづらいと感じたので、委譲によるアダプターパターンを適用した、Hoge というラッパークラスを作ることにした。
Hoge クラスでは、Open・Close を隠蔽し、また ID を元に単一の Fuga を取得するメソッドを新たに提供することにした。

Hoge クラス
public class Hoge
{
    public void AddFuga(Fuga target)
    {
        HogeService service = new HogeService();
        service.Open();
        try
        {
            service.AddFuga(target);
        }
        finally
        {
            service.Close();
        }
    }
    public IList<Fuga> GetFugas()
    {
        HogeService service = new HogeService();
        service.Open();
        try
        {
            return service.GetFugas();
        }
        finally
        {
            service.Close();
        }
    }
    public Fuga GetFuga(int id)
    {
        HogeService service = new HogeService();
        service.Open();
        try
        {
            var findFugaById =
                from fuga in service.GetFugas()
                where (fuga.Id == id)
                select fuga;
            return findFugaById.Single();
        }
        finally
        {
            service.Close();
        }
    }
}

見ての通り、各処理は try ブロック内が異なるだけで他は全て同一の処理を行っている。

ではここで、各処理をクラス化してみよう。
まず、基本クラスである HogeOperation クラス。

HogeOperation クラス
internal abstract class HogeOperation
{
    public HogeOperation(HogeService service)
    {
        this._service = service;
    }

    protected readonly HogeService _service;

    protected void Processing()
    {
        this._service.Open();
        try
        {
            this.MainProcessing();
        }
        finally
        {
            this._service.Close();
        }
    }

    protected abstract void MainProcessing();
}

戻り値を持たない処理は、この HogeOperation を直接継承する。
また、外部から処理を実行できるよう、適切なパラメータを持つ Execute メソッドを用意する。受け取った引数は、MainProcessing メソッドで使用できるよう、フィールドに保持しておく。

AddFugaOperation クラス
internal sealed class AddFugaOperation : HogeOperation
{
    public AddFugaOperation(HogeService service)
        : base(service)
    {
    }

    private Fuga _target;

    public void Execute(Fuga target)
    {
        this._target = target;
        return this.Processing();
    }

    protected override void MainProcessing()
    {
        this._service.AddFuga(this._target);
    }
}

続いて、戻り値を持つ処理の基本クラスとなる、HogeOperation<TResult> クラス。
このクラスは HogeOperation クラスを継承するのだが、new 修飾子を使用して、Processing メソッドが戻り値を返すように再定義を行っている。MainProcessing メソッドも戻り値を返せるようにする必要があるが、抽象メソッドなので new 修飾子による再定義はできない。代わりに、out パラメータとして戻り値を返せるバージョンの MainProcessing メソッドを新たに定義している。

HogeOperation<TResult> クラス
internal abstract class HogeOperation<TResult> : HogeOperation
{
    public HogeOperation(HogeService service)
        : base(service)
    {
    }

    private TResult _result;

    protected new TResult Processing()
    {
        base.Processing();
        return this._result;
    }

    protected override void MainProcessing()
    {
        this.MainProcessing(out this._result);
    }

    protected abstract void MainProcessing(out TResult result);
}

戻り値を持つ処理は、この HogeOperation<TResult> クラスを継承する。
こちらも、外部から処理を実行できるよう、適切なパラメータを持つ Execute メソッドを用意する。

GetFugasOperation クラス
internal sealed class GetFugasOperation : HogeOperation<IList<Fuga>>
{
    public GetFugasOperation(HogeService service)
        : base(service)
    {
    }

    public IList<Fuga> Execute()
    {
        return this.Processing();
    }

    protected override void MainProcessing(out IList<Fuga> result)
    {
        result = this._service.GetFugas();
    }
}

GetFugaOperation クラス
internal sealed class GetFugaOperation : HogeOperation<Fuga>
{
    public GetFugaOperation(HogeService service)
        : base(service)
    {
    }

    private int _id;

    public Fuga Execute(int id)
    {
        this._id = id;
        return this.Processing();
    }

    protected override void MainProcessing(out Fuga result)
    {
        var findFugaById =
            from fuga in this._service.GetFugas()
            where (fuga.Id == this._id)
            select fuga;

        result = findFugaById.Single();
    }
}

これらの処理クラスを使用することで、Hoge クラスは次のようにシンプルなコードとなる。

Hoge クラス
public class Hoge
{
    public void AddFuga(Fuga target)
    {
        HogeService service = new HogeService();
        new AddFugaOperaion(service).Execute(target);
    }
    public IList<Fuga> GetFugas()
    {
        HogeService service = new HogeService();
        return new GetFugasOperaion(service).Execute();
    }
    public Fuga GetFuga(int id)
    {
        HogeService service = new HogeService();
        return new GetFugaOperaion(service).Execute(id);
    }
}

以上で処理のクラス化は完了となる。
// 追記 (2008/09/24)
HogeOperation<TResult> クラスは、ややトリッキーだったかもしれない。別解を記事にしたのでこちらも参照して頂きたい。
戻り値を持つ処理のクラス化の別解
// 追記ここまで


さて、最初にも書いたが、各処理が複雑でない内は、デリゲートの注入で充分だろう。この例では各処理が複雑なものではないため、各処理をクラス化したことによって逆に複雑になってしまっている。だが、処理が複雑化してきたときには、処理をクラス化することは非常に有効である。これは単一責任の原則を適用するということだ。例えば Hoge クラスに何らかの状態を持たせたりする場合、状態操作の処理が Hoge クラス内で行われる。各処理で行われる複雑な処理は、処理クラスに切り離されているため、Hoge クラスが責任を持つべき処理だけが Hoge クラス内に残るわけである。


ところで、HogeOperation クラス、HogeOperation<TResult> クラスには、protected な Processing メソッドを用意して、外部から処理を実行させるための Execute メソッドは実際の処理クラスに用意しているが、なぜわざわざこのようなことをしているのか。
例えば、引数はコンストラクタなり引数設定用のメソッドなりで設定できるようにしてしまえば、Processing メソッドの代わりに public な Execute メソッドを基本クラスに用意できるだろう。
しかし、そうすると各処理がスローする可能性のある例外を明示する時、つまり、XML コメントの再定義で困る。結局 XML コメントを再定義するためには Execute メソッドをオーバーライドするなり new 修飾子で再定義するなりしなければならない。ならば、初めから Execute メソッドは処理クラスで定義しなければならないようにした方が良いと考えたわけだ。
無論、XML コメントの再定義を行わないつもりならば、引数はコンストラクタなり引数設定用のメソッドなりで設定できるようにして、基本クラスで Execute メソッドを定義してしまっても良い。
引数設定用のメソッドを用意するのなら、フルエントインターフェイスを意識すると良い。
当初はこういった別の実装方法も取り上げるつもりだったのだが、収拾がつかなくなってしまったため省略した。


Hogeというラッパークラスですが、
最初のものもシンプルなコードになったものも、各メソッドで
HogeService service = new HogeService();
新たなHogeServiceインスタンスを作ってしまっているので、
現状ではGetFugas、GetFuga共に、戻り値なしですよね。AddFugaはデータ溜まらず。

クラス内にprivateインスタンス作って各メソッドではそれを渡す事になるのでしょうか?
以下のように。
private HogeService _service = new HogeService();
public void AddFuga(Fuga target)
{
new AddFugaOperation(_service).Execute(target);
}

2010.09.16 16:57 URL | 通りすがり #- [ 編集 ]


あーなるほど、すみません誤解を招いてしまったようです。
HogeService はデータベースなどにデータを永続化するという前提で書いていました。
なので HogeService 自体は毎回 new しても問題ない、ということです。
もちろん、ケースバイケースでprivateインスタンスを作ってしまった方が良い場合もありますね。(クローズ後の再オープンが不可な場合は毎回 new する必要がありますね。)

2010.09.16 17:10 URL | よこけん #Ay6tTHf6 [ 編集 ]












トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/230-a05321fc