Во-первых, если Вы имеете дело с доменными объектами, то старайтесь всегда выделять базовые (общие) свойства.
// Общий для всех доменных объектов интерфейс.
// Очень удобно работать с доменными объектами
// зная, что все они обладают общим свойством.
// Например, у из них каждого есть идентификатор Id.
interface IEntity
{
int Id { get; set; }
// Здесь можно продолжить описывать ОБЩИЕ для всех
// свойства нашего доменного объекта:
//
// bool Enabled { get; set; }
// DateTime CreatedAt { get; set; }
}
Во-вторых, очень часто мне приходиться описывать доменный объект при помощи интерфейса, так это удобно и правильно, советую и Вам применять эту тактику:
// Интерфейс доменного объекта "Человек".
// Во всех сервис-методах, в методах доступа к данным(репозитории),
// в модели представления, используем именно IPerson.
interface IPerson : IEntity
{
string Name { get; set; }
int Age { get; set; }
// Логика, над доменным объектом.
void SayHello();
}
И в-третьих, отдельно от описания которого, следует и реализация:
// Конкретная реализация интерфейса IPerson:
partial class Person : IPerson
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine("{0}: Hello!", Name);
}
// Если надо, то после рефакторинга здесь
// могут появиться protected/private методы.
}
или при использовании любого ORM, например Entity Framework, при таком подходе Вы не зависите от конкретного ORM (зависимость только от интерфейса IPerson):
// Если мы имеем дело с Entity Framework
// (или любой другой ORM), в котором кодогенерацией
// уже был сгенерирован класс Person с нужными полями...
partial class Person : EntityObject
{
// Здесь нам EF нагенерировал кучу всего...
// в том числе реализовал Id, Name, Age.
// Этот код руками трогать нельзя, иначе
// повторная кодогенерация перетрет все труды!
}
// ... то нам просто остается реализовать
// не реализованное через partial класс:
partial class Person : IPerson
{
public void SayHello()
{
Console.WriteLine("{0}: Hello!", Name);
}
}
Ссылки:
Как быть с ассоциированными колекциями при таком подходе?
ОтветитьУдалитьНапример у Person есть множество Order.
@hazzik
ОтветитьУдалитьХороший вопрос!
interface IOrder {}
interface IPerson
{
IOrder[] Orders{get;set;}
}
partial class Person : IPerson
{
public IOrder[] Orders
{
get
{
[здесь маппинг на внутреннюю коллекцию]
}
set
{
[здесь маппинг на внутреннюю коллекцию]
}
}
}
Можно подробнее вот про эту часть: [здесь маппинг на внутреннюю коллекцию], я хотел узнать именно это?
ОтветитьУдалитьТеперь другой аспект - в один прекрасный день новый заказчкик вашего продукта говорит, что он хочет использовать другую ORM, и это его ключевое условие, а упускать заказчика не хочется.
Тебе необходимо будет все свои интерфейсы доменных объектов IProduct, IOrder и т.д. переимплементировать под новую ORM, а в реальном приложении этих интерфейса не 2, и не 10, а много больше.
Вознникнет огромное дублирование кода, и как ты собираешься поддерживать эти интерфейсы?
ЗЫ: для убедительности - проект просто огромный, и заказчик солидный с большими деньгами.
ЗЫЫ: премодерация + капча это перебор.
@hazzik
ОтветитьУдалитьДа, конечно. Например, если использовать EF:
partial class Person : IPerson
{
public IOrder[] Orders
{
get
{
this.Order.Load();
return this.Order.ToArray();
}
}
}
При смене ORM, сильнее всего пострадает репозитарий, так как именно на него возлагается функционал добавления, сохранения, удаления объектов, ну и не обойдется без модификации доменных объектов.
С другой стороны, не используя интерфейсы, при смене ORM пришлось бы вмешиваться в логику сервис-методов. Это ведь не совсем красиво, когда ты описываешь интерфейсную часть репозитария или сервиса и при этом привязываешься к конкретному ORM, используя его как домен. Например, новый ORM все поля оборачивал бы string в ILazy, а старые сервис-методы были заточены под обычный string. А с маппингом проблема вроде решается.
Да, если использовать абстрактные интерфейсы, то придется попотеть ради универсальности. И в общем, этого достаточно для работы, мне хватает :)
Я правильно понял, что основная мысль - это создание интерфейсов для доменных объектов?
ОтветитьУдалитьКак-то странно, что доменный объект может писать в консоль. Зачем ему знать, куда он будет выводить сообщения? Это совсем не его ответственность.
я бы хотел вклиниться в дискуссию со своими заметками :)
ОтветитьУдалить1.
interface IEntity
{
int Id { get; set; }
.....
не всегда ж во всех сущностях идентификаторами являются целые числа, потому я не вижу необходимости в этом интерфейсе, разве что его сделать обобщением.. да и тогда не понимаю, зачем он нужен.
2.
interface IPerson : IEntity
{
string Name { get; set; }
int Age { get; set; }
// Логика, над доменным объектом.
void SayHello();
}
я уже нахоливарился по поводу выноса логики в доменный объект и решил для себя, что нет универсального подхода - всё зависит от поставленной задачи, требований и сложности проекта.
3.
partial class Person : EntityObject {...}
Жесть, реально, при смене ОРМ твои доменные объекты перестанут быть наследниками EntityObject - а это, согласись, весьма серьёзные изменения для него. А если таких объектов у тебя не 2, не 20, а 200? Опять же, если в требованиях четко указано, что менять ОРМ не планируется и именно EF требует заказчик, то, почему бы и нет :)
4.
// Во всех сервис-методах, в методах доступа к данным(репозитории),
// в модели представления, используем именно IPerson.
interface IPerson : IEntity {...}
Можно посмотреть на код, который бы добавлял человека в базу? Где и как экземпляр этого класса будет инстанцироваться? то есть например, пользователь нажимает кнопку создать, где то в логике создается экземпляр IPerson (только как создается? о классе Person логика ведь не должна знать? у неё свой наследник интерфейса IPerson?), потом заполняются поля этого экземпляра и он едет в репозиторий, который принимает IPerson. Ну теперь в репозитории: у нас нечто, что реализует IPerson, но неизвестно, можно ли это нечно мапить в EF - а, значит, мы в репозитории тоже создаём экземпляр Person, заполняем его поля данными из пришедшего в параметрах IPerson и только тогда добавляем? В общем, хотелось бы на код поглядеть :)
>ну и не обойдется без модификации доменных объектов.
ОтветитьУдалитьВажное условие моей задачи: НЕ полностью перейти на другую ОРМ, а сделать дополнительно ее поддержку. Не будешь же ты своим существующим клиентам впаривать новую ORM.
@Александр Бындю
ОтветитьУдалитьДа, основная мысль в том, чтобы при описании интерфейсов сервиса IPersonService или репозитория IPersonRepository, использовать интерфейс IPerson, в противном случае мы будем иметь зависимость например от ORM.
С консолью Вы правы, но это всего лишь пример :)
@Артём
ОтветитьУдалить1. IEntity.Id удобно использовать в общих фильтрах, например GetById[TEntity](int id).
Плюс IEntity может выступает в роли whrere условия у generic типов.
Тип у Id в IEntity можно сделать любым, например struct Identificator {} и дальше управлять им.
2. Спорить не буду, дело вкуса :)
3. При смене ORM всегда будут сложности.
4. Есть код:
_dc - наш EF.
IAccount - домен.
Account : IAccount - сущность в EF.
Account.New - мой метод, строит экземпляр Account
При добавлении я не передаю сам объект:
public void AddNew(string login, string email, string passwordHash)
{
_dc.AddToAccount(Account.New(login, email, passwordHash));
}
А вот при поиске(фильтре):
public IAccount FindBySessionKey(Guid sessionKey)
{
return _dc.Account.Enabled().Where(a => a.SessionKey == sessionKey).FirstOrDefault();
}
@hazzik
ОтветитьУдалитьЕсли поддержка старой и новой, тогда как-то так:
class Person : IPerson
{
public string Name
{
if(isUseNewORM)
{return [новая ORM]}
else
{return [старая ORM]}
}
}
Это псевдокод, ясно что от if надо избавиться по-умному. Например используя IoC: Resolve[IORMMapper]("ef");
а как это будет работать одновременно с двумя репозиториями?
ОтветитьУдалитьНапример, один EF, а другой просто отправляет объекты на web-сервис.
Это как то решается с помощью partial-классов?
@ankstoo
ОтветитьУдалитьПервое что приходит в голову, это сделать обертку или прослойку для EF и WebServiceProxy, и попытаться использовать ее. Хотя конечно надо смотреть отдельно задачу и под нее строить определенную архитектуру.
В основном, partial класс нужен только когда, когда мы хотим расширить сгенерированный машиной код. Решать другие задачи partial не умеет :)
ОтветитьУдалить@Илья
ОтветитьУдалить1. Все бизнес объекты ты от IEntity не отнаследуешь, потому что:
а) не у всех объектов идентификаторы одинаковые (Ну, это можно решить обобщениями)
б) не у всех бизнес объектов есть идентификаторы, например, отношения многие-ко-многим (кстати, такие объекты отказываются мапиться в Linq2SQL)
Ну, суть этого интерфейса я понял, это ладно.
3. Можно свести набор этих сложностей к минимуму :).
4. ну, с аккаунтом всё понятно, а если у тебя заказ и список товаров, и всё это нужно положить в базу в рамках одной транзакции, как бы ты это сделал?
@hazzik
"Не будешь же ты своим существующим клиентам впаривать новую ORM"
я ни в жись не буду, но бывают случаи, когда по поводу ОРМ заказчик ещё не определился, или какой нибудь супер крутой программист из конторы заказчика поглядит на код, что уже 2 месяца в разработке и скажет, что "L2SQL фигня, BLToolkit работает намного быстрее, а вот эти данные вообще надо пролучать из сервиса, а сервис будет запущен через месяц" ну и всё такое прочее, а в техзадании стоит галка "расширяемость и модульность системы" и сроки давят - вот тогда начинаешь думать, как бы спроектировать систему так, чтобы изменение одной её части не затрагивало никакие другие
@Артём
ОтветитьУдалитьФразой: "Не будешь же ты своим существующим клиентам впаривать новую ORM" - под этим я имел ввиду, что есть уже заказчики с внедренным продуктом, которым нужна только поддержка + обновление + исправление багов. В большинстве случаев твой продукт уже проинтегрирован с другими продуктами, часто на уровне домена и т.п.
@Илья
Ваше решение не заработает.
@Артем
ОтветитьУдалить4. Эту проблему берет на себя EF. Достаточно "накопить" в нем изменения (тот же репозитарий), а потом вызывать метод Save у репозитария. На каждую транзакцию придется строить новый экземпляр EF.
@hazzik
ОтветитьУдалитьА как бы Вы сделали поддержку 2-х и более ORM одновременно, не привязываясь конкретно к каждой?
4. тут можно заставить создавать объекты сам репозиторий.
ОтветитьУдалитьIAccount acc = Repository.NewAccount();
ShowUI(acc);
Repository.AddAccount(acc);
Repository.Save();
@Артём сущность и доменный объект это 2 разных понятия.
ОтветитьУдалитьСущность от ValuеОbject отличает механизмом идентификации. Идентификатором сущности является ключ. Идентификатором ValueObject совокупность его полей (зачастую всех).
@hazzik понял тебя
ОтветитьУдалить@Илья - так в том то и дело, где ты будешь вызывать начало/конец транзакции? не в коде страницы жеж :)
@ankstoo конечно можно, мало того - иногда так и делаю, только вот проблема - экземпляр доменного объекта почему то создает репозиторий - это вообще никак не его обязанность :)
@Артём
ОтветитьУдалитьКонечно, все в сервис-классе(Service-layer), который инжектирует в себя репозитарий(Repository).
@Илья, вы не понимаете сути домена. Домен это ядро приложения. Если ваш домен зависит от ORM - все ваше приложение зависит от ORM.
ОтветитьУдалитьРешение следующее: Repository оперирует именно доменными объектами, и ВНУТРИ себя ЧЕРЕЗ мэпер делает преобразование DataRow <-> Entity.
NHibernate в общем случае позволяет избавиться от мапинга, потому что ОН уже берет на себя это преобразование.
Если вы читали PoEAA, то вы не могли не заметить, что почти все патерны из нее реализованы внутри NH, но естественно называются по-другому.
@Илья
ОтветитьУдалитьто есть у тебя сервис-класс управляет транзакциями? Это не гуд
@Артем
ОтветитьУдалитьПочему? Достаточно приемлемо. Сервис открывает транцакцию, точнее UnitOfWork, а репозитории работают в текущем UnitOfWork, в конце делаем Commit для UnitOfWork.
небольшой пример, если можно
ОтветитьУдалитьНа каждый экземпляр сервиса создаем новый репозиторий, который в себе создает UnitOfWork, либо инжектирует его:
ОтветитьУдалитьIRepository r = new Repository(/*инжектирует*/)
В сервис метод инжектируем созданный репозиторий, который потом используем в сервис методе.
void MakeBigWork()
{
// используем наши репозиторий..
r.Save(); //<- Save сохранит все измеения в БД
}
Можно использовать TransactionScope, все зависит от конкретной задачи
Это если по-простому.
Либо, можно посмотреть исходники Kigg.codeplex.com, там подобное реализовано очень хорошо, через абстрактный интерфейс IDataBase.
@hazzik
ОтветитьУдалитьЗдесь Вы ошибаетесь, что такое домен и как с ним работать я знаю.
>>Если ваш домен зависит от ORM - все ваше приложение зависит от ORM.
Пересмотрите топики, я это сказал еще в самом начале. Собственно здесь мы и пытаемся разорвать эту связь.
С NHibernate не работал, но представляю, что он работает как стандартный ORM.
>> Пересмотрите топики, я это сказал еще в самом начале. Собственно здесь мы и пытаемся разорвать эту связь.
ОтветитьУдалитьЕсли ORM может работать с доменом как с POCO, т.е. не требовать от домена наследоваться от спец класса, реализовывать спец интерфейсы или иметь спец атрибуты, то такая проблема даже не появляется.
NHibernate умее, EF в 4 версии вроде тоже.
Да, спасибо, абсолютно точно подмечено. В случае с EF нам "мешает" базовый тип System.Data.Objects.DataClasses.EntityObject.
ОтветитьУдалитьКстати, глянул в NHibernate, там такой проблемы нет. Надо будет глянуть применение T4 в EF4.
Что будет с инвариантной логикой, которая находиться в сущностях? Куда она денется при смене ОРМ?
ОтветитьУдалить@ankstoo
ОтветитьУдалитьДоменная сущность и объект отраженный из базы это совершенно разные вещи. И отражение называется Object Relation Mapping. А не Domain Entity-Relation Mapping
@hazzik
ОтветитьУдалитьСмотря как организована эта логика. Формально смена ORM никак не должна на нее влиять, и это правильно.
Но зачастую сталкиваешься с тем, что домен (отражающий реальную предметную область) есть просто реляционное отражение базы (так как нам удобно это хранить), плюс поведение (логика).
@Илья
ОтветитьУдалитьЯ хочу получить конкретный ответ:)
@hazzik
ОтветитьУдалитьВ идеале :)
Логика никуда не должна деваться (если только не захотите вынести ее в helper-ы)
Логика никак не должна меняться.
>если только не захотите вынести ее в helper-ы
ОтветитьУдалитьЧем это будет отличаться от примера 1, из статьи Александра Бындю?
Ну смотрите, псевдо-реальный пример:
У меня есть IAccount:
public interface IAccount {
bool Activated {get;}
void Activate(); /*other methods*/
}
И есть конкретная реализация под EF:
public class Account : EntityObject {
public bool Activated {get;set;} // ef realization
}
partial class Account : IAccount {
public void Activate() {
this.Activated = true;
// other activation logic
}
}
И теперь мне нужно *ДОБАВИТЬ* поддержку NH, как того требует мой щедрый новый заказчик. Добавить таким образом, чтоб существующие пользователи моего продукта не испытывали никаких неудобств - их полностью устраивает EF и они не собираются его менять.
Мои действия?
1. В helper-ы можно вынести повторный код.
ОтветитьУдалитьpartial class Account : IAccount {
public void Some() {
// вызвать helper, как вариант
}
}
Те же самые kigg, например в классе Tag, свойство Stories юзает EntityHelper.
2. Я не силен в NH, не могу сказать, может там есть нечто специфическое. Но подобное реализовано в kigg, где организовано переключение между LinkToSql и EF. Я особо не копался в исходниках, поэтому детально не расскажу. Например, можно подсмотреть реализацию свойства int StoryCount, в классе Tag.
PS: Да, щедрость заказчика определяет многое :)
Это вопрос из серии, "Как сделать поддержку нескольких систем логирования или нескольких IoC контейнеров". Данная проблема решается в лоб, с помощью введения своих внутренних интерфейсов ILog, IDependencyResolver.
ОтветитьУдалить> Я не силен в NH, не могу сказать, может там есть нечто специфическое.
ОтветитьУдалитьПод NH имеловсь ввиду сферический ORM в ваккууме, будь то L2S, NH, LLBLGenPro.
Чем тогда ваше решение с хелперами будет лучше чем пример 1 из сататьи Александра, от которого он так старательно избавляется?
Если например код из CalculateOrdersSum будет повторяться в других сущностях, то его проще вынести в helper.
ОтветитьУдалитьВ моих проектах всегда есть helper-расширения для IQueryble.
Например:
return account.Orders.Enabled().Where(order => order.IsComplete == false);
Где под Enabled() позволяет избежать дублирование Where(entity => entity.Enabled);
@hazzik
ОтветитьУдалить>> Доменная сущность и объект отраженный из базы это совершенно разные вещи.
Т.е. сначала мапим базу на объекты данных. Потом мапим объекты данных на доменную модель? Так?
@ankstoo
ОтветитьУдалитьТак.
Просто попытайтесь выделить ответственности такой на половину сгенерированной entity. И вы все сами поймете. Она несет в себе лишнюю ответсвенность - хранит информацию о том как она сохраняется в базе. Таким образом, видно что большинство ORM, требующие наследоваться от своего базового класса - нарушают SRP.
И еще раз повторю, что все что здесь мной сказано относиться к моему примеру с щедрым новым заказчиком и требованием сохранить старую ОРМ для старых заказчиков.
PS: Если использовать интерфейсы для того, что нагенерировала нам ОРМ, то можно возложить обязанность преобразования на сам доменный объект.
пример:
public interface IAccountRow { // наш интерфейс, чтоб отвязаться от ORM
int Id {get;set;}
string Name {get;set;}
}
public class AccountRow : EntityObject, IAccountRow { // то что нам нагенерировала EF
public int Id {get;set;}
public string Name {get;set;}
}
public class Account : IEntity { // доменный объект
private IAccountRow row;
public int Id {get{ return row.Id;} set{ row.Id = value;}}
public string Name {get { return row.Name;}set{ row.Name = value;}}
}
Но с этим подходом когда-нибудь также возникнут трудности.
PS: следуйте SOLID и у вас все получиться.
PSS: помните, что SOLID это только принципы, а не законы и правила. Они требуют подхода к делу с головой.
@hazzik
ОтветитьУдалить>> И вы все сами поймете.
я понимаю :) Если домен наследуется от EntityObject или чего-то подобного - это плохо, т.к. он зависит от persistence. Намного лучше, чтобы домен был POCO.
Но в случае, например, NHibernate, вводить дополнительный слой ORM-объектов бессмысленно. NH отлично работает с POCO доменом. Об этом я и писал.
>> PS:... Но с этим подходом когда-нибудь также возникнут трудности.
Тут трудности очевидны. Мы не можем использовать доменный объект без persistence (Например в unit-тестах). Или надо писать MockAccountRow.
@ankstoo
ОтветитьУдалитьк вам притензий нет и я поддерживаю точку зрения, что если ОРМ не предъявляет к сохраняемым объектам специфических требований, то от дополнительного слоя можно и нужно отказаться.
Мне не очень нравятся интерфейсы в качестве доменных объектов, т.к. при смене ОРМ будут проблемы сдублированием кода и его поддержкой.
Занимательная дискуссия получилась..
ОтветитьУдалить