web-dev-qa-db-ja.com

クラスの複雑さを軽減する

answers をいくつか見て、Googleで検索しましたが、役立つ情報は見つかりませんでした(つまり、厄介な副作用はありません)。

私の問題は、要約すると、オブジェクトがあり、longの操作シーケンスを実行する必要があることです。自動車を作るような、一種の組立ラインだと思います。

これらのオブジェクトは メソッドオブジェクト と呼ばれると思います。

そのため、この例では、ある時点でCarWithoutUpholsteryを作成し、その上でinstallBackSeat、installFrontSeat、installWoodenInsertsを実行する必要があります(操作は互いに干渉せず、並列で実行されることもあります)。これらの操作はCarWithoutUpholstery.worker()によって実行され、CarWithUpholsteryとなる新しいオブジェクトを生成します。このオブジェクト上で、cleanInsides()、verifyNoUpholsteryDefects()などを実行します。

単一フェーズでの操作はすでに独立しています。つまり、私はすでに、それらのサブセットに取り組んでいます。これは、任意の順序で実行できます(フロントシートとリアシートは任意の順序でインストールできます)。

私のロジックは現在、実装を簡単にするためにリフレクションを使用しています。

つまり、CarWithoutUpholsteryを取得すると、オブジェクトは、performSomething()と呼ばれるメソッドについてそれ自体を検査します。その時点で、次のすべてのメソッドを実行します。

_myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...
_

エラーやものをチェックしながら。操作の順序は重要ではありませんが、結局、何らかの順序が重要であることがわかった場合に備えて、辞書式順序を割り当てました。 これは [〜#〜] yagni [〜#〜] と矛盾しますが、コストは非常に低く、単純なsort()であり、メソッドの大規模な名前変更(またはテストを実行する他のいくつかのメソッドの導入)を節約できます、たとえばメソッドの配列).

別の例

車を作る代わりに、誰かに関する秘密警察の報告書を編集し、それを私の Evil Overlord に提出しなければならないとしましょう。最後のオブジェクトはReadyReportです。それを構築するために、基本的な情報(名前、姓、配偶者...)を収集することから始めます。これは私のフェーズAです。配偶者の有無に応じて、フェーズB1またはB2に進み、1人または2人の性に関するデータを収集する必要があります。これは、ナイトライフ、ストリートカム、セックスショップの売上レシートなどを制御するさまざまなEvil Minionsに対するいくつかの異なるクエリで構成されています。などなど。

被害者に家族がいない場合、私はGetInformationAboutFamilyフェーズに入ることもしませんが、そうする場合、最初に父親か母親か兄弟(もしあれば)をターゲットにするかどうかは関係ありません。しかし、FamilyStatusCheckを実行していない場合はそれを実行できません。したがって、これは以前のフェーズに属します。

すべてうまくいきます...

  • 追加の操作が必要な場合は、プライベートメソッドを追加するだけです。
  • 操作がいくつかのフェーズに共通している場合、スーパークラスから継承させることができます。
  • 操作はシンプルで自己完結型です。 1つの操作の値が他の操作で必要になることはありません(実行の操作は別のフェーズで実行されます)。
  • 作成者オブジェクトがこれらの条件を最初に確認していなかった場合、存在さえできなかったため、将来のオブジェクトは多くのテストを実行する必要はありません。つまり、ダッシュボードにインサートを配置し、ダッシュボードをクリーニングし、ダッシュボードを確認するときに、ダッシュボードが実際にあるであることを確認する必要はありません。
  • それは簡単なテストを可能にします。部分オブジェクトを簡単にモックして、その上で任意のメソッドを実行できます。すべての操作は確定的なブラックボックスです。

...だが...

問題は、メソッドオブジェクトの1つに最後の操作を1つ追加したときに発生しました。これにより、モジュール全体が必須の複雑度インデックス(「N未満のプライベートメソッド」)を超えました。

私はすでにこの問題を2階で取り上げており、この場合、プライベートメソッドの豊富さは災害の兆候ではないことを示唆しました。複雑さisがありますが、操作複雑であるため実際にありますが、実際にはそれほど複雑ではありません-longです。

邪悪な君主の例を使用すると、私の問題は、邪悪な君主(別名否定してはならない彼)がall食事情報を要求したことです。レストラン、簡易キッチン、露天商、無認可の露天商、温室の所有者など、そして悪(サブ)オーバーロードのクエリ-としても知られています-あまりにも多くのパフォーマンスをしていると不平を言うGetDietaryInformationフェーズのクエリ。

Note:いくつかの観点から、これはまったく問題ではないことを認識しています(パフォーマンスの問題などを無視して)。起こっていることはすべて、特定の測定基準が不幸であり、そのための正当化があるということです。

何が思うできる

最初のものを除いて、これらのオプションはすべて実行可能であり、防御可能だと思います。

  • 私はこっそりと私のメソッドの半分を宣言できることを確認しましたprotected。しかし、私はテスト手順の弱点を悪用しているので、捕まったときに自分自身を正当化することは別として、私はこれが好きではありません。また、それは応急措置です。必要な操作の数が2倍になった場合はどうなりますか?可能性は低いですが、それではどうでしょうか。
  • このフェーズを任意にAnnealedObjectAlpha、AnnealedObjectBravo、AnnealedObjectCharlieに分割し、各ステージで3分の1の操作を実行できます。これは実際には複雑さ(N-1多いクラス)を追加するという印象を受けており、テストに合格する以外に利点はありません。もちろん、CarWithFrontSeatsInstalledとCarWithAllSeatsInstalledが論理的に連続したステージであると私は考えることができます。後でAlphaがBravoメソッドを必要とするリスクは小さく、うまく使えばさらに小さくなります。それでも。
  • リモートで類似したさまざまな操作を1つの操作にまとめることができます。 performAllSeatsInstallation()。これはあくまで暫定的な措置であり、単一操作の複雑さが増します。操作AとBを異なる順序で実行する必要があり、それらをE =(A + C)とF(B + D)内にパックした場合、EとFをアンバンドルしてコードをシャッフルする必要があります。
  • ラムダ関数の配列を使用して、チェックを完全に回避できますが、それは不格好です。ただし、これはこれまでのところ最良の代替手段です。それは反射を取り除くでしょう。私が抱えている2つの問題は、仮説CarWithEngineInstalledだけでなく、allメソッドオブジェクトを書き換えるように求められることです。これは非常に優れたジョブセキュリティですが、実際にはそんなにアピールする。また、コードカバレッジチェッカーには、ラムダを使用した問題があります(これは解決可能ですが、はまだです)。

そう...

  • あなたは、どちらが私の最良の選択肢だと思いますか?
  • 私が考慮していないより良い方法はありますか? (多分私はきれいに来て直接尋ねるほうがいいですそれは何ですか?
  • このデザインにはどうしようもない欠陥があり、私は敗北と溝を認めたほうがいいでしょう-このアーキテクチャは完全にそうですか? 私のキャリアにとっては良くありませんが、長期的には、設計が不適切なコードを書く方が良いでしょうか?
  • 私の現在の選択は実際には1つの真の方法であり、より良い品質の指標(および/または計測)をインストールするために戦う必要がありますか?この最後のオプションについては、参照が必要です...をつぶやく間、@ PHBで手を振るだけでは不十分です。これらは、探しているメトリックではありません。 いくらでもやりたい
10
LSerni

操作の長いシーケンスは、独自のクラスである必要があるように思われます。そのため、その職務を実行するには追加のオブジェクトが必要です。あなたはこのソリューションに近いようです。この長い手順は、複数のステップまたはフェーズに分割できます。各ステップまたはフェーズは、パブリックメソッドを公開する独自のクラスにすることができます。各フェーズで同じインターフェースを実装する必要があります。次に、Assemblyラインはフェーズのリストを追跡します。

車のアセンブリの例を基にするには、Carクラスを想像してください。

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

簡潔にするために、IChassisSeatDashboardなどの一部のコンポーネントの実装は省略します。

Carは、自分自身を構築する責任はありません。アセンブリラインは次のとおりです。それをクラスにカプセル化しましょう。

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

ここでの重要な違いは、CarAssemblerにIAssemblyPhaseを実装するアセンブリフェーズオブジェクトのリストがあることです。これで、パブリックメソッドを明示的に処理しています。つまり、各フェーズを単独でテストできます。単一のクラスにあまり詰め込まないため、コード品質ツールの方が便利です。組み立てプロセスも非常に単純です。フェーズをループし、Assembleを呼び出して車を渡します。できました。 IAssemblyPhaseインターフェースも、Carオブジェクトを取る単一のパブリックメソッドだけで非常にシンプルです。

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

次に、特定のフェーズごとにこのインターフェースを実装する具象クラスが必要です。

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

各フェーズは個別に非常に単純にすることができます。一部はライナーが1つだけです。 4人の座席を追加するだけで目がくらむような人もいれば、車に追加する前にダッシュボードを設定する必要がある人もいます。この長く引き出されたプロセスの複雑さは、簡単にテストできる複数の再利用可能なクラスに分割されます。

現在、順序は重要ではないとおっしゃっていましたが、将来的には重要になる可能性があります。

これを完了するために、車を組み立てるプロセスを見てみましょう:

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

CarAssemblerをサブクラス化して、特定の種類の車またはトラックを組み立てることができます。各フェーズはより大きな全体の1つの単位であるため、コードの再利用が容易になります。


このデザインにはどうしようもない欠陥があり、私は敗北と溝を認めたほうがいいでしょう-このアーキテクチャは完全にそうですか?私のキャリアにとっては良くありませんが、長期的には、設計が不適切なコードを書く方が良いでしょうか?

私が書いた内容を書き直す必要があると判断することが私のキャリアのブラックマークである場合、私は今、溝掘り屋として働いているでしょう。 :)

ソフトウェアエンジニアリングは、学習、適用、再学習という永遠のプロセスです。コードを書き直すことにしたからといって、解雇されるわけではありません。その場合、ソフトウェアエンジニアはいません。

私の現在の選択は実際には1つの真の方法であり、より良い品質の指標(および/または計測)をインストールするために戦う必要がありますか?

概説したのはOne True Wayではありませんが、コードメトリックはOne True Wayではないことに注意してください。コードメトリックは警告として表示する必要があります。彼らは将来のメンテナンスの問題を指摘することができます。それらは法律ではなくガイドラインです。コードメトリックツールが何かを指摘した場合、まずコードを調査して、 [〜#〜] solid [〜#〜] プリンシパルが適切に実装されているかどうかを確認します。多くの場合、コードをリファクタリングしてそれをより多くしますSOLIDは、コードメトリックツールを満たします。場合によっては、そうではありません。これらのメトリックをケースバイケースで取得する必要があります。

15
Greg Burghardt

どういうわけか、問題は依存関係を思い出させます。あなたは車を特定の構成にしたいです。 Greg Burghardtのように、各ステップ/アイテム/…のオブジェクト/クラスを使用します。

各ステップでは、ミックスに何を追加するか(何をprovidesするか)(複数のものにすることができます)だけでなく、必要なものも宣言します。

次に、最終的に必要な構成をどこかに定義し、必要なものを調べ、実行する必要のあるステップ/追加する必要のある部分/収集する必要のあるドキュメントを決定するアルゴリズムを用意します。

依存関係解決アルゴリズムはそれほど難しくありません。最も単純なケースは、各ステップが1つのものを提供し、2つのものが同じものを提供せず、依存関係が単純である(依存関係に「or」がない)場合です。より複雑な場合:debian aptなどのツールを見てください。

最後に、自動車を組み立てると、可能な手順が減り、CarBuilderに必要な手順を理解させることができます。

ただし、重要なことの1つは、CarBuilderにすべてのステップを知らせる方法を見つける必要があることです。設定ファイルを使用してこれを実行してみてください。追加のステップを追加するには、構成ファイルに追加するだけで済みます。ロジックが必要な場合は、構成ファイルにDSLを追加してみてください。他のすべてが失敗した場合、コードでそれらを定義することに行き詰まっています。

1

これが可能かどうかはわかりませんが、よりデータ指向のアプローチを使用することを考えています。アイデアは、ルールと制約、操作、依存関係、状態などのいくつかの(宣言的な)式をキャプチャすることです。次に、クラスとコードはより一般的で、ルールに従うこと、インスタンスの現在の状態を初期化すること、より直接的にコードを使用するのではなく、ルールと状態変更の宣言的キャプチャを使用して、状態を変更します。プログラミング言語でのデータ宣言、一部のDSLツール、またはルールやロジックエンジンを使用する場合があります。

1
Erik Eidt

あなたが言及するいくつかのことは私にとって「悪い」と目立つ

「私のロジックは現在、実装を簡単にするためにリフレクションを使用しています」

これには言い訳はありません。本当に何を実行するかを決定するためにメソッド名の文字列比較を使用していますか?順序を変更した場合、メソッドの名前を変更する必要がありますか?

「単一フェーズの操作はすでに独立しています」

それらが互いに完全に独立している場合は、クラスが複数の責任を負っていることを示しています。私にとって、あなたの両方の例は、ある種のシングルに向いているようです

MyObject.AddComponent(IComponent component) 

各操作のロジックを独自のクラスに分割できるアプローチ。

実際、それらは真に独立していないと思います。それぞれがオブジェクトの状態を調べて変更するか、少なくとも開始前に何らかの検証を実行すると思います。

この場合、次のいずれかを行うことができます。

  1. ウィンドウからOOP=をスローし、クラス内の公開されたデータを操作するサービスがあります。

    Service.AddDoorToCar(車)

  2. 最初に必要なデータをアセンブルし、完成したオブジェクトを返すことでオブジェクトを構築するビルダークラスを用意します。

    Car = CarBuilder.WithDoor()。WithDoor()。WithWheels();

(withXロジックは再びサブビルダーに分割できます)

  1. 機能的アプローチを取り、関数/デリゲートを変更メソッドに渡します

    Car.Modify((c)=> c.doors ++);

0
Ewan