Лучшие практики объектно-ориентированного подхода — наследование v композиция v интерфейсы

StackOverflow https://stackoverflow.com/questions/216523

  •  03-07-2019
  •  | 
  •  

Вопрос

Я хочу задать вопрос о том, как бы вы подошли к простой задаче объектно-ориентированного проектирования.У меня есть несколько собственных идей о том, как лучше всего справиться с этим сценарием, но мне было бы интересно услышать мнения сообщества 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) и создать три класса, реализующие соответствующие интерфейсы?
  • Применить другой, совершенно другой подход?
Это было полезно?

Решение

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

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

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

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

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

Другие советы

Возможно, вы захотите рассмотреть возможность использования Партийные модели и модели подотчетности

Таким образом, у человека будет набор подотчетностей, которые могут относиться к типу «Клиент» или «Сотрудник».

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

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

Человек – это человек, а Заказчик – это всего лишь Роль, которую Человек может время от времени брать на себя.Мужчина и Женщина могли бы стать кандидатами на наследование Личности, но Клиент – это другое понятие.

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

Дайте мне знать, правильно ли я понял ответ Форедекера.Вот мой код (на 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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вероятно, вы не захотите использовать для этого наследование.Вместо этого попробуйте это:

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

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

class Staff {
    public Person PersonInfo;
    public int StaffId { get; set; }
    public string JobTitle { get; set; }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top