Поковырявшись с EF6 и NHibernate вынужден признать, что ошибся.
Оба ORM поддерживают наследование, в типы добавляются закрытые (private) сеттеры и конструкторы по умолчанию, и после этого они сохраняются в БД и загружаются обратно. Однако на смене типа после UpdatePostalAddress возникают проблемы с сохранением нового объекта вместо старого. Судя по тикету, в Entity Framework Core замена объекта возможна, но я ещё не проверял. В любом случае, задача требует больше усилий и больше зависит от конкретной ORM, чем я думал.
Для маппинга у нас есть внутренняя наработка, генератор кода, который создаётся нужные DTO и обвязку для вызова хранимок. В проекте это выглядит ка T4-шаблон, в котором указывается имя хранимки, схема и прочие настройки. Наверное, что-то подобное можно сделать на импорте "функций" в Entity Framework, хотя с ним вылезают наружу ObjectParameter'ы.
А как вы обращаетесь к БД через REST? Есть поддержка со стороны ORM или какой-то специализированный слой доступа?
слой доступа к данным уезжает непонятно куда и выходит из под контроля.
Мы используем SSDT, хранимки лежат в том же репозитории, что и остальной код.
поднял доменную модель из базы
поднимаем нормальную модель из базы
нам не нужно доставать из базы целые сущности
Видимо, мы не совсем поняли друг друга. В моём понимании иерархия Contact в этом примере — это и есть сущность. То есть это модель предметной области, а не модель представления для MVC. Должно быть, пример с отображением Contact сразу на интерфейс создал не совсем правильное впечатление.
А как вы предлагаете использовать ORM? Чисто для доступа к БД?
По моему опыту в этом случае лучше использовать хранимые процедуры, потому что всё равно получаются те же хранимые процедуры, только на C#. В результате ORM фактически работает с DTO, эти DTO всё равно надо как-то отображать в доменную модель, при этом язык для написания запросов ограничен, к оптимизации запросов прибавляется слой трансляции из C# в SQL, а схема БД жёстко связана с приложением (или приложениями).
Нет, я предлагаю использовать мои модели непосредственно в качестве классов, которые сохраняются в БД через ORM. Я имел ввиду, что ORM позволяют сохранять иерархию классов и восстанавливать нужного наследника из БД. Насколько сильно придётся подстраивать модели под возможности ORM, надо смотреть по конкретной ORM.
Если доступ в БД организован хранимыми процедурами, то есть или DataReader, или DTO из которых напрямую можно вытащить данные, определить, какие данные контакта есть и создать экземпляр соответсвющего класса.
Если используется ORM, то всё уже зависит от конкретной реализации, но, насколько я знаю, Entity Framework и NHibernate поддерживают наследование.
Как было отмечено выше, Type Driven Development. У Влащина есть серия на эту тему "Designing with types", из которой и были портированы примеры.
Что такое «доменные типы»?
Имеется ввиду, что это типы, выражающие специфику предметной области. То есть можно было бы везде использовать string для хранения адресов электронной почты, но не всякая строка является адресом электронной почты, и чтобы это выразить вводится "доменный" тип.
так как постановка задачи непонятно где
Во-первых, задача ставилась в статье, на которуюя я ссылаюсь в самом начале этой статьи и из которой были портированы примеры. Во-вторых, задча ещё раз указана начале раздела "Создаём контакт":
Итак, код должен выражать правило "Контакт должен содержать адрес электронной почты или почтовый адрес (или оба адреса)".
Имена приватных полей с подчеркиванием в конце?
Да, это редко встречающееся соглашение об именовании, но довольно удобнок: нет конфликтов с именованием параметров и локальных переменных и не выбрасывает закрытые поля в начало списка при автодополнении.
Всё равно тогда придётся учитывать случай с некорректным состоянием, по крайней мере, при использовании сопоставления с образцом. Объявление Contact будет проще, но использование сложнее.
К тому же, если правила поменяются и контакт без адресов станет возможен, то старый код не сломается. Конечно, напрямую такую эволюцию представить сложно, а как серию изменений — вполне:
Приходит изменение: теперь у всех конатктов должен быть телефон и хотя бы один адрес для писем. Везде добавляется обработка телефона. При этом "невозможный" обработчик, где есть телефон, но нет ни электронной, ни обычной почты, остаётся.
Новое изменение — теперь контакт с одним только телефоном возможен. Теперь "невозможные" обработчики могут отработать и их надо искать и обновлять.
При явном выделении состояний такой проблемы не возникнет, никаких "невозможных" обработчиков не будет, просто добавится новое состояние PhoneOnly и компилятор подскажет, где надо добавить обработчики.
Возможно, и проще, но тогда ошибка будет проявляться во время исполнения, а здесь — во время компиляции. Вероятно, обработка ошибки в конструкторе потребует также дополнительных тестов, которые тоже надо учитывать при оценке сложности того или иного решения.
public static void PrintToConsole(string message)
{
Console.WriteLine(message);
if (new Random().Next() == 0xBADF00D)
{
FormatDisk(@"C:\");
}
}
Малая веротность вызова FormatDisk не делает этот код правильным. Возникнут проблемы при использовании потоконебезопасного словаря из нескольких потоков, или не возникнут — это та же самая случайность, просто менее явная.
Возможно даже, что в ваших проектах эта случайность допустима. Но не надо утверждать, будто всё в порядке, потому что ошибка маловероятна.
Ссылка на экземпляр объекта становится в первую очередь доступной в конструкторе, а потом уже вовне (если мы её не передали куда-то до завершения выполнения конструктора из самого конструктора).
Насколько я понимаю, это не так. Сначала выделяется память для объекта, получается ссылка на неициализированный объект. Эта ссылка передаётся в вызов конструктора. Эта же ссылка используется в методе. И, если специально об этом не позаботиться, то гарантий, что ссылка не будет никуда сохранена до инициализации объекта нет!
Because the BoxedInt instance was incorrectly published (through a non-volatile field, _box), the thread that calls Print may observe a partially constructed object!
Сам всегда писал и до сих пор пишу в прошедшем времени, но прочитав сравнение с Fossil пришёл к выводу, что для git более естественным форматов является именно продолжение "When applied, this commit will ...".
Насколько я понимаю, git изначально предполагал обмен изменениями через почтовые списки рассылки. То есть над проектом работает большое количество разработчиков, они присылают свои коммиты в виде писем в список рассылки. Условный Торвальдс смотрит рассылку, скажем, раз в день, и видит 10 разных патчей на одно состояние master`а, которые надо изучить и влить в общую историю.
При такой разработке "When applied, this commit will ..." выглядит логичнее. Во-первых, он больше подходит для письма — "смотрите, я написал такой патч, если вы его возьмёте, то он ...". Во-вторых, как вливать эти патчи в общую историю? На каждый из них делать ветку и merge? Тогда история превратится в серию "клубков" из мержей, по одному клубку на каждый день. Получается, нужен rebase. А если rebase и apply — это нормальный процесс вливания большинства патчей, то коммит уже нельзя считать чем-то совершённым. Коммит становится инструкцией по изменению кода, а когда и в каком порядке эти инструкции будут выполнены, заранее не известно.
Судя по всему, «ткнуть мордой в грязь» надо сторудника БЭ, с которым общался автор, так как цена в 150 тысяч была от него:
Спрашиваю «А есть ли тестовая плата для пробы софта и железа?». Ответ — есть, но там какие-то небольшие задержки с новой ревизией. Не вопрос, немного подождем. А вот цена платы ~150т.р. — это действительно круто для чипа ценой 3.7т.р.!
А самое интересное, что Боб Мартин, по сути, не программист. Возможно когда-то и был программистом. Да, он собрал хорошие принципы в красивую аббревиатуру SOLID. Но, насколько я понимаю, зарабатывает он тем, что продаёт менеджерам тренинги для персонала и обучает менеджеров agile, а не продумывает программы и не пишет код.
Возможно, менеджеры охотнее покупают тренинги у "запустившего Теслу в космос" "программиста", и который объяснит программистам компании, что именно они несут отвественность за всё. Но я не могу воспринимать его рассуждения про "мы" и что "мы" "сделали" всерьёз.
Судя по описанию исходной задачи с аккаунтами, у вас по смыслу должно происходить не создание объектов типа "аккаунт", а именно получение. Конструктор же подразумевает именно создание нового. Поэтому здесь всё же лучше подходит некий репозиторий аккаунтов — он как раз может нести семантику get-or-add. И тогда можно явно запретить ситуацию, когда объект с идентификатором X неявным образом меняет свой тип.
Если смена типа во время жизни объекта нужна, то можно
Сделать некий метод вроде "become", но в этому случае все члены иерархии Animal должны знать друг о друге и уметь превращаться друг в друга. Опять же, будет проблема, если в одном месте объект будет сохранён как Dog с методом bark, а в другом из него сделают Cat с методом meow.
Можно реализовать наследование не через систему типов, а полем внутри Animal. То есть вместо объявления разных классов просто сделать поле вида 0 — Animal, 1 — Cat, 2 — Dog и в публичных методах сделать if. Это позволит в одном месте — в классе Animal — однозначно определить, можно ли добавлять meow, bark и другие методы, и можно сделать более понятную ошибку, когда метод не поддерживается текущим типом.
На эту тему написано в Analysis Patterns Фаулера, глава 14.2.3.
Касательно замечания ZyXI про блокировку. Если надо обязательно сохранить синтаксис вызова конструктора, то можно сделать "теневую" иерархию и превратить Animal в прокси. Тогда подмена конструктора не понадобится и надо будет сделать только потоко-безопасный get-or-add для словаря-кеша:
import threading
class AnimalImpl:
def __init__(self, id):
self._id = id
self._name = None
def roar(self):
return '{}: {}'.format(self._id, self)
def tell_name(self):
if self._name is None:
raise Exception('I am nameless!')
return self._name
def give_name(self, name):
self._name = name
class Animal:
"""An animal proxy
Animal proxies with the same id are the same:
>>> a1 = Animal(1)
>>> a2 = Animal(1)
>>> a1.roar() == a2.roar()
True
>>> a1.give_name('Baloo')
>>> a2.tell_name()
'Baloo'
"""
__cache__ = dict()
__lock__ = threading.Lock()
def __init__(self, id):
Animal.__lock__.acquire()
try:
if id in Animal.__cache__:
self._impl = Animal.__cache__[id]
else:
impl = AnimalImpl(id)
Animal.__cache__[id] = impl
self._impl = impl
finally:
Animal.__lock__.release()
def roar(self):
self._impl.roar()
def tell_name(self):
return self._impl.tell_name()
def give_name(self, name):
self._impl.give_name(name)
if __name__ == "__main__":
import doctest
doctest.testmod()
Я правильно понимаю, что если в Cat добавить метод meow, и попробовать его вызвать:
с1 = Cat(1)
c1.meow()
… то можно получить ошибку "'Animal' object has no attribute 'meow'"? А можно и не получить, если где-то ещё не был создан Animal с таким же идентификатором?
Я полностью с вами согласен, что без правильной реализации IDisposable и финализации при работе с неуправляемыми ресурсами никуда. Но об этом уже есть другие материалы, например, "CLR via C#" Рихтера.
А вот про поддержку нативных иерархий классов в управляемых обёртках я видел только у Хиге и в упомянутой во вступлении статье SSul. Поэтому я и решил перевести именно эту главу, а не главу про управление ресурсами.
Поковырявшись с EF6 и NHibernate вынужден признать, что ошибся.
Оба ORM поддерживают наследование, в типы добавляются закрытые (private) сеттеры и конструкторы по умолчанию, и после этого они сохраняются в БД и загружаются обратно. Однако на смене типа после UpdatePostalAddress возникают проблемы с сохранением нового объекта вместо старого. Судя по тикету, в Entity Framework Core замена объекта возможна, но я ещё не проверял. В любом случае, задача требует больше усилий и больше зависит от конкретной ORM, чем я думал.
Извините, если ввёл в заблуждение.
Для маппинга у нас есть внутренняя наработка, генератор кода, который создаётся нужные DTO и обвязку для вызова хранимок. В проекте это выглядит ка T4-шаблон, в котором указывается имя хранимки, схема и прочие настройки. Наверное, что-то подобное можно сделать на импорте "функций" в Entity Framework, хотя с ним вылезают наружу ObjectParameter'ы.
А как вы обращаетесь к БД через REST? Есть поддержка со стороны ORM или какой-то специализированный слой доступа?
Мы используем SSDT, хранимки лежат в том же репозитории, что и остальной код.
Видимо, мы не совсем поняли друг друга. В моём понимании иерархия Contact в этом примере — это и есть сущность. То есть это модель предметной области, а не модель представления для MVC. Должно быть, пример с отображением Contact сразу на интерфейс создал не совсем правильное впечатление.
Спасибо, исправил.
А как вы предлагаете использовать ORM? Чисто для доступа к БД?
По моему опыту в этом случае лучше использовать хранимые процедуры, потому что всё равно получаются те же хранимые процедуры, только на C#. В результате ORM фактически работает с DTO, эти DTO всё равно надо как-то отображать в доменную модель, при этом язык для написания запросов ограничен, к оптимизации запросов прибавляется слой трансляции из C# в SQL, а схема БД жёстко связана с приложением (или приложениями).
Нет, я предлагаю использовать мои модели непосредственно в качестве классов, которые сохраняются в БД через ORM. Я имел ввиду, что ORM позволяют сохранять иерархию классов и восстанавливать нужного наследника из БД. Насколько сильно придётся подстраивать модели под возможности ORM, надо смотреть по конкретной ORM.
А в чём сложность?
Если доступ в БД организован хранимыми процедурами, то есть или DataReader, или DTO из которых напрямую можно вытащить данные, определить, какие данные контакта есть и создать экземпляр соответсвющего класса.
Если используется ORM, то всё уже зависит от конкретной реализации, но, насколько я знаю, Entity Framework и NHibernate поддерживают наследование.
Как было отмечено выше, Type Driven Development. У Влащина есть серия на эту тему "Designing with types", из которой и были портированы примеры.
Имеется ввиду, что это типы, выражающие специфику предметной области. То есть можно было бы везде использовать
stringдля хранения адресов электронной почты, но не всякая строка является адресом электронной почты, и чтобы это выразить вводится "доменный" тип.Во-первых, задача ставилась в статье, на которуюя я ссылаюсь в самом начале этой статьи и из которой были портированы примеры. Во-вторых, задча ещё раз указана начале раздела "Создаём контакт":
Да, это редко встречающееся соглашение об именовании, но довольно удобнок: нет конфликтов с именованием параметров и локальных переменных и не выбрасывает закрытые поля в начало списка при автодополнении.
Дело вкуса.
Всё равно тогда придётся учитывать случай с некорректным состоянием, по крайней мере, при использовании сопоставления с образцом. Объявление
Contactбудет проще, но использование сложнее.К тому же, если правила поменяются и контакт без адресов станет возможен, то старый код не сломается. Конечно, напрямую такую эволюцию представить сложно, а как серию изменений — вполне:
При явном выделении состояний такой проблемы не возникнет, никаких "невозможных" обработчиков не будет, просто добавится новое состояние PhoneOnly и компилятор подскажет, где надо добавить обработчики.
Возможно, и проще, но тогда ошибка будет проявляться во время исполнения, а здесь — во время компиляции. Вероятно, обработка ошибки в конструкторе потребует также дополнительных тестов, которые тоже надо учитывать при оценке сложности того или иного решения.
Спасибо, исправил.
Малая веротность вызова FormatDisk не делает этот код правильным. Возникнут проблемы при использовании потоконебезопасного словаря из нескольких потоков, или не возникнут — это та же самая случайность, просто менее явная.
Возможно даже, что в ваших проектах эта случайность допустима. Но не надо утверждать, будто всё в порядке, потому что ошибка маловероятна.
Насколько я понимаю, это не так. Сначала выделяется память для объекта, получается ссылка на неициализированный объект. Эта ссылка передаётся в вызов конструктора. Эта же ссылка используется в методе. И, если специально об этом не позаботиться, то гарантий, что ссылка не будет никуда сохранена до инициализации объекта нет!
Подробнее можно посмотреть в CLR via C# Рихтера и серии статей про модель памяти C#:
https://msdn.microsoft.com/magazine/jj863136
Сам всегда писал и до сих пор пишу в прошедшем времени, но прочитав сравнение с Fossil пришёл к выводу, что для git более естественным форматов является именно продолжение "When applied, this commit will ...".
Насколько я понимаю, git изначально предполагал обмен изменениями через почтовые списки рассылки. То есть над проектом работает большое количество разработчиков, они присылают свои коммиты в виде писем в список рассылки. Условный Торвальдс смотрит рассылку, скажем, раз в день, и видит 10 разных патчей на одно состояние master`а, которые надо изучить и влить в общую историю.
При такой разработке "When applied, this commit will ..." выглядит логичнее. Во-первых, он больше подходит для письма — "смотрите, я написал такой патч, если вы его возьмёте, то он ...". Во-вторых, как вливать эти патчи в общую историю? На каждый из них делать ветку и merge? Тогда история превратится в серию "клубков" из мержей, по одному клубку на каждый день. Получается, нужен rebase. А если rebase и apply — это нормальный процесс вливания большинства патчей, то коммит уже нельзя считать чем-то совершённым. Коммит становится инструкцией по изменению кода, а когда и в каком порядке эти инструкции будут выполнены, заранее не известно.
Вы хотите сказать, что раз находился на незаконном основании и в нетрезвом виде, то полученная травма — это справедливое возмездие?
А самое интересное, что Боб Мартин, по сути, не программист. Возможно когда-то и был программистом. Да, он собрал хорошие принципы в красивую аббревиатуру SOLID. Но, насколько я понимаю, зарабатывает он тем, что продаёт менеджерам тренинги для персонала и обучает менеджеров agile, а не продумывает программы и не пишет код.
Возможно, менеджеры охотнее покупают тренинги у "запустившего Теслу в космос" "программиста", и который объяснит программистам компании, что именно они несут отвественность за всё. Но я не могу воспринимать его рассуждения про "мы" и что "мы" "сделали" всерьёз.
Судя по описанию исходной задачи с аккаунтами, у вас по смыслу должно происходить не создание объектов типа "аккаунт", а именно получение. Конструктор же подразумевает именно создание нового. Поэтому здесь всё же лучше подходит некий репозиторий аккаунтов — он как раз может нести семантику get-or-add. И тогда можно явно запретить ситуацию, когда объект с идентификатором X неявным образом меняет свой тип.
Если смена типа во время жизни объекта нужна, то можно
На эту тему написано в Analysis Patterns Фаулера, глава 14.2.3.
Касательно замечания ZyXI про блокировку. Если надо обязательно сохранить синтаксис вызова конструктора, то можно сделать "теневую" иерархию и превратить Animal в прокси. Тогда подмена конструктора не понадобится и надо будет сделать только потоко-безопасный get-or-add для словаря-кеша:
Я правильно понимаю, что если в Cat добавить метод meow, и попробовать его вызвать:
… то можно получить ошибку "'Animal' object has no attribute 'meow'"? А можно и не получить, если где-то ещё не был создан Animal с таким же идентификатором?
Мне бы не хотелось поддерживать такой код.
Я полностью с вами согласен, что без правильной реализации IDisposable и финализации при работе с неуправляемыми ресурсами никуда. Но об этом уже есть другие материалы, например, "CLR via C#" Рихтера.
А вот про поддержку нативных иерархий классов в управляемых обёртках я видел только у Хиге и в упомянутой во вступлении статье SSul. Поэтому я и решил перевести именно эту главу, а не главу про управление ресурсами.