Организация репозитория данных

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

Я пытаюсь написать тесты для определения классов и репозитория.

Допустим, у меня есть классы Customer, Order, OrderLine.

Теперь могу ли я создать класс Order как что-то вроде

abstract class Entity {
    int ID { get; set; }
}

class Order : Entity {
    Customer Customer { get; set; }
    List<OrderLine> OrderLines { get; set; }
}

Что будет хорошо сериализовано, но, если меня не волнуют OrderLines или Customer детали, не такие легкие, как хотелось бы. Или я просто сохраняю идентификаторы элементов и добавляю функцию для их получения?

class Order : Entity {
    int CustomerID { get; set; }
    List<OrderLine> GetOrderLines() {};
}

class OrderLine : Entity {
    int OrderID { get; set; }
}

И как бы вы структурировали репозиторий для чего-то подобного?

Могу ли я использовать абстрактный репозиторий CRUD с методами GetByID(int), Save(entity), Delete(entity), которые наследуются каждым репозиторием элементов, а также добавлять свои собственные конкретные методы, что-то вроде этого?

public abstract class RepositoryBase<T, TID> : IRepository<T, TID> where T : AEntity<TID>
{
    private static List<T> Entities { get; set; }

    public RepositoryBase()
    {
        Entities = new List<T>();
    }

    public T GetByID(TID id)
    {
        return Entities.Where(x => x.Id.Equals(id)).SingleOrDefault();
    }

    public T Save(T entity)
    {
        Entities.RemoveAll(x => x.Id.Equals(entity.Id));
        Entities.Add(entity);
        return entity;
    }

    public T Delete(T entity)
    {
        Entities.RemoveAll(x => x.Id.Equals(entity.Id));
        return entity;
    }
}

Какая здесь «лучшая практика»?


person CaffGeek    schedule 05.10.2010    source источник


Ответы (4)


Сущности

Начнем с сущности Order. Заказ - это автономный объект, не зависящий от «родительского» объекта. В доменно-ориентированном дизайне это называется совокупный корень; это корень всей совокупности заказов. Агрегат заказа состоит из корневой и нескольких дочерних сущностей, которые в данном случае являются OrderLine сущностями.

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

Customer также является совокупным корнем. Это не часть заказа, это связано только с заказом. Если заказ перестает существовать, покупатель - нет. И наоборот, если клиент перестанет существовать, вы захотите сохранить заказы для целей бухгалтерского учета. Поскольку Customer только родственник, вам нужно иметь только CustomerId в заказе.

class Order
{
  int OrderId { get; }

  int CustomerId { get; set; }

  IEnumerable<OrderLine> OrderLines { get; private set; }
}

Репозитории

OrderRepository отвечает за загрузку всего Order агрегата или его частей, в зависимости от требований. Он не несет ответственности за загрузку клиента. Если вам нужен заказчик, загрузите его из CustomerRepository, используя CustomerId из заказа.

class OrderRepository
{
  Order GetById(int orderId)
  {
    // implementation details
  }

  Order GetById(int orderId, OrderLoadOptions loadOptions)
  {
    // implementation details
  }
}

enum OrderLoadOptions
{
  All,
  ExcludeOrderLines,
  // other options
}

Если вам когда-нибудь понадобится загрузить строки заказа после этого, вы должны использовать принцип говори, не спрашивай. Сообщите заказу, чтобы загрузить его строки заказа, и какой репозиторий использовать. Затем заказ сообщит репозиторию информацию, которую он должен знать.

class Order
{
  int OrderId { get; }

  int CustomerId { get; set; }

  IEnumerable<OrderLine> OrderLines { get; private set; }

  void LoadOrderLines(IOrderRepository orderRepository)
  {
    // simplified implementation
    this.OrderLines = orderRepository.GetOrderLines(this.OrderId);
  }
}

Обратите внимание, что код использует IOrderRepository для извлечения строк заказа, а не отдельный репозиторий для строк заказа. Дизайн, управляемый доменом, гласит, что должен быть репозиторий для каждого совокупного корня. Методы получения дочерних сущностей принадлежат репозиторию корня и должны быть доступны только корню.

Рефераты / базовые репозитории

Я сам написал абстрактные репозитории с операциями CRUD, но обнаружил, что это не добавляет никакой ценности. Абстракция полезна, когда вы хотите передавать в коде экземпляры подклассов. Но какой код будет принимать любую BaseRepository реализацию в качестве параметра?

Кроме того, операции CRUD могут различаться для каждой сущности, что делает базовую реализацию бесполезной. Вы действительно хотите удалить заказ или просто установите его статус на удаленный? Если вы удалите клиента, что произойдет с соответствующими заказами?

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

person Niels van der Rest    schedule 18.10.2010

Я бы разделил свой проект на соответствующие части. Объекты передачи данных (DTO), объекты доступа к данным (DAO). Я хотел бы, чтобы DTO были как можно более простыми, здесь используются такие термины, как POJO (простой старый объект Java) и POCO (простой старый объект C), просто говоря, они являются объектами-контейнерами с очень небольшой встроенной функциональностью.

DTO по сути являются строительными блоками для всего приложения и объединяют слои. Для каждого объекта, моделируемого в системе, должен быть хотя бы один DTO. Как вы затем поместите их в коллекции, полностью зависит от дизайна приложения. Очевидно, что существуют естественные отношения «Один ко многим», например, «Клиент имеет много заказов». Но основы этих объектов таковы, каковы они есть. Например, заказ связан с клиентом, но также может быть автономным и поэтому должен быть отделен от объекта клиента. Все отношения «многие ко многим» должны быть преобразованы в отношения «один ко многим», что легко сделать при работе с вложенными классами.

Предположительно, в категории «Объекты доступа к данным» должны быть объекты CRUD. Здесь становится сложно, поскольку вам нужно управлять всеми взаимосвязями, которые были обнаружены в дизайне, и в моделях срока службы каждой из них. При получении DTO обратно из DAO параметры загрузки важны, поскольку это может означать разницу между вашей системой, работающей как собака из-за чрезмерной загрузки, или высоким сетевым трафиком из-за возврата данных и четвертой из вашего приложения и магазина из-за ленивой загрузки.

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

    class OrderDAO
    {
    public OrderDTO Create(IOrderDTO order)
    {
    //Code here that will create the actual order and store it, updating the 
flelds in the OrderDTO where necessary. One being the GUID field of the new ID. 
I stress guid as this means for better scalability.

    return OrderDTO
    }

}

Как видите, OrderDTO передается в метод Create.

Для метода Create при работе с новыми вложенными объектами должен быть некоторый код, связанный с объединением данных, которые были сохранены, например, клиента со старыми заказами и новым заказом. Системе придется иметь дело с тем фактом, что некоторые операции являются операторами обновления, а другие - операциями создания.

Однако одна часть головоломки, которую всегда упускают из виду, - это многопользовательские среды, в которых DTO (простые объекты) отключаются от приложения и возвращаются обратно в DAO для CRUD. Обычно это включает в себя некоторый контроль параллелизма, который может быть неприятным и может усложниться. Здесь работает простой механизм, такой как DateTime или номер версии, хотя при выполнении crud для вложенного объекта вы должны разработать правила о том, что обновляется и в каком порядке, а также, если обновление не работает с параллелизмом, вы должны решить, потерпите ли вы сбой. вся операция или частичная.

person WeNeedAnswers    schedule 22.10.2010
comment
Кстати, я бы избегал использования наследования при работе с Entities и репозиторием. Я бы попробовал вместо этого сделать инъекцию (инверсию управления) и композицию объектов с использованием интерфейсов. - person WeNeedAnswers; 22.10.2010
comment
DTO обычно являются «тупыми» объектами; они не содержат бизнес-логики. Куда бы вы поместили бизнес-логику? Я должен согласиться с тем, что управление параллелизмом сложно. Если это важный аспект приложения, архитектура CQRS вероятно более уместно. - person Niels van der Rest; 22.10.2010
comment
Зависит от того, что подразумевается под бизнес-логикой, похоже, меняется. Если вы имеете в виду бизнес-правила, они должны быть на уровне / репозитории DAO. Здесь я немного запутался. База данных кажется лучшим местом для размещения бизнес-правил, логики обновления и т. Д., Но если вы собираетесь использовать домены, то либо вы сохраняете домен и базу данных как единое целое, либо добавляете другой уровень, который имеет дело со всеми правилами. Например, заслуживает ли клиент кредит доверия, имеют ли они право на скидку, если они заказывают x, y, z и т. Д. Бизнес-правила отделены от объектов DTO, но являются частью уровня домена. - person WeNeedAnswers; 22.10.2010
comment
Слой домена - это не просто отдельный объект, а совокупность объектов, работающих в унисон. Слой домена также разделяется на подслои. Пока модель остается неизменной и находится в одном месте, я не вижу проблем с этим подходом, другие скажут, что объект Domain должен быть самодостаточным. Но как вы справляетесь с проблемами зависимости связанных объектов и применяемых к ним правил. Если вы перейдете на уровень бизнес-правил, вы можете решить эту проблему, добавив новое правило зависимости, которое при необходимости распространяется на другие правила. - person WeNeedAnswers; 22.10.2010

Почему бы не создать отдельные классы Order? Мне кажется, вы описываете базовый объект Order, который будет содержать базовую информацию о заказе и клиенте (или, возможно, даже не информацию о клиенте), и отдельный объект Order, в котором есть позиции.

Раньше я поступал так, как предлагал Нильс, и использовал логические флаги или перечисления для описания необязательной загрузки дочерних объектов, списков и т. Д. В Чистый код, дядя Боб говорит, что эти переменные и параметры функций являются оправданиями, которые программисты используют, чтобы не преобразовывать класс или функцию в меньшие, более простые переваривать кусочки.

Что касается дизайна вашего класса, я бы сказал, что это зависит от обстоятельств. Я предполагаю, что Заказ может существовать без каких-либо OrderLines, но не может существовать без клиента (или, по крайней мере, способа ссылки на клиента, как предложил Нильс). Если это так, почему бы не создать базовый класс Order и второй класс FullOrder. Только FullOrder может содержать список OrderLines. Следуя этой мысли, я бы создал отдельные репозитории для обработки операций CRUD для Order и FullOrder.

person jwheron    schedule 21.10.2010
comment
Что произойдет, если в Order есть два других свойства, которые вы можете или не хотите загружать, например, адрес для выставления счета и адрес доставки? Это приведет к 2 ^ 3 = 8 возможным сценариям загрузки. Будете ли вы создавать отдельный класс и репозиторий для каждой комбинации? См. этот вопрос о таких параметрах функции, чтобы узнать, почему это неплохая практика по определению. - person Niels van der Rest; 22.10.2010
comment
Я не предлагал и никогда не стал бы применять это во всех ситуациях. Я сказал, что это зависит от его общего дизайна класса. В этом конкретном сценарии строк заказов и заказов, я думаю, это имеет смысл. Однако я должен добавить, что я не согласен с вашим выводом относительно схем именования функций в ответе, который вы связали. Правильно названные функции информативны, но функции с параметрами, которые описывают некоторое внутреннее поведение, требуют больших затрат времени со стороны любого случайного программиста, поддерживающего проект после завершения. - person jwheron; 22.10.2010

Если вас интересует реализация доменно-ориентированного дизайна (DDD) с POCO вместе с пояснениями, обратите внимание на следующие 2 сообщения:

http://devtalk.dk/2009/06/09/Entity+Framework+40+Beta+1+POCO+ObjectSet+Repository+And+UnitOfWork.aspx

http://www.primaryobjects.com/CMS/Article122.aspx

Существует также проект, который реализует шаблоны, управляемые доменом (репозиторий, единица работы и т. Д.) Для различных фреймворков сохраняемости (NHibernate, Entity Frameworks и т. Д. И т. Д.) Под названием NCommon

person zam6ak    schedule 22.10.2010