2008-10-19 2 views
37

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

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

ответ

47

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

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

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

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

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

10

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

+0

Да, и вы можете выбрать одну реализацию сейчас и передумать позже, не нарушая при этом другой код. – 2008-10-19 15:33:14

16

Вы можете рассмотреть вопрос об использовании Party and Accountability patterns

Это то, как человек будет иметь коллекцию подотчётности, которая может быть типа Клиента или штаба.

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

1

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

3

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

1

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

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

+0

В данном примере лицо не может быть как клиентом, так и сотрудником. Вот о чем идет речь. – OregonGhost 2008-10-19 16:57:34

+0

Привет, Я думаю, что вопрос связан скорее с «Я не хочу отправлять несколько электронных писем, если Лицо является как клиентом, так и сотрудником». Чтобы решить эту проблему, 1) «Все» не должны допускать дубликатов 2) Если это разрешает дубликаты, тогда класс Person должен иметь «Роли», определенные как указано Foredecker – vj01 2008-10-19 17:07:15

5

Дайте мне знать, если я правильно поняла ответ 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) 

+0

. Да, это выглядит красиво и очень расширяемо. – 2008-10-19 18:32:22

1

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

7

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

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

+0

Организация часто квалифицируется как тип лица, то есть лица судебной системы. – ProfK 2011-10-15 17:53:04

1

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

1

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

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

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

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

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

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

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

Программирование на интерфейс и с помощью КОВАРИАНТНЫХ возвращаемых типов

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

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

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

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

0

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

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; } 
} 
Смежные вопросы