Объектно-ориентированные рекомендации - Наследование v Состав v Интерфейсы

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

Предположим, что я пишу приложение для хранения видео, база данных которого имеет таблицу Person, с полями PersonId, Name, DateOfBirth и Address. Он также имеет таблицу Staff, которая имеет ссылку на таблицу PersonId и a Customer, которая также ссылается на PersonId.

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

Ответ 1

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

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

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

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

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

Ответ 2

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

Таким образом, у человека будет свод подотчетности, который может иметь тип Customer или Staff.

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

Сообщите мне, правильно ли понял Foredecker. Вот мой код (в Python, извините, я не знаю С#). Единственное различие заключается в том, что я ничего не буду уведомлять, если человек "является клиентом", я бы сделал это, если одна из его роли "заинтересована" в этой вещи. Является ли это достаточно гибким?

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

Ответ 10

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

Ответ 11

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

Метод Finder возвращает Object

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

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

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

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

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

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

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

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

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

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

Ответ 12

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

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; }
}