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


10

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

私の問題は、要約すると、オブジェクトがあり、それに対して長い一連の操作を実行する必要があることです。自動車をつくるような組み立てラインのようなものだと思います。

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

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

単一フェーズでの操作は既に独立しています。つまり、私はすでに、それらのサブセットを任意の順序で実行できるように取り組んでいます(前部座席と後部座席は任意の順序でインストールできます)。

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

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

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

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

別の例

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

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

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

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

...だが...

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

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

Evil Overlordの例を使用すると、私の問題は、Evil Overlord(別名拒否されるべきではない彼)がすべての食事情報を要求したことです。所有者など、そして悪(サブ)オーバーロード- 否定されるべきではない彼としてよく知られている-は、GetDietaryInformationフェーズで実行するクエリが多すぎると不平を言っています。

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

だと思い、私が行うことができます

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

  • 私はこっそりできることを確認し、メソッドの半分を宣言します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非常に優れたジョブセキュリティですが、それほど魅力的ではありません。また、コードカバレッジチェッカーにはラムダに関する問題があります(これは解決可能ですが、まだ問題です)。

そう...

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

3
または、「最大複雑度を超えています」という警告を無視することもできます。
Robert Harvey

出来たらやる。どういうわけか、この測定基準はそれ自体がすべて神聖な品質を獲得しています。私は、PTBを有用なツールとして受け入れるように説得し続けていますが、これはツールにすぎません。私はまだ成功するかもしれません...
LSerni 2015

操作は実際にオブジェクトを作成していますか、それとも、アセンブリラインのメタファを使用していますか?意義は後者を充填詳細と他のいくつかのパターンを提案するかもしれないが、前者は、Builderパターンを提案することである。
outis

2
メトリックの専制政治。指標は有用な指標ですが、その指標が使用される理由を無視して強制することは役に立ちません。
ジェイディー

2
2階の人々に、本質的な複雑さと偶発的な複雑の違いを説明します。en.wikipedia.org/wiki/No_Silver_Bulletルールに違反している複雑さが不可欠​​であると確信している場合、プログラマーごとにリファクタリングすると、ルールの周りをスケートして拡散している可能性があります。複雑さを解消します。物事をフェーズにカプセル化することが任意ではなく(フェーズは問題​​に関連している)、再設計によってコードを保守する開発者の認知的負荷が軽減される場合は、これを行うのが理にかなっています。
Fuhrmanator 2015

回答:


15

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

車の組み立て例を基に、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インターフェースはまた、車のオブジェクトを取るだけで、単一のパブリックメソッドと死んでシンプルです。

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つの単位であるため、コードの再利用が容易になります。


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

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

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

私の現在の選択は実際にはOne True Wayであり、より良い品質のメトリック(および/またはインストルメンテーション)をインストールするために戦う必要がありますか?

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


1
はい、1つの神のクラスを持つよりもはるかに良いです。
BЈовић

このアプローチは機能しますが、主にパラメーターなしで自動車の要素を取得できるためです。それらを渡す必要がある場合はどうなりますか?次に、これらすべてを窓から捨てて、手順を完全に考え直します。150のパラメーターを持つ自動車工場が実際にはないからです。
アンディ

@DavidPacker:たくさんのものをパラメータ化する必要がある場合でも、「アセンブラ」オブジェクトのコンストラクタに渡すある種のConfigクラスにそれをカプセル化することもできます。この車の例では、塗装の色、内装の色と材料、トリムパッケージ、エンジンなどをカスタマイズできますが、必ずしも150のカスタマイズは必要ありません。おそらく1ダースほどになるでしょう。これは、オプションオブジェクトに適しています。
Greg Burghardt

ただし、構成を追加するだけでは、美しいforループの目的に反することになります。これは、構成を解析して適切なメソッドに割り当て、結果として適切なオブジェクトを取得する必要があるためです。したがって、おそらく元のデザインと非常によく似たものになるでしょう。
アンディ

そうですか。1つのクラスと50のメソッドの代わりに、実際には50のアセンブラリーンクラスと1つのオーケストレーションクラスがあります。結局、結果は同じで、パフォーマンスも同じように見えますが、コードレイアウトはきれいです。私はそれが好きです。操作を追加するのは少し厄介ですが(クラスでそれらを設定する必要があります。さらに、オーケストレーションクラスに通知します。この必要性から簡単な方法を見つけることはできません)、それでもとても気に入っています。このアプローチを探求するのに数日かかります。
LSerni 2015

1

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


一部の操作は、実際にはこの方法で実装されています(ルールのリストとして)。車の例に合わせるために、ヒューズ、ライト、プラグのボックスを取り付けるとき、実際には3つの異なる方法を使用していませんが、一般的に2ピンの電気部品を所定の位置に取り付け、ヒューズの3つのリストのリストを取得する1つだけを使用しています、ライト、プラグ。他の方法の大多数は、残念ながらこのアプローチを簡単に受け入れることができません。
LSerni 2015

1

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

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

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

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

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

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


0

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

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

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

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

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

MyObject.AddComponent(IComponent component) 

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

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

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

  1. ウィンドウの外にOOPをスローし、クラス内の公開されたデータを操作するサービスを用意します。

    Service.AddDoorToCar(車)

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

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

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

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

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


Ewan氏は次のように述べています。「ウィンドウの外にOOPを投げて、クラスの公開されたデータを操作するサービスを提供する」---これはオブジェクト指向ではないのはなぜですか。
Greg Burghardt 2015

?? それではない、それは非オブジェクト指向オプション
Ewan

オブジェクトを使用しています。つまり、「オブジェクト指向」です。コードのプログラミングパターンを識別できないからといって、オブジェクト指向ではないというわけではありません。SOLIDの原則は、適切なルールです。SOLID原則の一部またはすべてを実装している場合、それはオブジェクト指向です。実際、構造体を別のプロシージャや静的メソッドに渡すだけでなく、オブジェクトを使用している場合は、オブジェクト指向です。「オブジェクト指向コード」と「良いコード」には違いがあります。悪いオブジェクト指向のコードを書くことができます。
Greg Burghardt

データが構造体であるかのように公開し、プロシージャのように別のクラスで操作しているため、「オブジェクト指向ではありません」
Ewan

tbh避けられない「それはOOPではない!」コメント
Ewan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.