Первоначально опубликовано @ хэштег.

Эредиа, Коста-Рика, 05.11.2022

Итак, люди, которые меня знают, давно спрашивали меня, поэтому я дам вам один из лучших советов (и руководств), которые вы когда-либо получали по многоуровневому программированию бэкенда, скромность в сторону.

ПРИМЕЧАНИЕ: Это действительно небольшая часть моей более крупной многоуровневой архитектуры на основе SOLID, которую я нигде не публиковал. Если какая-либо часть этого неясна для вас, не стесняйтесь задавать вопросы.


Введение

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

Кроме того, я объясню это, используя С#.

Поэтому в оставшейся части этой статьи подумайте о многоуровневом бэкэнд-проекте, в котором есть как минимум уровень репозитория и уровень сервиса. Если вы думаете ASP.Net, тогда также подумайте о том, что уровень представления представляет собой контроллеры ASP.Net. я являюсь думаю ASP.Net, так что это то, что вы увидите здесь.


Обычный бизнес

На сегодняшний день наиболее распространенным способом программирования серверной многоуровневой службы RESTful является создание одного репозитория и одной службы для каждой из отдельных сущностей, определенных в базе данных, более или менее. Возможно, у вас нет отношения 1:1 между сущностями и таблицами базы данных, но обычно это так.

Также бывает так, что в типичном проекте среднего размера количество объектов может превышать 100. Так что, просто думая об этом, вы, возможно, закатываете глаза, думая: «Ну вот, опять, да начнется вечеринка копирования/вставки!» Особенности модели Это моя идея, которая значительно сократит производство репозиториев и классов обслуживания. Это даже поможет вам с валидатором и другими типами классов, в некоторой степени.


Определения

Прежде чем двигаться дальше, нужно определить пару терминов.

  1. Модель: Это отдельные данные, логически сгруппированные вместе. Модели обычно представлены НЕМНОГО классы.
  2. Организация: Термин взят из Отношения сущностей диаграммы, которые представляют собой диаграммы, используемые для описания реляционной базы данных. Ан организация любой модель который может быть однозначно идентифицирован с помощью первичный или уникальный ключ. Это единственная разница: Сущность = Модель с ПК.

Просто чтобы убедиться, что вы это понимаете, модели могут быть результатом агрегирования данных БД (у которых есть PK). Агрегирование — это вычисление, которое на самом деле нигде не сохраняется, поэтому агрегированные данные обычно моделируются с помощью модельа не организация. Такие данные не имеют первичного ключа.


Особенности модели

Думайте о своих данных с точки зрения сходства. Что первое приходит вам в голову? Кончик: Я уже вживил это тебе в мозг, Зарождение стиль. Вот так! Большая часть ваших данных Можно идентифицироваться первичным ключом.

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

А особенность модели — это любой признак данных, который можно использовать сам по себе. Так просто.


Функция модели IEntity

Итак, наиболее очевидная и, вероятно, наиболее полезная функция модели будет называться IEntity. Почему? Поскольку мы определяем функции модели как интерфейсы.

IEntity особенность модели гласит, что любая модель, которая ее реализует, имеет первичный ключ, и указанный первичный ключ доступен через Id имущество. Это эффективно делает модель навсегда организация.

Вот:

public interface IEntity<TKey>
    where TKey : IComparable<TKey>, IEquatable<TKey>
{
    TKey Id { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Я знаю, это выглядит пугающе. Однако это необходимо, потому что, по моему опыту, в больших проектах вы можете в конечном итоге заплатить сверхвысокую цену за хранилище базы данных, если просто решите, что все PK будут иметь размер 8 байт.

Но вот что: если вы готовы заплатить приз и Можно сделать все первичные ключи типа bigint или любой другой 8-байтовый тип данных, поддерживаемый вашей базой данных, дерзайте! В этом случае, IEntity можно сильно упростить до этого:

public interface IEntity
{
    long Id { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Гораздо лучше, я знаю.

Однако мир устроен не так просто, поэтому я притворюсь, что не знаю об этой упрощенной версии IEntity и продолжу объяснять на основе первой, общей версии.


Параметр типа TKey

В основном мы говорим: Сущности будут иметь Id свойство, которое является первичным ключом сущности, но тип данных ключа различается.

Ограничения типа в where пункт существует, чтобы убедиться, что у нас действительно есть возможность сортировать списки на основе первичного ключа (IComparable<TKey>) и найти определенные ключи в коллекциях сущностей (IEquatable<TKey>). Это может иметь место, когда вы кэшируете относительно небольшое количество справочных данных (страны, регионы, языки и т. д.), чтобы свести к минимуму использование базы данных.

ПРИМЕЧАНИЕ: На самом деле я создаю базу Entity<TKey> базовый класс, реализующий IComparable<Entity<TKey>>, IEquatable<Entity<TKey>> а также IEquatable<TKey>но я намеренно оставлю его в этой и без того длинной статье.


Примеры IEntity

Если бы у нас была таблица с именем Countriesи зная, что стран меньше 256 (но мы близки), мы могли бы определить первичный ключ таблицы как tinyint или любой другой тип в вашей базе данных, который использует только один байт. На самом деле я бы сказал, что мы слишком близки к пределу, поэтому лично я бы использовал 2-байтовый тип данных. В С# это будет:

public class Country : IEntity<short>
{
    public short Id { get; set; }
    public string Code { get; set; }
    public string Name { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

В качестве второго примера смоделируем таблицу Users.

public class User : IEntity<int>
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
    public string GivenName { get; set; }
    public string SurName { get; set; }
    public string Name { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Имеет ли это смысл? Вы следите? Я надеюсь вы. Если нет, не стесняйтесь оставить комментарий.

Хорошо, посмотрим, как мой Зарождение навыки работают на вас. Изучив приведенные выше модели, не могли бы вы рассказать мне об одной особенности модели, которую можно извлечь?


Функция модели INamed

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

INamed особенность модели гласит, что любая модель, которая ее реализует, имеет (удобочитаемое) имя, и это имя доступно через Name свойство типа string.

Вот:

public interface INamed
{
    string Name { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Именованные примеры

Обновим примеры из IEntity:

За Country:

public class Country : IEntity<short>, INamed
{
    public short Id { get; set; }
    public string Code { get; set; }
    public string Name { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

За User:

public class User : IEntity<int>, INamed
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
    public string GivenName { get; set; }
    public string SurName { get; set; }
    public string Name { get; set; }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Я мог бы продолжить с другими функциями модели, если спрос высок в другой статье. Например, IAlternateKey особенность модели, которая фактически позволяет UPSERT операций, или IActivatable функция модели, позволяющая мягкое удаление. В любом случае, на самом деле нет ограничений на количество элементов модели, которые можно создать. Достаточно об этом, давайте посмотрим, как особенности модели помогите написать код.


Написание базового кода на основе особенностей модели

Мы прошли через трудности определения особенности модели не потому, что нам было нечего делать, а потому, что у нас есть более высокая цель: упростить производство репозиториев и сервисов. Никогда не теряйте из виду свою цель.


Основные функции RESTful

В службе RESTful мы обычно предоставляем следующее основной функции на ресурс (сущность):

  • Получить всю коллекцию ресурса.
  • Получить отдельный ресурс по первичному ключу.
  • Добавьте отдельный ресурс.
  • Обновите отдельный ресурс.
  • Удалить отдельный ресурс.

В зависимости от случая реализуется или разрешается большее или меньшее количество функций.


Базовый класс репозитория

Просто с помощью IEntity модели, мы можем реализовать базовый класс репозитория, который сразу реализует 3 основные функции в полном объеме: получить все, получить один и удалить.

ПРИМЕЧАНИЕ: Код на самом деле неполный, потому что я намеренно не выбрал ORM. В зависимости от вашего выбора ORM вашему базовому классу потребуются службы и другие данные, введенные через его конструктор, в зависимости от вашего выбора.

public abstract class EntityRepository<TEntity, TKey>
    where TEntity : IEntity<TKey>
    where TKey : IComparable<TKey>, IEquatable<TKey>
{
    public Task<IEnumerable<TEntity>> GetAllAsync()
    {
        // Use Dapper or EF or your favorite ORM to get all.
    }

    public Task<TEntity> GetAsync(TKey id)
    {
        // Use your ORM to get by ID.  You can access the Id property on variables of type TEntity.
        // I'll write LINQ to demonstrate, assuming EF.
        return _dbSet.Where(e => e.Id.Equals(id)).SingleOrDefaultAsync();
    }

    public async Task<TEntity> DeleteAsync(TKey id)
    {
        // Use your ORM to delete.  Personally, I always GET before deleting, and I return 
        // the last known version of the entity being deleted.
        TEntity toBeDeleted = await GetAsync(id);
        // Now delete using your ORM.
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь, используя эту базу, вы действительно можете создавать репозитории, не прибегая к копированию/вставке, СУХОЙ— сторона-нарушитель.

public class CountryRepository : EntityRepository<Country, short>
{ }

public class UserRepository : EntityRepository<User, int>
{ }
Войти в полноэкранный режим

Выйти из полноэкранного режима

Эти репозитории полностью работоспособный на трех основных функциях REST, упомянутых выше. Я уже привлек ваше внимание? После выбора ORM вы можете написать код в базовом классе для выполнения двух других функций REST. По крайней мере, я могу засвидетельствовать это, если ORM — мой любимый щеголеватыйили раздражающий Структура сущности (почему меня это раздражает).


Базовый класс службы

Хорошо, есть трюк, о котором я пока не упоминал, который необходим для создания базового класса службы. По сути, нам нужен тип данных, который мы можем использовать для определения нужного репозитория. Вы могли бы подумать, что дженерики плюс where пункт, который гарантирует, что он является производным типом базового класса репозитория. Это нехорошо, потому что будут репозитории, которые, возможно, не унаследованы от этого класса.

Таким образом, хитрость заключается в том, чтобы фактически подготовить интерфейс базового репозитория и реализовать его в базовом репозитории.

public interface IEntityRepository<TEntity, TKey>
    where TEntity : IEntity<TKey>
    where TKey : IComparable<TKey>, IEquatable<TKey>
{
    Task<IEnumerable<TEntity>> GetAllAsync();
    Task<TEntity> GetAsync(TKey id);
    Task<TEntity> DeleteAsync(TKey id);
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь обновите базовый класс репозитория:

public abstract class EntityRepository<TEntity, TKey> : IEntityRepository<TEntity, TKey>
// Etc.  The rest is the same.
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь у нас есть подходящий тип для нашего объекта репозитория в базовом классе службы.

public abstract class EntityService<TEntity, TKey, TRepository>
    where TEntity : IEntity<TKey>
    where TKey : IComparable<TKey>, IEquatable<TKey>
    where TRepository : IEntityRepository<TEntity, TKey>
{
    // Our repository object, usually passed via the constructor using dependency injection.
    private readonly TRepository _repository;

    public virtual Task<IEnumerable> GetAllAsync()
        => _repository.GetAllAsync();

    public virtual Task<TEntity> GetAsync(TKey id)
        => _repository.GetAsync(id);

    public virtual Task<TEntity> DeleteAsync(TKey id)
        => _repository.DeleteAsync(id);
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Наконец, как и в случае с репозиториями, мы объявляем конкретные классы для каждой сущности:

public class CountryService : EntityService<Country, short, ???>
{ }

public class UserService : EntityService<User, int, ???>
{ }
Войти в полноэкранный режим

Выйти из полноэкранного режима

Ой ой!! Что случилось? Нам не хватает типов данных репозитория. Да, мы могли бы использовать CountryRepository а также UserRepository и это сработало бы, но лучшей практикой внедрения зависимостей (по причинам, не обсуждаемым здесь) является внедрение интерфейсов, а не конкретных классов.

Чтобы закончить это правильно, мы должны определить интерфейсы для каждого репозитория:

public interface ICountryRepository : IEntityRepository<Country, short>
{ }

public interface IUserRepository : IEntityRepository<User, int>
{ }
Войти в полноэкранный режим

Выйти из полноэкранного режима

Обновим классы репозитория:

public class CountryRepository : EntityRepository<Country, short>, ICountryRepository
{ }

public class UserRepository : EntityRepository<User, int>, IUserRepository
{ }
Войти в полноэкранный режим

Выйти из полноэкранного режима

Это было не так уж плохо. Теперь закончим:

public class CountryService : EntityService<Country, short, ICountryRepository>
{ }

public class UserService : EntityService<User, int, IUserRepository>
{ }
Войти в полноэкранный режим

Выйти из полноэкранного режима

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


Об упрощениях в базовом классе услуг

Как я уже упоминал, это лишь малая часть моей многоуровневой архитектуры, основанной на SOLID, значительно большей. Без объяснения других частей моей архитектуры я не могу показать более сложный базовый класс службы, например, включая выполнение валидаторов данных, или как стандартизировать возвращаемые типы методов службы (потому что простое возвращение объекта не сокращает его). в реальных проектах).

Я по крайней мере сделал методы virtual поэтому наследники могут добавить к ним некоторую бизнес-логику путем переопределения.


Вывод

Я знаю, что некоторые из вас сочтут, что мы охватили не все. Мы не сделали. Ты прав. Я мог бы написать целую книгу об этом и других передовых методах, пока буду объяснять свою архитектуру. Если вам понравился контент, дайте мне знать, комментируя.

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


Надеюсь, вам понравилось это чтение. Это не то, что вы найдете где-либо еще. Это письмо от меня к вам, надеюсь, оно побудит вас изучить возможности, которые предоставляет выбранный вами язык программирования, и, надеюсь, поощрит ваше творчество. Но прежде всего, демонстрируя, что СУХОЙ вполне возможно во многих, многих сценариях, какими бы сложными они ни казались.

Удачного кодирования.