Архитектура приложений

Onion-архитектура. Часть 1

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core

Последнее обновление: 31.10.2015

Термин "Onion Architecture" ("луковая" архитектура) был предложен Джеффри Палермо (Jeffrey Palermo) еще в 2008 году. Спустя годы данная концепция стала довольно популярной и является одной из наиболее применяемых типов архитектуры при построении приложения на ASP.NET.

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

Количество уровней может отличаться, но в центре всегда находится модель домена (Domain Model), то есть те классы моделей, которые используются в приложении и объекты которых хранятся в базе данных:

Onion-архитектура в ASP.NET

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

Внешний уровень представляет такие компоненты, которые очень часто изменяются. Обычно внешний уровень образуют пользовательский интерфейс, тесты, какие-то вспомогательные классы инфраструктуры приложения. К этому уровню также относятся конкретные реализации интерфейсов, объявленных на нижележащих уровнях. Например, реализация интерфейса репозитория, который объявлен на уровне Domain Services. Вообще все внутрение уровни, которые можно объединить в Application Core, определяют только интерфейсы, а конкретная реализация этих интерфейсов располагается на внешнем уровне.

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

Для более подробного рассмотрения данного типа архитектуры создадим обычный проект ASP.NET MVC 5, который будет называться OnionApp.

Пока это монолитное приложение, в котором весь код размещен в одном проекте. Теперь добавим в решение (не в проект) новую папку. Назовем ее Domain. Затем добавим в папку новый проект. В качестве типа проекта выберем тип Class Library, а в качестве его названия укажем OnionApp.Domain.Core:

Добавим в новый проект класс, представляющий книгу, который и будут представлять Domain Model:

namespace OnionApp.Domain.Core
{
    public class Book
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

Затем добавим в папку Domain новый проект также по типу Class Library, а в качестве его названия укажем OnionApp.Domain.Interfaces. Затем добавим в этот проект ссылку на вышеопределенный проект OnionApp.Domain.Core и также добавим новый интерфейса:

using System;
using System.Collections.Generic;
using OnionApp.Domain.Core;

namespace OnionApp.Domain.Interfaces
{
    public interface IBookRepository: IDisposable
    {
        IEnumerable<Book> GetBookList();
        Book GetBook(int id);
        void Create(Book item);
        void Update(Book item);
        void Delete(int id);
        void Save();
    }
}

Этот интерфейс и составляет уровень Domain Services и зависит от уровня Domain Model.

И на данный момент проекты выглядят так:

При создании архитектуры приложения надо понимать, что реальное количество уровней здесь весьма условно. В зависимости от масштаба задач уровней может быть и больше, и меньше. Однако важно понимать сам принцип, что в центре у нас модели домена, а все остальное зависит от них. Каждый внешний уровень может зависеть от внутреннего, но не наоборот.

На выше представленной схеме между внешним уровнем и уровнем Domain Services есть еще уровень API или интерфейсов бизнес-логики приложения - уровень Application Services. Этот уровень может включать интерфейсы вспомогательных классов. Например, покупка книги может представлять собой объект, который в зависимости от способа оплата (наличкой, через кредитную карту, через электронные деньги) может включать тот или иной функционал. И, возможно, было бы неплохо определить общий интерфейс покупки, а в зависимости от типа магазина использовать его конкретную реализацию. Поэтому добавим в решение новую папку, которую назовем Services. В эту папку добавим новый проект по типу Class Library, который назовем OnionApp.Services.Interfaces

И добавим в этот проект интерфейс IOrder:

using OnionApp.Domain.Core;
namespace OnionApp.Services.Interfaces
{
    public interface IOrder
    {
        void MakeOrder(Book book);
    }
}

Данный проект также имеет зависимость от классов проекта OnionApp.Domain.Core. А интерфейс IOrder, представляющий процесс покупки и оформления заказа, использует эти классы в методе MakeOrder(). Предполагается, что в метод передается объект купленной книги.

Теперь перейдем к созданию внешнего уровня, который и будет реализовывать данные интерфейсы. Для этого добавим в решение папку Infrastructure и затем в нее добавим новый проект по типу Class Library, который назовем OnionApp.Infrastructure.Data.

Данный проект будет реализовывать интерфейсы, объявленные на нижних уровнях, и связывать их с хранилищем данных. В качестве хранилища данных будет использоваться бд MS SQL Server, с которой мы будем взаимодействовать через Entity Framework. Поэтому добавим в этот проект через nuGet все пакеты Entity Framework. Также добавим в проект ссылки на проекты OnionApp.Domain.Core и OnionApp.Domain.Interfaces.

После этого добавим в проект новый класс OrderContext:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using OnionApp.Domain.Core;

namespace OnionApp.Infrastructure.Data
{
    public class OrderContext : DbContext
    {
        public DbSet<Book> Books { get; set; }
    }
}

Также добавим класс репозитория BookRepository:

using System;
using System.Collections.Generic;
using System.Linq;
using OnionApp.Domain.Core;
using OnionApp.Domain.Interfaces;
using System.Data.Entity;

namespace OnionApp.Infrastructure.Data
{
    public class BookRepository : IBookRepository
    {
        private OrderContext db;

        public BookRepository()
        {
            this.db = new OrderContext();
        }

        public IEnumerable<Book> GetBookList()
        {
            return db.Books.ToList();
        }

        public Book GetBook(int id)
        {
            return db.Books.Find(id);
        }

        public void Create(Book book)
        {
            db.Books.Add(book);
        }

        public void Update(Book book)
        {
            db.Entry(book).State = EntityState.Modified;
        }

        public void Delete(int id)
        {
            Book book = db.Books.Find(id);
            if (book != null)
                db.Books.Remove(book);
        }

        public void Save()
        {
            db.SaveChanges();
        }

        private bool disposed = false;

        public virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    db.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850