Dmitriy Azarov

Скрытие логики внутри сервисов как архитектурный паттерн

Для начала рассмотрим общие архитектурные подходы. Всегда есть возможность реализовывать приложение и все необходимую логику как есть. Это и быстро и просто. В начале. В случае, если нужно делать какие-либо действия несколько раз или подключить тестирование, или АБ тесты - то весь код поростет мхом и копаться в нем будет довольно трудно. Для этого люди придумали различные артихектуры. В каждом сообществе есть свои любимые паттерны и архитектуры, но на деле их несколько, все остальные лишь ответвления или вид сбоку. Различные подходы это в большинстве случаев дело вкуса. На раннем этапе сложно однозначно выбрать подходящую архитектуру, потому что цель архитектуры и паттернов решать существующие проблемы. В начале разработки нет проблем и нечего решать. Enterprise мир смотрит в сторону N-tier, Onion architecture и DDD подхода. Клиентские части в большинстве своем используют популярные подходы в сообществах на текущий момент (VIPED,MVVM etc).

Рассмотрим слоеную модель. В модели N-TIER предполагается наличие N слоев, которые отвечают за работоспособность всего приложения. Наиболе распространенная 3-tier модель, которая состоит из Business Logic Layer - слоя, ответственного за бизнес логику. Data Access Layer, слой ответственный за доступ к данным и Presentation Layer - слой представления, то, как выглядит приложение. Каждый слой выделяется для инкапсуляции определенного типа логики и кода.

Business Logic Layer предназначен для реализации практически всей логики приложения. Это Роли пользователей, расчет скидок на товары, генерация отчетов, и др. BLL получает данные с базы данных, производит обработку пользовательских данных, пишет логи. Также сюда стоит отнести различные службы и демоны. Иногда этот слой называют компонентным слоем и он может состоять из набора конмпонентов.

Data Access Layer должен осуществлять доступ к данным. Этот слой должен инкапсулировать реальный источник и приемник данных, чтобы пользователь DAL не знал с какой базой / кешом работает. Для чего это нужно? Это дает много преимуществ, наиболее значимые:

  • Контроль доступа к данным
  • Возможность смены хранилища, добавления нового типа хранилища
  • Консолидация структур данных
  • Уменьшение связности с другими слоями

Presentation Layer отвечает за все взаимодействие с пользователем. Сюда относится навигация, первичная валидация пользовательских данных, интерфейс, различные типы приложений. Это может быть и мобильное приложение и мобильная версия сайта и сам сайт или устройство, все это относится к слою представления.

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

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

Для контроллеров на сайте это должен быть почти пустой Action с минимальным количеством кода. Проверки осуществляются модели. В идеальном случае это вызов некоторого сервиса и обработка его результата. Ниже приведен пример из реального проекта. Это Action формы бронирования и его улучшенный вариант. В данном случае слой представления не взаимодействует напрямую со слоем доступа данных. С точки зрения шаблона это условие выполнено. Но с точки зрения тестируемости и надежности данный код требует существенной доработки. По моему же принципу тут должен быть один вызов сервисного метода. Далее отдельным вызовом проиходит попытка бронирования слота пользователем (это и смс уведомление и отправка письма на почту и подсчет статистики и другие необходимые действия). Метод принимает модель и возвращает результат выполнения. Я продолжаю использовать подход IOperationResult для получения результатов произвольного действия. Исходный код доступен на гитхабе.

Оригинальный action

[Route("games/book")]
[HttpPost]
public ActionResult Book(string Phone, string Name, string Email, string Comment,
	string Team, string Agree, string Source, int Id = 0)
{
	QRGameSourceTypes source = QRGameSourceTypes.Site;
	int? subTypeId = null;
	if (Source != null && Source.Contains("admin"))
		source = QRGameSourceTypes.AdminPanel;

	Guid? cookieName = null;
	var cookie = OxoCookies.RequestCookies[SiteController.CookieName];
	if (cookie != null)
		cookieName = Guid.Parse(cookie.ToString());

	var result = BookingService.BookingGame(new BookGameTM
	{
		Id = Id,
		Name = Name,
		Email = Email,
		Phone = Phone,
		Comment = Comment,
		Team = Team,
		Source = source,
		AgreeToEmail = Agree == "true",
		Language = Language,
		UserAgent = OxoRequest.UserAgent,
		Referrer = OxoRequest.UrlReferrer,
		CookieId = cookieName,
		SubSourceId = subTypeId
	});

	if (!result.Success)
		return JsonContent(new MessageResponse(Translate("GamesController", result.Messages[0]), true));

	if (source == QRGameSourceTypes.AdminPanel)
		return Redirect(OxoRequest.UrlReferrerUnsafe ??  Url.Action("Index", "Bookings", new { Area = "Admin" }));


	HtmlResponse response = new HtmlResponse();

	var city = result.Object.City;
	var game = result.Object.Book;
	var gameModel = result.Object.GameModel;
	var booking = result.Object.Booking;

	var socials = SocialIconsFactory.GetWithCity(city);
	string tempalte = Translate("Book", "ApproveOrderTemplate", booking.Name, city.Name, gameModel.Address, socials);
	if (city.Id == 1)
		tempalte = Config.GetSpbGameConfirmSiteText(booking, city);

	response.Html = RenderViewToString("Ajax/ApproveOrder", tempalte);
	return JsonContent(response);
}

Измененный action

[HttpPost]
[Route("book")]
public IActionResult BookGame(GamesControllerBookGameRequest model)
{
    var result = _gamesMiddleware.Book(model);

    if (!result.Success)
    {
        return JsonContent(new
        {
            error = true,
            sad = true,
            message = "К сожалению этот сеанс только что забронировал другой пользователь."
        });
    }

    return View("Ajax/SuccessBooking", viewModel);
}

Этот принцип следует применять во всех конечных точках взаимодействия как с пользователем, так и с разработчиком. В службах и демонах и кронах должен быть вызов сервисного метода в одну строчку. Ниже приведен Job, исходный код доступен также на github.

public class UpdatePriceTimetableJob : JobBase
{
    private readonly PriceUpdateService _service;

    public UpdatePriceTimetableJob(PriceUpdateService service)
    {
        _service = service;

        Schedule = new JobSchedule
        {
            PeriodicTime = TimeSpan.FromMinutes(10),
            PeriodicType = PeriodicType.Timespan,
            StartTimeUtc = new DateTime(2016, 12, 04, 0, 5, 0)
        };
    }

    public override string Name => "Обновление цен";

    protected override void OnStart()
    {
        _service.UpdateTimetables();
    }
}

Какие преимущества дает вынос логики в сервисы

  • Возможность тестирования цельного процесса. Если атомарные функции или меньшие части цельного процесса могут быть протестированы, то целое бронирование обычно трудно тестировать. Когда для бронирования существует целый метод, который внутри инкапсулирует и доступ к базе и отправку смс - такой метод тоже можно тестировать. В большинстве случаев необходимо будет использовать Mock объекты для тестирования обработки запросов. Но и можно также автоматизировать интеграционное тестирование.
  • Возможность переиспользования целой службы или процесса. Имея метод бронирования в сервисном слое мы можем использовать его же для бронирования из Api.
  • Легкий ввод в проект новых разработчиков, которые могут понять логику происходящего по вызову методов. Если в коде контроллера 300 строк, то нужно прочитать их все, чтобы понять, что происходит
  • 25 апр 2017
  • архитектура, паттерны
0 комментариев
Ваш комментарий
адрес не будет опубликован
Текст