Здравствуйте, коллеги!
Очень прошу помощи, советов.
Есть некая платформа (закрытое решение на C#, используется у самых разных клиентов). Там реализована связь между Employee и Person через наследование:
class Employee : IEmployee, NaturalPerson
{
Guid PersonID { get; set; } // - id человека, от которого унаследован экземпляр Employee - это фактически поле в БД.
}
По такому же принципу наследуются, например, банки от юр.лиц: class Bank : LegalPerson (и т.д.)
Правильно ли так вообще наследовать (повторюсь — это платформа и под конкретные задачи трудно что-то определить)?
Пообщавшись с коллегами (некоторые находятся на rsdn-е), пребываю в некотором смятении.
Приведу аргумент одного из них (подобный аргумент привёл только один коллега, остальные даже не поняли в чём грабли).
------------------------------------ 8< ------------------------------------
// один и тот же человек, два сотрудника в разных конторах
employee1 = ...
employee2 = ...
person1 = (NaturalPerson)employee1
person2 = (NaturalPerson)employee2
if (person1 == person2) // True или false?if (employee1 == employee2) // True или false?
// добиваем:if (person1 == employee2) // True или false?
------------------------------------ 8< ------------------------------------
//а теперь контрольный:if (object.Equals(person1, person2))
// или, в словарик с ключом типа NaturalPerson добавлен employee1if (someDictionaryByPerson.ContainsKey(employee2))
// ------------------------------------ 8< ------------------------------------
Подумал: а не переписать ли с использованием explicit operator (I)NaturalPerson, дабы не сильно ломать то, что есть, и включить в состав Employee весь Person. Т.е.:
public class Employee : IEmployee
{
public static exlicit operator/*I*/NaturalPerson(Employee e) { return e.Person; }
NaturalPerson Person {get;set;}
}
Но! С другой стороны, насколько оправданы такие опасения (я про сравнения)?
Что скажете?..
ЗЫ. Сейчас наследование помогает при частых изменениях, добавляя новое поле не надо думать о том, где оно должно ещё появится в связанных классах; функции пакетной обработки так же, благодаря ковариантности, работают как надо со всеми унаследованными типами. Правда, и проблем, тоже огребаю по самое не хочу.
------------------------------------ 8< ------------------------------------
C>// один и тот же человек, два сотрудника в разных конторах
C>employee1 = ...
C>employee2 = ...
C>person1 = (NaturalPerson)employee1
C>person2 = (NaturalPerson)employee2
C>if (person1 == person2) // True или false?
C>if (employee1 == employee2) // True или false?
C>// добиваем:
C>if (person1 == employee2) // True или false?
C>------------------------------------ 8< ------------------------------------
C>//а теперь контрольный:
C>if (object.Equals(person1, person2))
C>// или, в словарик с ключом типа NaturalPerson добавлен employee1
C>if (someDictionaryByPerson.ContainsKey(employee2))
C>// ------------------------------------ 8< ------------------------------------
Мне кажется, что как только вы ответите на все эти вопросы ("True или false?" и т.д.) сразу станет очевидным, мешает ли наследование <b>в вашей задаче<b>.
Другими словами, правильного решения самого по себе не существует, рассматривать нужно всегда в контексте задачи. Где-то лучше окажется наследование, где-то — агрегирование.
C>Подумал: а не переписать ли с использованием explicit operator (I)NaturalPerson, дабы не сильно ломать то, что есть, и включить в состав Employee весь Person. Т.е.: C>
public class Employee : IEmployee
{
public static exlicit operator/*I*/NaturalPerson(Employee e) { return e.Person; }
C>}
Есть ещё вариант с реализацией интерфейса:
public interface IEmployee : IPerson {}
// илиpublic class Employee : IEmployee, IPerson {}
Это лучше отражает связь между работником и человеком: работник это не специальный подвид человека, он просто может рассматриваться как человек.
Но повторюсь ещё раз — вне контекста задачи подобные размышления ничего не значат.
Здравствуйте, Poul_Ko, Вы писали:
P_K>Мне кажется, что как только вы ответите на все эти вопросы ("True или false?" и т.д.) сразу станет очевидным, мешает ли наследование <b>в вашей задаче<b>. P_K>Другими словами, правильного решения самого по себе не существует, рассматривать нужно всегда в контексте задачи. Где-то лучше окажется наследование, где-то — агрегирование.
Спасибо за ответ. Я понимаю, что это так. Всё же хотел бы увидеть примеры как другие (возможно в других контекстах) используют нечто подобное или другое.
P_K>Есть ещё вариант с реализацией интерфейса: P_K>
P_K>public interface IEmployee : IPerson {}
P_K>// или
P_K>public class Employee : IEmployee, IPerson {}
P_K>
P_K>Это лучше отражает связь между работником и человеком: работник это не специальный подвид человека, он просто может рассматриваться как человек.
Да, но тогда, опять же, получается, что у Person и Employee будет проблема(?) с одинаковыми ссылками и т.п. (ReferenceEquals(...)). P_K>Но повторюсь ещё раз — вне контекста задачи подобные размышления ничего не значат.
Направление – в основном – это документооборот. Но ещё раз оговорюсь, для конечного потребителя это достаточно гибкое и универсальное решение.
Здравствуйте, Crauberg, Вы писали:
P_K>>Это лучше отражает связь между работником и человеком: работник это не специальный подвид человека, он просто может рассматриваться как человек. C>Да, но тогда, опять же, получается, что у Person и Employee будет проблема(?) с одинаковыми ссылками и т.п. (ReferenceEquals(...)).
Зачем вам этот ReferenceEquals? Например, одинаковые строки (System.String) не всегда будут равны по ReferenceEquals, однако это никому не мешает.
Сравнивать людей на равенство можно по ИИН или другому идентификатору (у вас вроде GUID).
В случае с агрегацией, на мог взгляд, всё становится проще и понятнее:
public class Employee : IEmployee
{
NaturalPerson Person {get;set;}
}
// один и тот же человек, два сотрудника в разных конторах
employee1 = ...
employee2 = ...
person1 = employee1.NaturalPerson;
person2 = employee2.NaturalPerson;
// Сравниваем работников как людейif (person1 == person2) // должно быть True (перегружаем сравнение по ID или обеспечивает один экземпляр NaturalPerson на человека)
// Сравниваем работниковif (employee1 == employee2) // должно быть false, работники-то разные
// добиваем:if (person1 == employee2) // становится недопустимо (можно реализовать явно через перегрузку оператора сравнения?)
Здравствуйте, Poul_Ko, Вы писали:
P_K>Зачем вам этот ReferenceEquals? Например, одинаковые строки (System.String) не всегда будут равны по ReferenceEquals, однако это никому не мешает. P_K>Сравнивать людей на равенство можно по ИИН или другому идентификатору (у вас вроде GUID).
Я, так сказать, с этим не спорю. Первый раз, видимо, выразился не точно. Вопрос в том, что employee1 и employee2 одного и тоже физ.лица, приведённые к person, будут равны. Насколько такая "проблема" может быть актуальна. Были ли у кого-то из коллег подобные вопросы. Поверьте, вопрос не праздный. Или может быть просто скажите, вашу best practice (хотя бы в 2-х словах)? Как у вас это было реализовано, например, в какой-нить бух.системе.
P_K>В случае с агрегацией, на мог взгляд, всё становится проще и понятнее: P_K>
P_K>public class Employee : IEmployee
P_K>{
P_K> NaturalPerson Person {get;set;}
P_K>}
P_K>// один и тот же человек, два сотрудника в разных конторах
P_K>employee1 = ...
P_K>employee2 = ...
P_K>person1 = employee1.NaturalPerson;
P_K>person2 = employee2.NaturalPerson;
P_K>// Сравниваем работников как людей
P_K>if (person1 == person2) // должно быть True (перегружаем сравнение по ID или обеспечивает один экземпляр NaturalPerson на человека)
P_K>// Сравниваем работников
P_K>if (employee1 == employee2) // должно быть false, работники-то разные
С этим понятно, согласен.
P_K>// добиваем:
P_K>if (person1 == employee2) // становится недопустимо (можно реализовать явно через перегрузку оператора сравнения?)
P_K>
Конечно, для Person и его производных реализован вирт.метод NaturalEqual, в котором и происходит сравнение по необходимым критерием. Правда, необходимость появления этого метода была вызвана, дабы исключать повторы, но его использование оказалось очень удобным в самых разных случаях.
Здравствуйте, Centaur, Вы писали:
C>Вот этот комментарий ясно показывает, что Employee IS NOT A Person. Employee — это роль Person’а в ассоциации Person *—* Company.
Хм... А можете прокомментировать более развёрнуто (хотя бы пару use case-ов)?
Здравствуйте, Crauberg, Вы писали:
C>>Вот этот комментарий ясно показывает, что Employee IS NOT A Person. Employee — это роль Person’а в ассоциации Person *—* Company.
C>Хм... А можете прокомментировать более развёрнуто (хотя бы пару use case-ов)?
Каких юзкейсов? Я пока только про статическую структуру говорил.
OK, юзкейс 1: Person hr_director может принять Person applicant на работу в Company company на должность Position new_position с окладом double new_salary, при условии [(существует Employment hr)(hr.employee == hr_director && hr.employer == company && hr.position == Position::HrDirector && !hr.terminated)], при этом создаётся новый Employment(employee=applicant, employer=company, position=new_position, salary=new_salary, trial=true).
Юзкейс 2: Person employee может уволиться из Company company, при условии [(существует Employment em)(em.employee == employee && em.employer == company && !em.terminated)], постусловие [em.terminated && !(существует Employment em2)(!em2.terminated && em2.boss == em)].