web-dev-qa-db-ja.com

OOP原則を反映するためのif-elseロジックのリファクタリング

_if/else if_ステートメントに基づいてコードをリファクタリングし、OOPの原則に厳密に従う方法に関するいくつかの関連する質問を読みましたが、これを具体的なユースケースに適用することは困難です。

次の基本クラスがあります。

_    public class Human
    {
        public bool IsMother { get; set; }

        public bool IsFather { get; set; }

        public bool Isdaughter { get; set; }
        public bool IsSon { get; set; }

        public string WhatAmI { get; set; }
    }
_

およびいくつかの派生クラス:

_    public class Parent : Human
    {
        public Parent()
        {
            WhatAmI = "Parent";
        }
    }

    public class Daughter : Human
    {
        public Daughter()
        {
            WhatAmI = "Daughter";
        }
    }

    public class Son : Human
    {
        public Son()
        {
            WhatAmI = "Son";
        }
    }
_

したがって、いくつかの条件に基づいて、特定のタイプの人間を返す必要があります。

たとえば、私がこれを持っている場合:

_   var human = new Human();
   human.IsFather = false;
   human.IsMother = false;
   human.IsSon = true;
   human.Isdaughter = false;

   var newHuman = HumanHelper.GetHuman(human);
   Console.WriteLine(newHuman.WhatAmI);
_

コンソール出力は「Son」ですが、GetHuman(Human human)メソッドの実装は、_if-else_ステートメントの乱用のために私を悩ませています。そして具体的な実装はこれです:

_    public static class HumanHelper
    {
        private static string kindOfHuman = ConfigurationManager.AppSettings["kindOfHuman"];
        public static Human GetGuman(Human human)
        {
            if (kindOfHuman == "parent")
            {
                if (human.IsMother || human.IsFather)
                {
                    return new Parent();
                }
            }
            else if (kindOfHuman == "child")
            {
                if (!human.IsMother && !human.IsFather)
                {
                    if (human.Isdaughter)
                    {
                        return new Daughter();
                    }
                    else if (human.IsSon)
                    {
                        return new Son();
                    }
                }
            }
            throw new ArgumentException("Should not get here");
        }
    }
_

読むのは難しいですが、私は単純で比較的標準的なものを単純ではなく標準的でない方法でやっていると思います。それで、このコードの改善を手伝っていただけませんか?

そのコード

_var human = new Human();
human.IsFather = false;
human.IsMother = false;
human.IsSon = true;
human.Isdaughter = false;

var newHuman = HumanHelper.GetHuman(human);
Console.WriteLine(newHuman.WhatAmI);
_

に簡略化できます

_var human = new Son();

Console.WriteLine(newHuman.WhatAmI);
_

私はそれが愚かに見えることを知っていますが、ハードコードされたブール値を割り当てる場合は、実際の型を直接インスタンス化することができます。

実際のコードを単純化して理解しやすくした場合は、さまざまな役割を表す列挙型を追加することをお勧めします

_enum Role {
    FATHER,
    MOTHER,
    SON,
    DAUGHTER
};
_

そして人間の創造に責任を持つ工場を作る

_public static Human create(Role role) {
    switch(role) {
        case Role.FATHER:
        case Role.MOTHER:
            return new Parent();
        case Role.SON:
            return new Son();
        case Role.DAUGHTER:
            return new Daughter();
    }
}
_

したがって、これらの醜いIsXXXHumanから削除して、インターフェイスに変換できます。

_interface Human {
    string WhatAmI(); //pay attention that this method seems redundant with ToString()
    // other methods ?
}
_

派生クラスあり

_public class Parent : Human
{
    public string WhatAmI() {
        return "parent";
    }
}

...
_

ここで理解する最も重要なことは、コードベースが_if/else_で乱雑になっている場合、常に私たちはいくつかの初期設計選択を行った "メソッドを追加するIsSonIsFatherなど」または、メソッドパラメータとして「フラグ」を渡すことを決定するたび:write(bool shouldOverride)は常に次のようなコードになります

_public void write(shouldOverride) {
    if(shouldOverride) {
        //some behaviour
    }
    else {
        //some other behaviour
    }
}
_
20
Spotted

いくつかの観察:

  1. クラスHumanは、作成するオブジェクトの種類の仕様と実際に作成されるオブジェクトの基本クラスの両方であるため、二重の責任があります。これらは実際には個別の責任であり、個別のプロパティが含まれます。最初のHumanは、HumanSpecificationのような名前の個別のクラスである必要があります。 (IsXXXプロパティはHumanSpecificationに属しますが、WhatAmIは人間に属します)

    また、Humanabstractである必要がありますが、仕様オブジェクトとしても使用する限り、これは不可能です。

  2. 仕様では、有効な構成のみを許可する必要があります。ブール値のセットは、排他的なオプションを表現するのに適切ではありません。例えば。 IsMotherIsFatherの両方を設定することは可能ですが、意味がありません。排他的なオプションの代わりにenumを使用してください。人間が同時に父と息子になることができるかどうかは明らかではありませんが、仕様ではそれを許可しています。

  3. WhatAmIプロパティは、このプロパティを実際に使用するようにクライアントを誘惑する可能性があるため、不必要で潜在的に危険に思われます。デバッグのためには、ToString()を使用する必要があります。

これらの3つの変更を実装すると、コードがはるかにシンプルになり、エラーが発生しにくくなります。

最もわかりにくいのは、ファクトリメソッド内のロジックで、フラグが構成設定と組み合わされてインスタンスが生成されます。

コードからロジックをリバースエンジニアリングします。

kindOfHuman |IsMother |IsFather |IsDaughter |IsSon |Result
parent      |T        |F        |*          |*     |Parent
parent      |F        |T        |*          |*     |Parent
parent      |T        |T        |*          |*     |Parent
parent      |F        |F        |*          |*     |!ERROR!
child       |T        |T        |*          |*     |!ERROR!
child       |T        |F        |*          |*     |!ERROR!
child       |F        |T        |*          |*     |!ERROR!
child       |F        |F        |T          |*     |Daughter
child       |F        |F        |F          |T     |Son
child       |F        |F        |F          |F     |!ERROR!

(どこ *は任意の真理値を意味します。)いくつかの奇妙なロジックにすぐに気づくでしょう:IsSontrueである場合、結果はDaughterである可能性があります。

ロジックを詳しく見ると、実際のルールでは入力の3つのケースしか区別されないようです。そのため、入力をオプションPARENT、DAUGHTER、SONを使用して列挙型に簡略化できます。これにより、ルールが大幅に簡略化されます。

kindOfHuman |spec     |Result
parent      |PARENT   |Parent
parent      |DAUGHTER |!ERROR
parent      |SON      |!ERROR
child       |PARENT   |!ERROR
child       |DAUGHTER |Daughter
child       |SON      |Son

出力は同じですが、ロジックが大幅に簡略化されます。有効な出力は3つしかないため、次のようなifが最も明確で読みやすいと思います。

   if (spec == PARENT && kindOfHuman == "parent") {
       return new Parent();
   } else if (spec == DAUGHTER && kindOfHuman == "child") {
      return new Daughter();
   } else if (spec == SON && kindOfHuman == "child") {
      return new Son();
   }
   throw new ArgumentException("Should not get here");

これはかなり読みやすいと思いますが、将来的にルールを頻繁に追加または削除することが予想される場合は、次のようなルールの構成など、よりデータ駆動型のアプローチを検討するかもしれません。

   var rules = new[]{ 
      Rule("parent", PARENT, ()=>new Parent()), 
      Rule("child", DAUGHTER, ()=>new Daughter()), 
      Rule("child", Son, ()=>new Son()),
   }

しかし、YAGNI。

3
JacquesB

OOPの原則に厳密に従うために、if/else ifステートメントに基づいてコードをリファクタリングする

よろしければ、ダブルディスパッチ(または必要に応じてビジターパターン)がここでいくつかのオプションを提供する方法を示したいと思います。私は人間をブール値オブジェクトとして使用しません。実際には、ifをまったく使用しません。

Javaすべてのc#パーティーでここにあります(たまたま手元にあっただけで、テストしていないコードは投稿しませんでした)。ご容赦ください。同じトリックはc#でも問題なく機能します。

public class Human {
    Gender gender;
    Role role;

    public Human(Gender gender, Role role) {
        this.gender = gender;
        this.role = role;
    }

    @Override
    public String toString() {
        return whatAmI();
    }

    String whatAmI() {
        return gender.whatAmI(role);
    }

    static interface Gender {
        String whatAmI(Role role);
    }

    static interface Role {
        public String whatAmI(Male male);
        public String whatAmI(Female female);
    }

    static class Male implements Gender {
        @Override
        public String whatAmI(Role role) {
            return role.whatAmI(this);
        }
    }

    static class Female implements Gender {
        @Override
        public String whatAmI(Role role) {
            return role.whatAmI(this);
        }
    }

    static class Parent implements Role{
        @Override
        public String whatAmI(Male male) {
            return "Parent";
        }

        @Override
        public String whatAmI(Female female) {
            return "Parent";
        }
    }

    static class Child implements Role{
        @Override
        public String whatAmI(Male male) {
            return "Son";
        }

        @Override
        public String whatAmI(Female female) {
            return "Daughter";
        }        }

    public static void main (String args[]){
        System.out.println(new Human(new Female(), new Parent()).whatAmI());
        System.out.println(new Human(new Male(), new Parent()).whatAmI());
        System.out.println(new Human(new Female(), new Child()).whatAmI());
        System.out.println(new Human(new Male(), new Child()).whatAmI());
   }
}

出力




息子

それは少し設計された少しです。それはあなたが父親であるか母親であるかを判断し、それからそれを忘れ始めます。あなたの行動を真似て追いついた。

私はこれがあなたがリファクタリングできる一つの方法を示していると信じていますif-else-if OOPの原則を使用したコードです。これらの原則は存在しない状態ですぐにリファクタリングされています。このアプローチには独自の欠点がないわけではありません。性別と役割は、私の趣味に関してお互いをよく知りすぎています。しかし、尋ねるのではなく、人間に何をすべきかを伝えるのは良いことです。

2
candied_orange

あなたがしたことはすべて間違っていたので、私は変更を提案するつもりはありません。

OOP、特に継承が必要です。これは、あなたの世界をモデル化する目的には役立ちません。

他の誰かが気づいたように、息子は父親になることができ、逆もまた同様です。それに父親は息子だと付け加えます。誰かが父親であるかどうかは、彼のタイプではなく、他の家族との関係によって決まります。

あなたの実体が人間であるという事実は無関係であるように見えます、あなたは家族を扱っています。だからここにクラスツリーがあります:

家族の一員

そうです、たった1つのクラスです。次のプロパティがあります。

  • 名前(文字列)
  • セックス(列挙)
  • 父(FamilyMemberである父オブジェクトへの参照)
  • 母(FamilyMemberである母オブジェクトへの参照)
  • 息子と娘(FamilyMembersのコレクション)
  • IsSon(ブール値、セックスが男性の場合はtrue)
  • IsDaughter(ブール値、セックスが女性の場合はtrue)
  • IsFather(ブール値、SonsまたはDaughtersに少なくとも1つのメンバーがいる場合はtrue)
  • IsMother(ブール値、SonsまたはDaughtersに少なくとも1人のメンバーがいる場合はtrue)
1
Martin Maat

つまり、Humanオブジェクトがあり、そのオブジェクトから、基本クラスのオブジェクトのプロパティを使用して派生クラスのオブジェクトを取得したいとします。そもそもなぜSonを作成しなかったのですか?

また、これらの派生クラスでは、IsMotherおよびIsFatherおよびIsdaughterおよびIsSonプロパティをオーバーライドするか、コンストラクターで設定する必要があります。そうでない場合は、 SonIsSonはfalseを返します。

静的メソッドが目的のオブジェクトを返すようにしたい場合は、次のようにします。

public static class HumanHelper
{
    enum Gender { Male, Female };

    private static string kindOfHuman = ConfigurationManager.AppSettings["kindOfHuman"];

    public static Human GetGuman(Gender gender)
    {
        if (kindOfHuman == "parent")
        {
            return new Parent();
        }
        else if (kindOfHuman == "child")
        {
            if (gender == Gender.Female)
            {
                return new Daughter();
            }
            else if (gender == Gender.Male)
            {
                return new Son();
            }
        }
        throw new ArgumentException("Should not get here");
    }
}

そうすれば、いくつかのifsを削除できます。

1
Maxim Bernard