web-dev-qa-db-ja.com

SOLIDの原則に従いながら子から親へと状態を変える

私は次のSOLID原則に従って練習しようとしています。

私は次の例(これは実際のコードに基づく改造/人工的な例であり、ここでは投稿できません)について少し戸惑っています。

public class Driver{
    ICar car;
    public Driver(ICar car){
        this.car = car;
    }

    public void Drive(){
        car.Drive(this);
    }

    public void ChangeCar(ICar car){
        this.car = car;
    }
}
public class ICar {
    void Drive(Driver owner);
}
public class ExampleCar : ICar{
    private int fuel;

    public ExampleCar(int fuel){
        this.fuel = fuel;
    }

    public void Drive(Driver owner){
        if(fuel > 0){
            fuel--;
            Console.Writeline("driving exampleCar. Fuel: " + fuel);
            return;
        }
        owner.ChangeCar(new AnotherCar(200));
    }
}
public class Main{
    ICar car = new ExampleCar(100);
    Driver mcLaren = new Driver(car);

    for(int i = 0; i < 200; i++){
        mcLaren.Drive();
    }
}

つまり、ここで本質的に行われていることは、「ExampleCarの燃料が空の場合、ExampleCarを運転して、AnotherCarに切り替える」です。

一種の「疑似有限状態機械」(これが有限状態機械に近いかどうかさえわかりませんが、これは私が理解できる最良の解釈です)。

したがって、これを使用して、現在の状態のみがチェックできる状態(燃料が空かどうか)に基づいて状態の変化を達成しようとしています。

オープンクローズの原則に違反するため、条件をドライバークラスにシフトし、その条件をチェックするのは論理的ではないようです(まったく異なる条件またはまったく条件のない新しいICarを配置したい場合)例えば)。

ただし、この現在の実装はICarがDriverに依存しているため、依存関係の逆転の原則に違反しているようです。

ここで何をすべきか、またはこの実装に問題がないかどうかはわかりません。

どうぞよろしくお願いいたします。

7
Alan

ガスがなくなったときにCarが所有者が何をすべきかを決定することは適切ではなく、ドライバーがガスがなくなった場合にドライバーが運転する他の種類の車を車が選択することは確かに受け入れられません。

ICarインターフェースは次のようになります。

public class ICar {
    // return false if the car is undriveable
    boolean Drive();
}

次に:

public class Driver{
    ICar car;
    public Driver(ICar car){
        this.car = car;
    }

    // false if the car is undriveable
    public boolean Drive(){
        return car.Drive();
    }

    public void ChangeCar(ICar car){
        this.car = car;
    }
}

public class Main{
    ICar car = new ExampleCar(100);
    Driver mcLaren = new Driver(car);

    for(int i = 0; i < 200; i++){
        while (!mcLaren.Drive()) {
            car = new AnotherCar(200);
            mcLaren.changeCar(car);
        }
    }
}

これは、単一責任の原則に過ぎません。最初に、ドライバーが運転する車を決定するのはMainの仕事であり、ドライバーが与えられた車を運転するのはDriverの仕事だと決めました、そして、それが可能な限り推進されるのはICarの仕事です。

その仕事をするために、Mainは時々車が運転できなくなることを知る必要があります。

9
Matt Timmermans

あらゆる文脈からの人工的な例の場合、それらがSOLIDに違反するかどうかにかかわらず、多くの場合決定不可能であり、賢明な方法でコードを「良い」または「悪い」として評価することは不可能です。例、DIP says

「高レベルのモジュールは低レベルのモジュールに依存するべきではありません。どちらも抽象化(例えばインターフェース)に依存するべきです。」

しかし、この例では「高レベルモジュール」とは何であり、「低レベルモジュール」とは何ですか。それはおそらく、そのような不自然な車/ドライバーの例では好みの問題です。 CarクラスとDriverクラスの両方を「1つのモジュール」として一緒に表示することも、それぞれを独自のモジュールとして表示することもできます。後者の場合、前述のDIP問題を解決するのは非常に簡単です。別のIDriverインターフェースを導入し、ICarDriverではなくそれに依存させるだけです。

しかし、これは「より良い」でしょうか?インターフェースを追加すると、コードが少し複雑になり、それが目的を果たさない場合は KISSの原則 に違反します。

コードのもう1つの詳細は、ExampleCar.Driveの奇妙な副作用です。 Drive内の完全に新しい自動車オブジェクトへの参照を変更することは、私には自明ではありません。既存の自動車オブジェクトに「燃料を補給する」ことは、おそらく次のようにもっと明白でしょう。

 public void Drive(Driver owner){
    if(fuel > 0){
        fuel--;
        Console.Writeline("driving exampleCar. Fuel: " + fuel);
        return;
    }
    fuel=200;
    Console.Writeline("exampleCar refuled. Fuel: " + fuel);
 }

したがって、これは "最小の驚異の原則" の違反である可能性があります。それがreallyである場合、違反は、実際のクラスと実際のユースケースで、実際のアプリケーションのコンテキストでのみ評価できます。おそらく、元の副作用が必要である理由があるかもしれませんし、多分「実際の」コンテキストでは、それらのクラスのユーザーにとってその効果はそれほど予想外には見えません。

要するに、SOLIDだけではないプログラミング原則があり、それらを適用するにはトレードオフが必要になることが多く、人工的な例では通常、それらを「良い」または「悪い」設計として評価するための十分なコンテキストがありません。

3
Doc Brown

車とドライバーの間の相互作用のほとんどは、どちらのパブリックメソッドを使用して行われるべきではありません。代わりに、車に乗りたいドライバーは、車からの通知を受け取るように設計されたプライベートオブジェクトを作成し、それを車に「乗り込む」関数に渡す必要があります。次に、車はドライバーに、ドライバーが操作するために使用できるプライベートな「車制御」オブジェクトへの参照を与える必要があります。

ラッパーオブジェクトがこのように使用され、ドライバーが車を離れるときに明示的に無効化されている場合は、車がドライバーの指示を誤って受け取ることがなくなり、ドライバーが誤って受け取ることがなくなります。他の誰かが運転している車からのステータス更新。

1
supercat

私は次の例について少し困惑しています:

あなたはnotサンプルの作成者ですか?投稿されたコードと質問は、問題の規模が小さくても大きくても、問題を解決して分析し、公正な設計を作成する前に、原則について心配していることを示しています。

あなたは言う:

一種の「疑似有限状態機械」(これが有限状態機械に近いかどうかさえわかりませんが、これは私が理解できる最良の解釈です)。

技術的には、すべてのコンピュータープログラムは有限状態マシンです。エラーが発生しました。言い換えましょう。

技術的に、すべてのコンピュータプログラムは有限状態機械です。 「技術的に」という言葉に焦点が当てられていることに注目してください。これは、私があなたの視点で間違っているように見えるものを強調するための意図的な装置です。

あなたはオブジェクト指向プログラミング、クラス、インターフェース、カプセル化などを使用していて、オブジェクト指向設計の原則を心配し、それらに従おうとしますが、あなたは考えていませんobjects、あなたは考えています技術

では、型にはまらない真実をお話ししましょう。技術的には、コードは完璧です。それは(おそらく)あなたがそれをしたいことをします、それは正しく、機能的で、エラーがなく、パフォーマンスさえもかもしれません!つまり、技術的にです。

問題は、オブジェクト指向設計/プログラミングは盲目的にすべての問題を解決するための方法論ではなく、技術的に解決できる問題の分析を支援する哲学thenであるということです。問題が現実の世界から生じている場合(現実の世界から生じているわけではないことに注意してください)、オブジェクト指向デザインは、手袋をはめたものにぴったりです。自然なオブジェクト、それらの間の関係、違反できない自然な境界がどのように存在するか、各オブジェクトがどのように自分自身を担当するかを考えることができます。オブジェクトは、依存する他のオブジェクトで構成される場合があります。

あなたがモデル化しようとしている対応する現実の概念を借りて、あなたのケース/問題(それが何であるかさえもわかりませんが、とにかく)にオブジェクトのような考え方を適用してみましょう。その間、しばらくの間SOLIDを忘れるです。

  1. 車にはドライバーが含まれておらず、ドライバーには車が含まれていません。それらは、互いに依存することなく、別々の瞬間に共存することができます。つまり、車はドライバーなしで存在でき、ドライバーは車なしで存在できます。簡単な言葉で、あなたはいつも通りに歩いている空の車と運転手を見ます。

つまり、CarまたはDriverの表現は、(したがって)DriverまたはCarに依存しません。つまり、タイプDriverまたはその子の実装は、コンストラクターにCarがあってはなりません。したがって、Carまたはその特殊化の実装では、コンストラクターにドライバーを含めないでください。

  1. ドライバーと車は一緒に集まる、つまりmeetで、「ドライビング」と呼ばれるものを実行できます。ドライバーは車を運転します。つまり、車駆動されるドライバーであり、このアクションは、ドライバーと車が互いに分離されている場合は発生しません。ドライバーはany車を運転でき、車はanyドライバーによって運転されます。また、ドライバーは一生涯車を運転するのではなく、毎回与えられた距離/時間だけ運転します。

2つのクラスが互いに出会うとき、そのうちの1つのインスタンスの参照は最終的にinsideもう一方のインスタンスのコードになる可能性があります。それらを一緒にする必要がある場合、常に、それは通常、コンストラクター注入の時間であるため、「結婚」することができます。彼らが一緒にいる必要があるとき時折メソッドの注入について考えてください。 ここでは運転の行動、つまりロジックをモデル化していることを思い出してください。たとえば、車を眠る場所として使用する場合、分析はかなり異なります

ポイント2は、methodが車またはドライバー(または両方)のどこかにあり、他のパラメーターを取ることを意味します。ポイント2の分析における動詞と推移性(ドライバーdrives車)に基づいて、ドライバーは車を運転しているようです...車はドライバーを運転しないので、ドライバーパラメーターを受け取る「ドライブ」という名前のメソッドを持つ車は、現実の世界では意味がありません。また、ドライバーは、車を運転する距離(または時間)を明示する必要があります。

public interface IDriver
{
    void Drive(ICar car, double miles);
}
  1. 車は永遠に行くことはできず、たまに燃料を補給する必要があります。燃料タンクは内部車であり、機能燃料タンクなしでは車は存在せず、燃料なしでは車は機能しません。しかし、自動車走行距離に基づいて燃料を消費します(はい、私は知っています、そして他の12の要因もそうです。私はしばらくの間、単純化しすぎています)。したがって、車は単に「駆動」することはできず、距離で駆動する必要があります。最後に、自動車距離に基づいて異なる量の燃料を消費する、そしてまた、ドライバーは自由に燃料をチェックアップでき、燃料を補給できます。

これを見通しに入れると、自動車は燃料を消費しますが、自動車自体だけが方法を知っています。それが車の秘密です。ドライバーが燃料をチェックできるということは、残りの燃料が車の中に隠れてはならないということです。ここでは、線が少しぼやけ始めます。燃料は公共か民間か? 依存しますユースケースによって異なります。正確な燃料量に基づいて多くのことをサポートする場合は、遅かれ早かれ、それを公開する必要があるかもしれません。今のところは省略します。車が動けるかどうかだけを気にします。

ポイント3は、自動車が独自の方法で燃料を消費することを明確にしているため、ドライバーは運転するだけで燃料が減少します。オブジェクト指向プログラミングでは十分な柔軟性が得られるため、車に移動を依頼してから、それを通知することができますかどうか移動したおよびどれだけ動いた。自動車がさまざまな理由で移動に失敗する可能性がある場合は、technical対応するいくつかの理由を作成できます...理由!

public interface ICar
{
    bool Move(double miles, out double actuallyTravelledMiles);
}

Not想像力を駆使して、実際のオブジェクトで考えると、次のこともできます。

public enum CarTripResult
{
    Successful,
    OutOfGas,
    NoIgnition,
    //...
}

public interface ICar
{
    CarTripResult MakeTrip(double miles, out double travelledMiles);
}

だから、あなたがそれを知る前に、上のコードに基づいて、あなたは持っています:

public class Car : ICar
{
    private double fuelCapacity_in_gallons;

    private double fuel_in_gallons;

    private double consumptionInMPG;

    public Car(double mpgConsumption, fuelCapacity)
    {
        consumptionInMPG = mpgConsumption;
        fuelCapacity_in_gallons = fuelCapacity;
    }

    //Simplistic representation of the action of Refueling.
    public void Refuel(double gallons)
    {
        fuel_in_gallons += gallons;
        if (fuel_in_gallons > fuelCapacity_in_gallons)
        {
            fuel_in_gallons = fuelCapacity_in_gallons;
        }
    }

    public CarTripResult MakeTrip(double miles, out double travelledMiles)
    {
        double neededFuel = miles / consumptionInMPG; //to get gallons.

        if (fuel_in_gallons > neededFuel)
        {
            fuel_in_gallons -= neededFuel;
            travelledMiles = miles;
            return CarTripResult.Successful;
        }
        else
        {
            //Calculate how much you can travel in miles.
            double distanceCapacity = fuel_in_gallons * consumptionInMPG;

            travelledMiles = distanceCapacity;
            return CarTripResult.OutOfGas;
        }
    }
}

public class Driver : IDriver
{

    public void Drive(ICar car, double miles)
    {
        CarTripResult = car.MakeTrip(miles, out double actualMiles);

        Console.WriteLine("Travelled a distance of " + actualMiles.ToString("0.00"));

        //The code from this point on will depend on
        //what it is you are trying to achieve.
        if (CarTripResult != CarTripResult.Successful)
        {
            //Do something, depending on your actual problem/scenario.
        }
        else if (...)
        {
            //...etc
        }
    }
}

public class Application
{
    public void Main()
    {
        //Remember, parameters are (mpg, capacity).
        ICar cheapCar = new Car(20, 40);
        ICar expensiveCar = new Car(10, 40);

        cheapCar.Refuel(40);
        expensiveCar.Refuel(40);

        Driver driver = new Driver();

        driver.Drive(cheapCar, 50);
        driver.Drive(expensiveCar, 50);
    }
}

ドメインロジックが比較的厳密に分析され、追跡されているので、このようにして得られた設計はどのように「確実」に進化していますか? onceロジックを配置する際にSOLIDの原則についても考慮していません。また、ここで覚えておくべきことの1つは、この設計全体が特定特定の問題に取り組む必要があります完全に同じドメインの見方が異なる場合、ソリューション、設計、および対応するコードは最終的にかなり異なります。

(最終的に)元の質問に回答するには、次のようにしますnot SOLID原則を設計の主要ガイドとして扱います。ガイドは- domainモデリングしようとしているproblemsを組み合わせて解決しようとしています。ドメインを注意深くフォローすると、物事が自然に発生します。過度に単純化のようですが、いずれの場合でも、SOLIDの原則は、設計が完了していないときに、特に最初にモデルを展開しようとする試みの邪魔になることがあります。 SOLID原則に焦点を当てないでください。問題の適切な「オブジェクト指向」、ドメイン駆動の(該当する場合...)分析を行うことを犠牲にして) =。オブジェクトを記録し、関係を調査し、それらを表現し、いくつかのプロトタイプを作成します。現実の世界は、多くの場合「SOLID」であるため、この「SOLID」シティをドメインに変換します。

簡単に言えば、あなたのデザインに焦点を当て、原則を「呼び出す」あなた持っている何か。原則は次のようになりますデザインを開始します正しいことをしていることを示すためではありません(「正しい」は技術であることを忘れないでください)、しかし長期的にどれほどのトラブルが発生するか

1
Vector Zita