Поставщики типа F # против интерфейсов С# + Entity Framework

Вопрос очень технический, и он тесно связан с различиями в F #/С#. Вполне вероятно, что я мог что-то пропустить. Если вы обнаружите концептуальную ошибку, пожалуйста, прокомментируйте, и я обновлю вопрос.

Давайте начнем с мира С#. Предположим, что у меня есть простой бизнес-объект, назовите его Person (но, пожалуйста, имейте в виду, что есть объекты 100+, намного более сложные, чем в бизнес-области, с которой мы работаем):

public class Person : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

и я использую DI/IOC и, таким образом, я никогда не передаю Person. Скорее я всегда использовал бы интерфейс (упомянутый выше), назовите его IPerson:

public interface IPerson
{
    int PersonId { get; set; }
    string Name { get; set; }
    string LastName { get; set; }
}

Бизнес-требование заключается в том, что человек может быть сериализован/десериализован из базы данных. Допустим, я решил использовать Entity Framework для этого, но фактическая реализация кажется несоответствующей этому вопросу. На данный момент у меня есть возможность ввести класс (ы), связанные с базой данных, например, EFPerson:

public class EFPerson : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

вместе с соответствующими атрибутами и кодом, связанными с базой данных, которые я для краткости IPerson, а затем использую Reflection для копирования свойств интерфейса IPerson между Person и EFPerson ИЛИ просто используйте EFPerson (переданный как IPerson) напрямую ИЛИ делайте что-то еще. Это довольно неактуально, поскольку потребители всегда видят IPerson и поэтому реализация может быть изменена в любое время, когда потребители ничего не знают об этом.

Если мне нужно добавить свойство, то я сначала IPerson интерфейс IPerson (скажем, я добавляю свойство DateTime DateOfBirth { get; set; }), а затем компилятор скажет мне, что исправить. Однако, если я удалю свойство из интерфейса (допустим, мне больше не нужно LastName), компилятор мне не поможет. Однако я могу написать тест на основе отражений, который обеспечит идентичность свойств IPerson, Person, EFPerson и т.д. Это на самом деле не нужно, но это можно сделать, и тогда это будет работать как по волшебству (и да, у нас есть такие тесты, и они работают как по волшебству).

Теперь давайте перейдем к миру F #. Здесь у нас есть поставщики типов, которые полностью исключают необходимость создания объектов базы данных в коде: они создаются автоматически поставщиками типов!

Здорово! Но так ли это?

Во-первых, кто-то должен создать/обновить объекты базы данных, и если в проекте участвует более одного разработчика, то вполне естественно, что база данных может и будет обновляться/понижаться в разных ветвях. Насколько мне известно, это очень болезненно, когда в дело вовлечены провайдеры типа F #. Даже если для обработки миграций используется С# EF Code First, для "счастья" провайдеров F #-типа требуются "обширные танцы шаманов".

Во-вторых, в мире F # все является неизменным по умолчанию (если только мы не делаем его изменяемым), поэтому мы явно не хотим передавать изменяемые объекты базы данных вверх по течению. Это означает, что, как только мы загружаем изменяемую строку из базы данных, мы хотим как можно скорее преобразовать ее в "нативную" неизменяемую структуру F #, чтобы она работала только с чистыми функциями в восходящем направлении. В конце концов, использование чистых функций уменьшает количество необходимых тестов, я думаю, в 5 - 50 раз, в зависимости от домена.

Давайте вернемся к нашей Person. Я пока пропущу все возможные переопределения (например, целое число базы данных в случае F # DU и тому подобное). Итак, наш F # Person будет выглядеть так:

type Person =
    {
        personId : int
        name : string
        lastName : string
    }

Итак, если "завтра" мне нужно добавить dateOfBirth: DateTime к этому типу, то компилятор скажет мне обо всех местах, где это нужно исправить. Это здорово, потому что компилятор С# не скажет мне, где мне нужно добавить эту дату рождения, кроме базы данных. Компилятор F # не скажет мне, что мне нужно пойти и добавить столбец базы данных в таблицу Person. Однако в С#, поскольку мне сначала нужно обновить интерфейс, компилятор скажет мне, какие объекты должны быть исправлены, включая один или несколько объектов базы данных.

Судя по всему, я хочу лучшего из обоих миров в F #. И хотя этого можно добиться с помощью интерфейсов, он просто не чувствует F #. В конце концов, аналог DI/IOC выполняется совсем по-другому в F # и обычно достигается передачей функций, а не интерфейсов.

Итак, вот два вопроса.

  1. Как я могу легко управлять миграцией вверх/вниз базы данных в мире F #? И, для начала, как правильно осуществить миграцию базы данных в мире F #, когда в нее вовлечено много разработчиков?

  2. Как F # способ достичь "лучшего из мира С#", как описано выше: когда я обновляю Person типа F #, а затем исправляю все места, где мне нужно добавлять/удалять свойства записи, какой будет наиболее подходящий способ F # для "сбой" или во время компиляции, или, по крайней мере, во время тестирования, когда я не обновил базу данных, чтобы она соответствовала бизнес-объектам?

Ответ 1

  Как я могу легко управлять миграцией вверх/вниз базы данных в мире F #? И, для начала, как правильно сделать базу данных? Миграция в мире F #, когда в нее вовлечено много разработчиков?

Наиболее естественный способ управления миграцией БД - использовать инструменты, родные для БД, то есть обычный SQL. В нашей команде мы используем пакет dbup, для каждого решения мы создаем небольшой консольный проект, чтобы свернуть миграции db в dev и во время развертывания. Потребительские приложения представлены как на F # (поставщики типов), так и на С# (EF), иногда с одной и той же базой данных. Работает как шарм.

Вы упомянули EF Code First. Все поставщики F # SQL по своей природе являются "Db First", потому что они генерируют типы на основе внешнего источника данных (базы данных), а не наоборот. Я не думаю, что смешивание двух подходов - хорошая идея. Фактически, я бы никому не рекомендовал EF Code First для управления миграциями: простой SQL проще, не требует "обширных танцев шаманов", бесконечно более гибок и понятен гораздо большему количеству людей. Если вас не устраивает создание сценариев SQL вручную и вы решили использовать EF Code First только для автоматической генерации сценариев миграции, то даже дизайнер MS SQL Server Management Studio может сгенерировать сценарии миграции для вас

Как F # способ достичь "лучшего из мира С#", как описано выше: когда я обновляю F # типа Person, а затем исправляю все места, где я нужно добавить/удалить свойства записи, что было бы наиболее соответствующий F # способ "провалиться" либо во время компиляции, либо по крайней мере в время тестирования, когда я не обновил базу данных, чтобы соответствовать бизнесу объект (ы)?

Мой рецепт выглядит следующим образом:

  • Не используйте интерфейсы. как ты сказал :)

интерфейсы, он просто не чувствует путь F #

  • Не позволяйте автоматически сгенерированным типам из провайдера типов просачиваться за пределы тонкого уровня доступа к БД. Они не являются бизнес-объектами, и ни EF-объекты на самом деле не являются.
  • Вместо этого объявите записи F # и/или дискриминируемые объединения как объекты вашего домена. Смоделируйте их, как вам угодно, и не чувствуйте себя стесненными схемой БД.
  • В слое доступа к БД сопоставьте автоматически сгенерированные типы БД с типами F # вашего домена. Каждое использование типов, автоматически сгенерированных провайдером типов, начинается и заканчивается здесь. Да, это означает, что вам нужно писать сопоставления вручную и вводить здесь человеческий фактор, например, Вы можете случайно сопоставить FirstName с LastName. На практике это крошечные накладные расходы и выгоды от развязки перевешивают его на величину.
  • Как убедиться, что вы не забыли нанести на карту какую-либо недвижимость? Это невозможно, F # компилятор выдаст ошибку, если запись не полностью инициализирована.
  • Как добавить новое свойство и не забыть его инициализировать? Начните с кода F #: добавьте новое свойство в запись/записи домена, компилятор F # проведет вас по всем экземплярам записей (обычно только один) и заставит инициализировать его чем-то (вам придется соответствующим образом добавить сценарий миграции/обновить схему базы данных).
  • Как удалить свойство и не забудьте очистить все до схемы БД. Начните с другого конца: удалите столбец из базы данных. Все сопоставления между типами поставщиков типов и записями домена F # будут нарушены и выделены свойства, которые стали избыточными (что более важно, вы заставите вас дважды проверить, действительно ли они избыточны, и пересмотреть ваше решение).
  • Фактически в некоторых сценариях вы можете захотеть сохранить столбец базы данных (например, для исторических целей или в целях аудита) и удалить только свойство из кода F #. Это всего лишь один (и довольно редкий) из множества сценариев, когда удобно отделить модель предметной области от схемы БД.

Короче говоря

  • миграция с помощью простого SQL
  • типы доменов объявляются вручную записями F #
  • ручное сопоставление провайдеров типов с типами доменов F #

Еще короче

Придерживайтесь принципа единой ответственности и наслаждайтесь преимуществами.