Объектно-ориентированные рекомендации - Наследование v Композиция v Интерфейсы - PullRequest
39 голосов
/ 19 октября 2008

Я хочу задать вопрос о том, как бы вы подошли к простой объектно-ориентированной задаче проектирования. У меня есть несколько собственных идей о том, как лучше всего решить этот сценарий, но мне было бы интересно услышать некоторые мнения от сообщества Stack Overflow. Ссылки на соответствующие статьи онлайн также приветствуются. Я использую C #, но вопрос не зависит от языка.

Предположим, я пишу приложение для видеомагазина, база данных которого имеет таблицу Person с полями PersonId, Name, 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 был один и тот же человек дважды, один раз как Customer и один раз как Staff, мы делаем произвольный выбор между:

class StaffCustomer : Customer { ...

и

class StaffCustomer : Staff { ...

Очевидно, что только первый из этих двух не нарушит функцию SendEmailToCustomers.

Так что бы вы сделали?

  • У класса Person есть необязательные ссылки на классы StaffDetails и CustomerDetails?
  • Создать новый класс, содержащий Person, плюс необязательные StaffDetails и CustomerDetails?
  • Сделать все интерфейсом (например, IPerson, IStaff, ICustomer) и создать три класса, которые реализовали соответствующие интерфейсы?
  • Есть другой совершенно другой подход?

Ответы [ 12 ]

49 голосов
/ 19 октября 2008

Марк, это интересный вопрос. Вы найдете столько же мнений по этому вопросу. Я не верю, что есть «правильный» ответ. Это отличный пример того, как жесткая конструкция иерархического объекта действительно может вызвать проблемы после построения системы.

Например, допустим, вы пошли с классами "Клиент" и "Персонал". Вы разворачиваете свою систему и все устраивает. Несколько недель спустя кто-то указывает, что они оба являются «сотрудниками» и «клиентами» и не получают электронных писем от клиентов. В этом случае вам нужно внести много изменений в код (изменить дизайн, а не заново).

Я считаю, что было бы слишком сложно и трудно поддерживать, если вы попытаетесь получить набор производных классов, которые реализуют все перестановки и комбинации людей и их ролей. Это особенно верно, учитывая, что приведенный выше пример очень прост - в большинстве реальных приложений все будет сложнее.

Для вашего примера я бы сказал: «Возьми другой, совершенно другой подход». Я бы реализовал класс Person и включил в него коллекцию «ролей». Каждый человек может иметь одну или несколько ролей, таких как «Клиент», «Персонал» и «Продавец».

Это облегчит добавление ролей при обнаружении новых требований. Например, вы можете просто иметь базовый класс «Роль» и получать из него новые роли.

17 голосов
/ 19 октября 2008

Возможно, вы захотите использовать шаблоны Party и Accountability

Таким образом, у Лица будет коллекция Ответственностей, которая может быть типа Клиент или Персонал.

Модель также будет проще, если вы добавите больше типов отношений позже.

10 голосов
/ 19 октября 2008

Чистый подход был бы: сделать все интерфейсом. В качестве подробностей реализации вы можете по желанию использовать любую из различных форм композиции или реализации-наследования. Поскольку это детали реализации, они не имеют значения для вашего общедоступного API, поэтому вы можете выбирать, что будет проще в вашей жизни.

7 голосов
/ 20 октября 2008

Человек - это человек, в то время как Клиент - это просто роль, которую Человек может принимать на себя время от времени. Мужчина и Женщина были бы кандидатами на наследование Персоны, но Клиент - это другое понятие.

Принцип подстановки Лискова говорит, что мы должны иметь возможность использовать производные классы, где у нас есть ссылки на базовый класс, не зная об этом. Если Клиент унаследует Персона, это будет нарушено. Возможно, Заказчик также играет роль, которую играет Организация.

5 голосов
/ 19 октября 2008

Дайте мне знать, правильно ли я понял ответ Foredecker. Вот мой код (на Python; извините, я не знаю C #). Разница лишь в том, что я бы не уведомлял что-то, если человек «является клиентом», я делал бы это, если бы его роль «интересовала» эта вещь. Это достаточно гибко?

# --------- 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)

3 голосов
/ 19 октября 2008

Я бы избегал проверки "is" ("instanceof" в Java). Одним из решений является использование Pattern Decorator . Вы можете создать EmailablePerson, который украшает Person, где EmailablePerson использует композицию для хранения частного экземпляра Person и делегирует все не связанные с электронной почтой методы объекту Person.

1 голос
/ 03 августа 2012

Вот еще несколько советов: Из категории «даже не думай делать это» вот несколько плохих примеров встреченного кода:

Метод Finder возвращает объект

Проблема: в зависимости от количества найденных вхождений метод поиска возвращает число, представляющее количество вхождений - или! Если только один найденный возвращает реальный объект.

Не делай этого! Это одна из худших практик кодирования, которая вносит двусмысленность и портит код таким образом, что, когда в игру вступает другой разработчик, он или она будет ненавидеть вас за это.

Решение: Если есть необходимость в таких 2 функциях: при подсчете и извлечении экземпляра создайте 2 метода, один из которых возвращает счетчик, а другой - экземпляр, но ни один метод не выполняет оба действия.

Проблема: производная плохая практика - это когда метод поиска возвращает либо одно найденное вхождение, либо массив вхождений, если найдено более одного. Этот ленивый стиль программирования часто делается программистами, которые делают предыдущий вообще.

Решение: Имея это в моих руках, я бы возвратил массив длины 1 (один), если будет найдено только одно вхождение, и массив длиной> 1, если будет найдено больше экземпляров. Более того, если вообще не будет найдено ни одного вхождения, в зависимости от приложения будет возвращен ноль или массив длиной 0

Программирование на интерфейсе и использование ковариантных типов возврата

Проблема: программирование интерфейса и использование ковариантных типов возврата и приведение в вызывающем коде.

Решение: вместо этого используйте тот же супертип, определенный в интерфейсе, для определения переменной, которая должна указывать на возвращаемое значение. Это сохраняет программирование на подходе интерфейса и Ваш код чистым.

Классы с более чем 1000 строками представляют собой скрытую опасность Методы с более чем 100 строками также представляют опасность!

Проблема: некоторые разработчики вкладывают слишком много функциональности в один класс / метод, будучи ленивыми, чтобы нарушить функциональность - это приводит к низкой когезии и, возможно, к высокой связи - обратный принцип очень важного в ООП! Решение: Избегайте использования слишком большого количества внутренних / вложенных классов - эти классы должны использоваться ТОЛЬКО по мере необходимости, вам не нужно привыкать к их использованию! Их использование может привести к большему количеству проблем, таких как ограничение наследования. Ищите дубликат кода! Такой же или слишком похожий код может уже существовать в некоторой реализации супертипа или, возможно, в другом классе. Если это в другом классе, который не является супертипом, вы также нарушили правило сплоченности. Остерегайтесь статических методов - возможно, вам нужен служебный класс для добавления!
Больше на: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

1 голос
/ 22 июля 2010

Возьмите другой совершенно другой подход: проблема с классом StaffCustomer заключается в том, что ваш сотрудник может начать с простого персонала, а затем стать клиентом, поэтому вам нужно будет удалить их как персонал и создать новый экземпляр класса StaffCustomer. , Возможно, простое логическое значение в классе Staff isCustomer позволит нашему списку «Все» (предположительно, составленному из всех клиентов и всех сотрудников из соответствующих таблиц) не получать сотрудника, поскольку он будет знать, что он уже включен в качестве клиента. 1001 *

1 голос
/ 19 октября 2008

Ваши классы - это просто структуры данных: ни один из них не имеет никакого поведения, только получатели и установщики. Наследование здесь неуместно.

1 голос
/ 19 октября 2008

Что плохого в отправке электронного письма Клиенту, который является Сотрудником? Если он является клиентом, то ему можно отправить электронное письмо. Я ошибаюсь, думая так? И почему вы должны принять «все» в качестве списка адресов электронной почты? Не лучше ли иметь список клиентов, так как мы имеем дело с методом sendEmailToCustomer, а не с методом sendEmailToEveryone? Даже если вы хотите использовать список «все», вы не можете разрешить дублирование в этом списке.

Если ничего из этого не достижимо с большим количеством перерисовок, я пойду с первым ответом Foredecker и, возможно, вам нужно назначить некоторые роли каждому человеку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...