1つのことだけを行うクラスのパターン


24

のは、私が手続き持っていると言うものを行うに

void doStuff(initalParams) {
    ...
}

今、私は「何かをする」ことは非常に複雑な操作であることを発見しました。手順は大きくなり、複数の小さな手順に分割し、すぐに何かを行うときに何らかの状態を持つことが有用であることに気付きました。したがって、私はそれを独自のクラスに分解します:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

そして、私はこれを次のように呼び出します:

new StuffDoer().Start(initialParams);

またはこのように:

new StuffDoer(initialParams).Start();

そして、これが間違っていると感じるものです。.NETまたはJava APIを使用するとき、私は常にを決して呼び出さないためnew SomeApiClass().Start(...);、間違っていると思われます。確かに、StuffDoerのコンストラクターをプライベートにし、静的ヘルパーメソッドを追加できます。

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

しかし、その場合、外部インターフェイスが1つの静的メソッドのみで構成されるクラスがあり、これも奇妙に感じます。

したがって、私の質問:このタイプのクラスには定評のあるパターンがありますか?

  • エントリポイントが1つしかない
  • 「外部的に認識可能な」状態がありません。つまり、インスタンス状態はその1つのエントリポイントの実行中にのみ必要です。

私がbool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");気にかけたのは、特定の情報とLINQ拡張メソッドが利用できないということだけでした。うまく機能し、if()フープを飛び越える必要なしに条件に収まります。もちろん、そのようなコードを書いている場合は、ガベージコレクションされた言語が必要です。
CVn

言語に依存しない場合、なぜjavac#も追加するのですか?:)
スティーブンジュリス

私は興味がありますが、この「1つの方法」の機能は何ですか?特定のシナリオで最適なアプローチが何であるかを判断するのに大いに役立ちますが、それでも興味深い質問です!
スティーブンジュリス

@StevenJeuris:さまざまな状況でこの問題に出くわしましたが、現在のケースでは、ローカルデータベースにデータをインポートする(Webサーバーからデータを読み込み、操作を行ってデータベースに保存する)メソッドです。
ハインジ

1
インスタンスを作成するときに常にメソッドを呼び出す場合、コンストラクター内で初期化ロジックにしないでください。
NoChance

回答:


29

メソッドオブジェクトと呼ばれるパターンがあり、多数の一時変数/引数を持つ単一の大きなメソッドを別のクラスに分解します。ローカル状態(パラメーターと一時変数)へのアクセスが必要であり、インスタンス変数を使用してローカル状態を共有することはできないため、メソッドの一部を個別のメソッドに抽出するのではなく、これを行います。 (およびそこから抽出されたメソッド)のみであり、残りのオブジェクトでは使用されません。

その代わり、メソッドは独自のクラスになり、パラメーターと一時変数はこの新しいクラスのインスタンス変数になり、メソッドは新しいクラスの小さなメソッドに分割されます。通常、結果のクラスには、クラスがカプセル化するタスクを実行するパブリックインスタンスメソッドが1つだけあります。


1
これは私がやっていることのように聞こえます。おかげで、少なくとも今は名前があります。:
ハインジ

2
非常に関連性の高いリンク!私にはアンチパターンのような匂いがします。メソッドがそれほど長い場合、おそらくその一部を再利用可能なクラスで抽出するか、一般的にはより良い方法でリファクタリングできますか?また、このパターンについても聞いたことがありません。他の多くのリソースを見つけることもできません。
スティーブンジュリス


1
@StevenJeurisアンチパターンだとは思わない。アンチパターンは、インスタンス変数のこれらのメソッド間で一般的なこの状態を保存するのではなく、多くのパラメーターでリファクタリングされたメソッドを混乱させることです。
アルフレドオソリオ

@AlfredoOsorio:リファクタリングされたメソッドを多くのパラメーターで混乱させます。それらはオブジェクト内に存在するため、それらをすべて渡すよりも優れていますが、各パラメーターの限られたサブセットのみを必要とする再利用可能なコンポーネントにリファクタリングするよりも悪いです。
Jan Hudec

13

問題のクラス(単一のエントリポイントを使用)は適切に設計されていると思います。使いやすく、拡張も簡単です。かなり固い。

についても同じですno "externally recognizable" state。それは良いカプセル化です。

次のようなコードで問題は発生しません。

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
もちろん、同じものをdoer再度使用する必要がない場合は、になりvar result = new StuffDoer(initialParams).Calculate(extraParams);ます。
CVn

Calculateの名前をdoStuffに変更する必要があります!
shabunc

1
クラスを使用する(おそらくファクトリを使用する)クラスを初期化(新規)したくない場合は、ビジネスロジックのどこかでオブジェクトを作成し、パラメータを直接渡すオプションがあることを追加します。あなたが持っている単一のパブリックメソッドに。もう1つのオプションは、ファクトリを使用し、パラメータを取得し、コンストラクタを介してすべてのパラメータを使用してオブジェクトを作成するmakeメソッドを使用することです。どこからでもパラメーターなしでパブリックメソッドを呼び出すよりも。
パトコスチャバ

2
この答えは非常に単純です。StringComparer使用するクラスはnew StringComparer().Compare( "bleh", "blah" )適切に設計されていますか?まったく必要がないときに状態を作成するのはどうですか?さて、動作はうまくカプセル化されています(少なくとも、クラスのユーザーにとっては、私の回答詳しく説明しています)が、デフォルトでは「良いデザイン」にはなりません。「実行結果」の例について。これは、とはdoer別に使用する場合にのみ意味がありますresult
スティーブンジュリス

1
存在する理由の1つではありませんone reason to change。違いは微妙に見えるかもしれません。その意味を理解する必要があります。1つのメソッドクラスを作成することはありません
jgauffin

8

固い原則の観点から、jgauffinの答えは理にかなっています。ただし、情報の非表示など、一般的な設計原則を忘れないでください。

特定のアプローチにはいくつかの問題があります。

  • あなた自身が指摘したように、作成されたオブジェクトが状態を管理しない場合、人々は「新しい」キーワードを使用することを期待しません。あなたのデザインはその意図を反映しています。クラスを使用している人々は、それが管理する状態について混乱し、メソッドへの後続の呼び出しが異なる動作を引き起こす可能性があるかどうかについて混乱するかもしれません。
  • クラスを使用する人の観点からは、内部状態はよく隠されていますが、クラスを変更したり、単に理解したい場合は、より複雑になります。私はすでにちょうどそれらを小さくする分割方法で私が見る問題について多くのことを書かれているクラスのスコープに状態を移動する場合は特に、。より小さな関数を使用するためだけに、APIの使用方法を変更しています!私の意見では、それは間違いなく行き過ぎです。

関連する参考文献

おそらく議論の主なポイントは単一責任原則をどこまで拡大するかにあります。「それを極端に進めて、存在する理由が1つあるクラスを構築すると、クラスごとに1つのメソッドしか作成できなくなる可能性があります。理解するのが難しく、変えるのが難しい。」

このトピックに関連するもう1つの関連参照:「1つの識別可能なタスクを実行するメソッドにプログラムを分割します。メソッド内のすべての操作を同じ抽象化レベルに保ちます。」- ここでのケントベックキーは「同じレベルの抽象化」です。しばしば解釈されるので、それは「一つのこと」を意味するものではありません。このレベルの抽象化は、設計対象のコンテキスト次第です。

それでは、適切なアプローチは何ですか?

あなたの具体的なユースケースを知らなければ、それを伝えるのは難しいです。同様のアプローチを使用することがある(あまり頻繁ではない)シナリオがあります。この機能をクラススコープ全体で利用できるようにすることなく、データセットを処理する場合。私はそれについてのブログ記事を書きました、ラムダがカプセル化をさらに改善する方法。私もここでプログラマのトピックに関する質問を始めました。以下は、このテクニックを使用した最新の例です。

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

内の非常に「ローカル」なコードForEachは他のどこでも再利用されないので、関連する正確な場所に単純に保持できます。私の意見では、互いに依存するコードが強くグループ化されるような方法でコードをアウトライン化すると、読みやすくなります。

可能な代替案

  • C#では、代わりに拡張メソッドを使用できます。したがって、この「1つのこと」メソッドに渡す引数を直接操作します。
  • この関数が実際に別のクラスに属していないかどうかを確認してください。
  • それ作る静的クラスの静的関数。これが最も適切なアプローチである可能性が最も高く、参照した一般的なAPIにも反映されています。

別の回答であるPoltergeistsで概説されているように、このアンチパターンの名前の候補を見つけました。
スティーブンジュリス

4

私はそれはかなり良いと言いますが、それはユーザーが期待していることなので、あなたのAPIはまだ静的クラスの静的メソッドでなければなりません。静的メソッドがnewヘルパーオブジェクトを作成して作業を行うために使用するという事実は、それを呼び出している人から隠されるべき実装の詳細です。


うわー!あなたはなんとか私よりもはるかに簡潔に理解することができました。:)ありがとう。(もちろん、私たちが反対するかもしれないところを少し進めますが、私は一般的な前提に同意します)
スティーブンジュリス

2
new StuffDoer().Start(initialParams);

共有インスタンスを使用して使用できる無知な開発者に問題があります

  1. 複数回(前の(おそらく部分的な)実行が後の実行を台無しにしない場合はOK)
  2. 複数のスレッドから(OKではない、明示的に内部状態があると言った)。

そのため、スレッドセーフではなく、軽量(作成が高速で、外部リソースやメモリを大量に使用しない)である場合、その場でインスタンス化してもよいという明示的なドキュメントが必要です。

静的メソッドで非表示にすると、インスタンスの再利用が発生しないため、これに役立ちます。

それはいくつかの高価な初期化を持っている場合、可能性があり(それは常にではない)だけの状態が含まれているし、それが複製可能になるだろう別のクラスを作成することにより、一度それを初期化し、それを複数回使用して準備することが有益で。初期化により、に保存される状態が作成されdoerます。start()クローンを作成し、内部メソッドに渡します。

これにより、部分実行の状態を永続化するような他のことも可能になります。(電力供給の失敗などの外部要因が長くかかる場合、実行が中断される可能性があります。)しかし、これらの追加の派手なものは通常必要ないので、価値がありません。


良い点。クリーンアップを気にしないでください。
スティーブンジュリス

2

ほかに私のより精巧な、パーソナライズされた他の回答、私は次の観察は別の答えに値する感じます。

Poltergeistsのアンチパターンに従っている可能性があります。

Poltergeistsは、非常に限られた役割と効果的なライフサイクルを持つクラスです。多くの場合、他のオブジェクトのプロセスを開始します。リファクタリングされたソリューションには、Poltergeistsを排除する、長寿命のオブジェクトへの責任の再割り当てが含まれます。

症状と結果

  • 冗長ナビゲーションパス。
  • 一時的な関連付け。
  • ステートレスクラス。
  • 一時的な短期オブジェクトおよびクラス。
  • 一時的な関連付けを介して他のクラスを「シード」または「呼び出す」ためにのみ存在する単一操作クラス。
  • start_process_alphaなどの「コントロールのような」操作名を持つクラス。

リファクタリングされたソリューション

ゴーストバスターズは、ポルターガイストをクラス階層から完全に削除することで解決します。ただし、削除した後は、ポルターガイストによって「提供された」機能を置き換える必要があります。これは、アーキテクチャを修正するための簡単な調整で簡単です。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.