web-dev-qa-db-ja.com

オブジェクト指向のベストプラクティス-継承v構成vインターフェイス

単純なオブジェクト指向設計の問題にどのように取り組むかについて質問したいと思います。このシナリオに取り組む最善の方法について私自身のアイデアがいくつかありますが、StackOverflowコミュニティからいくつかの意見を聞くことに興味があります。関連するオンライン記事へのリンクもありがたいです。私はC#を使用していますが、質問は言語固有ではありません。

データベースにPersonPersonIdName、およびDateOfBirthフィールドを持つAddressテーブルがあるビデオストアアプリケーションを作成しているとします。また、StaffへのリンクがあるPersonIdテーブルと、CustomerにもリンクしているPersonIdテーブルもあります。

単純なオブジェクト指向のアプローチは、Customer "は" Personであると言うことであり、したがって、次のようなクラスを作成します。

class Person {
    public int PersonId { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Address { get; set; }
}

class Customer : Person {
    public int CustomerId { get; set; }
    public DateTime JoinedDate { get; set; }
}

class Staff : Person {
    public int StaffId { get; set; }
    public string JobTitle { get; set; }
}

これで、すべての顧客に電子メールを送信するという関数を記述できます。

static void SendEmailToCustomers(IEnumerable<Person> everyone) { 
    foreach(Person p in everyone)
        if(p is Customer)
            SendEmail(p);
}

このシステムは、顧客でありスタッフでもある人がいるまでは問題なく機能します。 everyoneリストに同じ人物を2回(1回はCustomerとして、もう1回はStaffとして)含めたくないと仮定して、任意の選択を行いますか?の間に:

class StaffCustomer : Customer { ...

そして

class StaffCustomer : Staff { ...

明らかに、これら2つのうち最初のものだけがSendEmailToCustomers関数を壊すことはありません。

それで、あなたは何をしますか?

  • PersonクラスにStaffDetailsおよびCustomerDetailsクラスへのオプションの参照を持たせますか?
  • Personに加えて、オプションのStaffDetailsCustomerDetailsを含む新しいクラスを作成しますか?
  • すべてをインターフェース(例:IPersonIStaffICustomer)にして、適切なインターフェースを実装する3つのクラスを作成しますか?
  • 別の完全に異なるアプローチを取りますか?
39
Mark Heath

マーク、これは興味深い質問です。あなたはこれについて多くの意見を見つけるでしょう。私は「正しい」答えがあるとは思わない。これは、システムの構築後に、堅固な階層オブジェクト設計が実際に問題を引き起こす可能性があることを示す好例です。

たとえば、「Customer」クラスと「Staff」クラスを使用したとします。システムをデプロイすると、すべてが満足のいくものになります。数週間後、誰かが「スタッフ」と「顧客」の両方であり、顧客の電子メールを受け取っていないことを指摘しました。この場合、多くのコード変更を行う必要があります(リファクタリングではなく、再設計)。

人とその役割のすべての順列と組み合わせを実装する派生クラスのセットを作成しようとすると、非常に複雑で保守が困難になると思います。上記の例が非常に単純であることを考えると、これは特に当てはまります。ほとんどの実際のアプリケーションでは、状況はより複雑になります。

ここでのあなたの例では、「別のまったく異なるアプローチを取る」と言います。 Personクラスを実装し、それに「ロール」のコレクションを含めます。各人は、「顧客」、「スタッフ」、「ベンダー」などの1つ以上の役割を持つことができます。

これにより、新しい要件が発見されたときに役割を簡単に追加できるようになります。たとえば、基本の「Role」クラスがあり、それらから新しいロールを派生させることができます。

49
Foredecker

パーティと説明責任のパターン の使用を検討することをお勧めします

このようにして、PersonはCustomerまたはStaffタイプの説明責任のコレクションを持つことになります。

後で関係タイプを追加すると、モデルも単純になります。

純粋なアプローチは次のとおりです。すべてをインターフェイスにします。実装の詳細として、オプションで、さまざまな形式の構成または実装の継承のいずれかを使用できます。これらは実装の詳細であるため、パブリックAPIには関係ありません。したがって、人生を最もシンプルにするものを自由に選択できます。

10
yfeldblum

人は人間ですが、顧客は人が時々採用する可能性のある役割にすぎません。男性と女性は人を継承する候補ですが、顧客は別の概念です。

リスコフの置換原則は、基本クラスへの参照がある派生クラスを、それについて知らなくても使用できなければならないと言っています。顧客に個人を継承させることはこれに違反します。顧客は、おそらく組織が果たす役割でもあるかもしれません。

7
Cyril

Foredeckerの答えを正しく理解したかどうか教えてください。これが私のコードです(Pythonで。申し訳ありませんが、C#はわかりません)。唯一の違いは、人が「顧客である」場合は通知しないことです。彼の役割の1つがそのことを「興味がある」場合は通知します。これは十分な柔軟性がありますか?

# --------- PERSON ----------------

class Person:
    def __init__(self, personId, name, dateOfBirth, address):
        self.personId = personId
        self.name = name
        self.dateOfBirth = dateOfBirth
        self.address = address
        self.roles = []

    def addRole(self, role):
        self.roles.append(role)

    def interestedIn(self, subject):
        for role in self.roles:
            if role.interestedIn(subject):
                return True
        return False

    def sendEmail(self, email):
        # send the email
        print "Sent email to", self.name

# --------- ROLE ----------------

NEW_DVDS = 1
NEW_SCHEDULE = 2

class Role:
    def __init__(self):
        self.interests = []

    def interestedIn(self, subject):
        return subject in self.interests

class CustomerRole(Role):
    def __init__(self, customerId, joinedDate):
        self.customerId = customerId
        self.joinedDate = joinedDate
        self.interests.append(NEW_DVDS)

class StaffRole(Role):
    def __init__(self, staffId, jobTitle):
        self.staffId = staffId
        self.jobTitle = jobTitle
        self.interests.append(NEW_SCHEDULE)

# --------- NOTIFY STUFF ----------------

def notifyNewDVDs(emailWithTitles):
    for person in persons:
        if person.interestedIn(NEW_DVDS):
            person.sendEmail(emailWithTitles)

5

「is」チェック(Javaでは「instanceof」)は避けます。 1つの解決策は、 デコレータパターン を使用することです。 EmailablePersonが構成を使用してPersonのプライベートインスタンスを保持し、すべての非電子メールメソッドをPersonオブジェクトに委任する、Personを装飾するEmailablePersonを作成できます。

3
Paul Croarkin

私たちは昨年大学でこの問題を研究しました。私たちはエッフェルを学んでいたので、多重継承を使用しました。とにかく、Foredeckerの役割の選択肢は十分に柔軟なようです。

1
Sunday

スタッフメンバーである顧客に電子メールを送信することの何が問題になっていますか?彼が顧客である場合、彼は電子メールを送信することができます。私はそう考えるのは間違っていますか?そして、なぜあなたはあなたの電子メールリストとして「全員」を取るべきですか? 「sendEmailToEveryone」メソッドではなく「sendEmailToCustomer」メソッドを扱っているので、顧客リストがある方が良いでしょうか? 「全員」のリストを使用したい場合でも、そのリストでの重複を許可することはできません。

これらのどれも多くの再認識で達成できない場合、私は最初のForedeckerの答えに行きます、そしてあなたはそれぞれの人にいくつかの役割を割り当てられるべきかもしれません。

1
vj01

クラスは単なるデータ構造です。どのクラスにも動作はなく、ゲッターとセッターだけです。ここでは継承は不適切です。

1
fizzer

別のまったく異なるアプローチを取ります。クラスStaffCustomerの問題は、スタッフのメンバーが単なるスタッフとして開始し、後で顧客になる可能性があるため、スタッフとして削除して、StaffCustomerクラスの新しいインスタンスを作成する必要があることです。おそらく、「isCustomer」のStaffクラス内の単純なブール値により、全員のリスト(おそらく、すべての顧客とすべてのスタッフを適切なテーブルから取得してコンパイルされたもの)は、すでに顧客として含まれていることがわかるため、スタッフを取得できません。

1
Mike D

さらにいくつかのヒントがあります。「これを行うとは思わない」というカテゴリから、遭遇したコードの悪い例をいくつか示します。

FinderメソッドはObjectを返します

問題:見つかったオカレンスの数に応じて、Finderメソッドはオカレンスの数を表す数値を返します–または!見つかったものが1つだけの場合は、実際のオブジェクトを返します。

これをしないでください!これは最悪のコーディング慣行の1つであり、あいまいさをもたらし、別の開発者が関与したときに彼女または彼がこれを行うことを嫌うような方法でコードを混乱させます。

解決策:このような2つの機能が必要な場合:インスタンスのカウントとフェッチは、カウントを返すメソッドとインスタンスを返すメソッドの2つのメソッドを作成しますが、単一のメソッドが両方の方法を実行することはありません。

問題:派生した悪い習慣は、Finderメソッドが、見つかった1つのオカレンス、または複数見つかった場合はオカレンスの配列のいずれかを返す場合です。この怠惰なプログラミングスタイルは、一般的に前のスタイルを実行するプログラマーによって多く実行されます。

解決策:これを手元に置いて、オカレンスが1つだけ見つかった場合は、長さ1(one))の配列を返し、オカレンスがさらに見つかった場合は、長さが1より大きい配列を返します。アプリケーションに応じて、nullまたは長さ0の配列を返します。

インターフェイスへのプログラミングと共変リターンタイプの使用

問題:インターフェースへのプログラミングと共変リターンタイプの使用、および呼び出し元コードでのキャスト。

解決策:代わりに、戻り値を指す必要がある変数を定義するために、インターフェースで定義されているのと同じスーパータイプを使用してください。これにより、インターフェースアプローチへのプログラミングとコードがクリーンに保たれます。

1000行を超えるクラスは潜在的な危険です100行を超えるメソッドも潜在的な危険です!

問題:一部の開発者は、1つのクラス/メソッドに機能を詰め込みすぎて、機能を壊すのが面倒です。これにより、凝集度が低くなり、結合度が高くなる可能性があります。これは、OOPの非常に重要な原則の逆です。解決策:内部/ネストされたクラスを使いすぎないようにします。これらのクラスは必要に応じてのみ使用する必要があり、習慣的に使用する必要はありません。それらを使用すると、継承の制限など、より多くの問題が発生する可能性があります。コードの重複に注意してください!同じまたは類似しすぎたコードが、一部のスーパータイプの実装または別のクラスにすでに存在している可能性があります。スーパータイプではない別のクラスにある場合は、凝集ルールにも違反しています。静的メソッドに注意してください–追加するにはユーティリティクラスが必要かもしれません!
詳細: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

1
Bogdan