web-dev-qa-db-ja.com

静的クラスを再設計して拡張可能にする方法は?

私はコンソールエクステンダープロジェクトに取り組んでおり、リファクタリングが必要だと感じるところまで来ています。

プロジェクトの唯一のパブリッククラスはExConsoleと呼ばれ、標準のConsoleクラスと同じくらい簡単に使用できるように、静的クラスでもありますが、私は考え始めています結局、それはそれほど素晴らしい考えではありません。

現在、コードは次のようになっています。

public static class ExConsole
{
    public static void Write(string markup) { /* ... */ }

    public static int Menu(string title, params string[] items) { /* ... */ }

    public static void ClearLastLines(int linesToClear) { /* ... */ }

    public static bool ReadBool(string title, ConsoleKey keyForTrue) { /* ... */ }

    // and so on...
}

私のコードに今見ている主な問題が2つあります。

  1. ExConsoleを拡張する唯一の方法は、継承によるものです。拡張メソッドを使用できれば、はるかに優先されます(もちろん、静的クラスでは不可能です)。

  2. コードはかなり大きくなり、私はそれを大きくすることを考えています(拡張するのは難しいので、多くの機能を意味する強力なベースラインが欲しいです。

ほとんどのメソッドを2つのグループに分けることができます。

1つのグループはコンソールに内容を書き込むためのものです(明確な行は書き込みと見なされます)、
と他のグループはコンソールからのものを読むためのものです。 (メニューは読書と見なされます)

だから私はこのようにExConsoleクラスを再構築することを考えています:

public static class ExConsole
{
    static ExConsole
    {
        Writer = new Writer();
        Reader = new Reader(Writer);
    }

    public static IWriter Writer {get;}
    public static IReader Reader {get;}
}

それはライターとリーダーの簡単な拡張メソッドを許可しますが、それはまた、使用をより厄介でちょっとばかげたものにします:

ExConsole.Writer.Write("seems silly, right?");
ExConsole.Reader.ReadInt("Please enter an integer");

ついに、私が今持っているものと比較して、

ExConsole.Write("You know you don't want to see code like `Writer.Write`, right?");
ExConsole.ReadInt("At least the `Reader.ReadInt` can be renamed to `Reader.Int`...");

それで、どのようにそれを再設計するように私にアドバイスしますか:

  1. できるだけ簡単に使用してください。
  2. 拡張を許可します。
  3. 大規模なクラス(約30-40のメソッド(オーバーロードを含む))になることはありません。

ヘレナのコメントに対処するには:

拡張機能を許可する理由は何ですか?提供したい拡張機能の例は何ですか?

2つの主な理由により、ユーザーがこのクラスを拡張できるようにしたいと思います。

  1. 含める必要があると思うすべてのものを追加する時間がありません。
  2. ユーザーが何を必要としているのか、事前にわかりません。

そのような拡張の例-ReadDateTimeメソッドの追加-タイトル、形式を表す文字列、およびおそらく文化情報を入力パラメーターとして、そしてDateTimeを戻り値として想像しています-次のようなもの:

public DateTime? ReadDateTime(string source, string format, IFormatProvider provider, DateTimeStyles style, string quitValue)
{
  /* 
    implementation: Show title, and loop until the user enters a value that either can be parsed to DateTime using the specified format or is the quitValue (and return null)
  */
}

あなたの例のすべてのメソッドはパブリックです、それらは外部モジュールから呼び出されますか、それとも代わりにプライベートにすることができますか?

それらは設計上公開されています。このプロジェクトが他のプロジェクトから参照される場合の目的は、コンソールアプリでの作業を容易にします。

Menu、ClearLastLines、またはReadBoolが使用されている例を挙げられますか?

Menuクラスの使用例:

static void Main(string[] args)
{
    var result = ExConsole.Menu(
            "Please select an action:", 
            "<c f='green'>Quit</c>", 
            "Do this", 
            "Do that",
            "Do the other thing");

    switch (result)
    {
        case 1:
            DoThis();
            break;
        case 2:
            DoThat();
            break;
        case 3:
            DoTheOtherThing();
            break;
        default:
            return;
    }
}

これにより、コンソールにPlease select an action:というタイトルのメニューが表示され、0. quitquitの前景色は緑色)で始まり、3. Do the other thingで終わる番号付きリストが表示されます。メニューから項目を選択するようユーザーに要求する行:Please select an item from the menu.ユーザーが0〜3の数値(両端を含む)を入力する場合のみ、Menuメソッドはユーザーが入力した値を返します。
ユーザーがオプションを選択すると、メニューがコンソール画面から消えます。

メニューメソッドは、実際にはExConsoleの3つのメソッド、WriteLineReadIntおよびClearLastLinesを使用します。

4
Zohar Peled

30-40メソッドはそうではありませんそれ多く-このようなクラスのsersの観点から。読み取りまたは書き込みを行う各メソッドにプレフィックスReadWriteを使用するなどの厳格な命名規則に準拠している限り、おそらく彼らはまだ簡単に探しているメソッド/関数を見つけるでしょう。また、ExConsole.Reader.ReadIntExConsole.ReadIntに比べてユーザーにメリットをもたらさないと思われる場合は、インターフェースをそのままにしてください。

別の質問は、ExConsoleが内部でどのように構成されているかです。それを小さなクラスに分割することが効果的である場合(そしてExConsoleをファサードにして、それらの内部クラスへの呼び出しを委任する場合)、またはその努力に値しない場合は、現在の実装の規模と複雑さに大きく依存します。メソッドはほとんど独立していて、1行または2行の実装ですか?または、小さなクラスにグループ化できる多くのプライベートなものを使用して、お互いに呼び出していますか?そのコードの保守と単体テストに本当に問題がありますか、それとも、「ボクおじさん」が本の中できれいなコードについて何かを書いたので、クラスを小さくする必要があると思っているだけですか。

管理する内部状態のない単純なメソッドが多数ある静的クラスの場合、1つのクラスに多数のメソッドがあることは、複雑な関数を持つステートフルクラスの状況と比較すると、通常それほど問題にはなりません。クラスを部分に分割すること自体が目的ではなく、目的を達成するための手段であり、「複雑さを管理する」ことです。しかし、それほど複雑でない場合は、おそらくこの楽器は必要ありません。

ただし、内部実装をもう少し構造化することが目標であると想定して、次のオプションを検討してください。

  • exConsoleを部分クラスにして、「Reader」部分を1つのファイルに、「Writer」部分を別のファイルに配置します。これは、コードをもう少し整理しておくのに役立ちます。

  • 選択したパーツのヘルパークラスを作成します。たとえば、メソッドMenuが特定の複雑さを持つ他のプライベートメソッドを呼び出す場合、MenuHelperクラスを作成してExConsole.Menuを実装することを検討してください

    Menu(string title, params string[] items){return menuHelper.Menu(title,items);}
    

    (コードをより保守しやすくすると思われる場合は、同じ方法を内部のReaderおよびWriterオブジェクトに使用できます)。

拡張性に関して:コアライブラリに入る "コア" ExConsoleと、おそらくチームの異なるメンバーによって書かれた複数のプログラムが必要であり、このコアに異なる拡張機能を追加する可能性があるようです。このシナリオでは、クラスに(現在)管理する状態がない場合でも、拡張メソッドを使用できるように、おそらく静的クラスを非静的クラスに置き換える必要があります。そのようなクラスの使用は、現在のアプローチの場合とほとんど同じくらい簡単です。ExConsole.ReadIntexConsole.ReadIntになります。ここで、exConsoleは、ExConsole型のオブジェクトです。

これらのExConsoleメソッドと拡張機能の唯一のメンテナーである他のシナリオ、またはチームのすべてのメンバーが「コア」ライブラリを直接変更できる場合は、ExConsoleに直接メソッドを追加して拡張機能を作成することを検討してください。継承を使用して静的クラスを拡張することは、そのようなクラスのポリモーフィズムを利用できないため、通常はあまりメリットがありません。確かに、派生クラスを実装するための基本クラスからprotectedヘルパー関数にアクセスするために使用できますが、これは要件ではないと思います。

6
Doc Brown
  1. できるだけ簡単に使用してください。

コードはオブジェクト指向ではなく、独立した静的ヘルパーメソッドのコレクションです。そして、それは大丈夫です。それは実際に使用法を非常に簡単にします、そしてあなたがオブジェクトを使用するために変更することからあなたが望むものを得るのを見ていません。

  1. 拡張を許可します。
  2. 大規模なクラス(約30-40のメソッド(オーバーロードを含む))になることはありません。

オブジェクトがない(そしてそれらを必要としないように見える)ので、いくつかの機能を追加する新しい静的クラスを作成することができます。

次のようになります。

public static class ExOutput
{
    public static void Write(string markup) { /* ... */ }

    public static int Menu(string title, params string[] items) { /* ... */ }

    public static void ClearLastLines(int linesToClear) { /* ... */ }

    // and so on...
}

public static class ExInput
{
    public static bool ReadBool(string title, ConsoleKey keyForTrue) { /* ... */ }

    // and so on...
}

拡張についても同様です。新しいコードが古いメソッドとあまり相互作用しない限り、継承を使用する理由はありません。新しい静的メソッドで新しいクラスを作成するだけです。

あなたが説明したシナリオから、OOPは、より簡単な方法で解決できなかった問題を解決しません。そうは言っても、OOPが便利な状況を見ることができます。-さまざまなタイプの出力で動作するライターが必要です。次に、WriterオブジェクトをTextWriterインスタンスで初期化します。 -一部のメソッドが他のメソッドを再利用する場合(例: 「メニュー」が内部で「書き込み」を呼び出す場合、「書き込み」を変更し、「メニュー」を暗黙的に変更することができます。

ただし、これらを有効にする場合は、明示的に計画する必要があります。オブジェクトを使用すれば、無料で拡張できるだけではありません。

ですから、私の理由は、特に理由がない限り、クラスをシンプルに保つことです。

2
Helena