web-dev-qa-db-ja.com

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

何かを行うという手順があるとします。

void doStuff(initalParams) {
    ...
}

「物事を行う」ことはかなり複雑な操作であることがわかりました。プロシージャが大きくなり、それを複数の小さなプロシージャに分割したところ、何かの処理中に何らかのstateを使用すると便利であることがわかり、小さなプロシージャ間で渡すパラメータを少なくする必要があります。だから、私はそれを独自のクラスに分解します:

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つだけあり、
  • 「外部で認識可能な」状態がない、つまりインスタンスの状態のみが必要duringその1つのエントリポイントの実行?
26
Heinzi

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

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

30
anon

問題のクラスは(単一のエントリポイントを使用して)うまく設計されていると思います。使い方も拡張も簡単です。かなりソリッド。

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

次のようなコードには問題がありません。

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

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

与えられたアプローチにはいくつかの問題があります:

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

関連する参考資料

おそらく 議論の主なポイント単一の責任の原則 をどこまで引き延ばすかにあります。 "その極端に行き、存在する理由が1つあるクラスを構築すると、クラスごとにメソッドが1つだけになる可能性があります。これにより、最も単純なクラスでも、クラスが大きく増加します。プロセスが原因で、システムが理解しにくくなり、変更が困難になります。」

このトピックに関連する別の関連参照:「1つの識別可能なタスクを実行するメソッドにプログラムを分割します。メソッドのすべての操作を同じレベルの抽象化に保ちます。」 - Kent Beck ここでの鍵は、「同じレベルの抽象化」です。それはしばしば解釈されるので、「1つのこと」を意味しません。このレベルの抽象化は、設計しているcontextに完全に依存します。

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

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

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にも反映されているように、最も適切なアプローチです。
8
Steven Jeuris

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

4
Scott Whitlock

より複雑で、パーソナライズされた他の回答 に加えて、次の観察は別の回答に値すると感じます。

あなたがフォローしている可能性がある兆候があります ポルターガイストのアンチパターン

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

症状と結果

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

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

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

2
Steven Jeuris
_new StuffDoer().Start(initialParams);
_

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

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

したがって、これはスレッドセーフではなく、軽量である(作成が高速で、外部リソースや大量のメモリを占有しない)場合、その場でインスタンス化しても問題ないという明確なドキュメントが必要です。

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

コストのかかる初期化がある場合、could(常にそうとは限らない)は、初期化を1回準備し、状態のみを含む別のクラスを作成してそれを複数回使用して、複製可能にすることは有益です。初期化すると、doerに格納される状態が作成されます。 start()はそれを複製して内部メソッドに渡します。

これにより、部分実行の状態を永続化するなど、他のことも可能になります。 (長時間の外部要因、たとえば電力供給の障害が実行を中断する可能性がある場合)。しかし、これらの追加の空想的なことは通常必要とされないので、価値はありません。

2
user470365

Martin Fowlerの EAAカタログのP ;には、いくつかのパターンがあります。

  1. トランザクションスクリプト :あなたが言及したアプローチにやや似ています。
  2. メソッドオブジェクトとメソッドを置き換える :@anonが述べたように、良いアプローチのようにも見えます。
0