---
title: "Использование паттерна Service Handle в Haskell: руководство"
date: 2023-10-30
description: "Введение в паттерн Service/Handle и тестирование на Haskell. Описание и примеры применения паттерна Service/Handle для начинающих разработчиков"
author: "Олег Ромашин"
intro_image: "https://metalamp.ru/images/keyboard.png"
fulltext_image: "https://metalamp.ru/images/keyboard.png"
categories:
  - name: "Magazine"
    url: "https://metalamp.ru/magazine.md"
tags:
  - name: "education"
    url: "https://metalamp.ru/tags/education.md"
  - name: "web2"
    url: "https://metalamp.ru/tags/web2.md"
  - name: "haskell"
    url: "https://metalamp.ru/tags/haskell.md"
---

# Использование паттерна Service Handle в Haskell: руководство

![Использование паттерна Service Handle в Haskell: руководство](https://metalamp.ru/images/keyboard.png)

Меня зовут [Олег Ромашин](https://www.linkedin.com/in/oleg-romashin-6a0238216/), я занимаюсь разработкой на языке Haskell. Когда 1.5 года назад я начинал своё первое приложение на нём, у меня был скудный опыт написания учебных проектов на других языках. В итоге я делал как мог: проект получился негибким — приходилось переделывать весь код под небольшие изменения, было непонятно, как правильно выделить компоненты, к проекту нельзя было написать автоматизированные юнит-тесты, и я тратил много времени на то, что просто смотрел на дерево проекта и пытался придумать, как мне добиться хоть какой-то эстетики. В этой статье я постараюсь доступным языком рассказать про паттерн Service/Handle и построение архитектуры тем, кто за неимением опыта столкнулся с такими же проблемами.  
  
Статья написана для тех, кто только начинает погружаться в разработку. Некоторые сокращения кода не сделаны намеренно, чтобы облегчить понимание тем, у кого ещё не натренирован глаз. Кроме этого, в статье не упомянуты некоторые приёмы работы с паттерном, сначала рекомендуется разобраться с причинами использования паттерна и механизмом. В конце статьи предложены материалы для дальнейшего изучения.  
  
Небольшая ремарка: сложилось так, что в сообществе Haskell у паттерна, о котором мы будем говорить, есть несколько названий: The Service Pattern, The Handle Pattern, The Service Handle pattern, The Service/Handle pattern, но обычно все имеют в виду одно и то же. В статье будем пользоваться последним вариантом.

 
## Как Service/Handle помогает в разработке

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

 ![](https://metalamp.ru/images/6125ee3f6b22949dcc1907de_Screenshot 2021-08-25 at 13.33.png)

 У таких частей проекта есть интерфейс, через который мы к ним обращаемся: типы, классы, функции. Обычно через имена функций (типы и классы нас сейчас не интересуют) мы напрямую обращаемся к реализации — к тому, что по факту делает функция. Например, пакет http-conduit в интерфейсе экспортирует функцию httpBS, которая выполняет HTTP-запрос и возвращает ответ в виде строки байтов. В таком виде интерфейс и реализация тесно связаны: вызываешь функцию — используешь реализацию. В некотором роде это усложняет написание и поддерживаемость проекта и сковывает движения.

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

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

 В-третьих, усложняется написание тестов логики. Чтобы протестировать, что, например, пользователю будет отправлено SMS-сообщение при определённых условиях, придётся фактически его отправить, так как функция напрямую использует все зависимости.

 Паттерн Service/Handle позволяет создать дополнительный промежуточный интерфейс для отдельных частей проекта.

 ![](https://metalamp.ru/images/6125f502d7c6258381d41758_2.png)

 Приложение разрабатывается с использованием интерфейса вместо прямого вызова функций внутри библиотек. Связь с библиотеками указывается один раз при заполнении нового интерфейса Service/Handle, а дальше этот интерфейс передаётся в другие функции. Таким образом мы легко и обособленно можем вносить любые изменения, связанные с зависимостями, меняя код только в одном месте. Из схемы выше видно, что интерфейс можно добавить к каждой части проекта, причём у каждой такой части он будет свой.

 Имея выделенный промежуточный интерфейс, мы получаем возможность подменять реализацию.

 
## Как Service/Handle позволяет легко менять реализацию и разрабатывать проект независимо от неё

 Рассмотрим на примере логгера. В случае с данным паттерном интерфейс — это рекорды конструктора данных.

 ![](https://metalamp.ru/images/6125f546d95e6104529de981_3.png)

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

 ![](https://metalamp.ru/images/6125f55cce0079493f2259ce_4.png)

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

 ![](https://metalamp.ru/images/6125f580403b2c232867b0e3_5.png)

 Теперь давайте посмотрим, как можно абстрагироваться от IO, чтобы писать чистые тесты для функций с логированием:

 ![](https://metalamp.ru/images/6125f5947b7fb0ff17b6dda8_6.png)

 Теперь из тестов можно запускать функцию doSomeWork, подставив реализацию, которая не будет обращаться в IO:

 ![](https://metalamp.ru/images/6125f5a83288ec8d85edec00_7.png)

 Ранее мы говорили, что интерфейс можно добавить как к библиотеке/сервису, так и к простым функциям, однако к функции doSomeWork интерфейс добавлять нет необходимости, поскольку она уже не имеет прямых зависимостей. Добавлять интерфейс к отдельным функциям имеет смысл, когда не для всех зависимостей определён промежуточный интерфейс. Например, когда для логирования выделен Service/Handle, а для обращения к базе данных нет.

 
## Избавляемся от зависимостей в логике регистрации пользователя

 Разберёмся с примером посложнее: представим, что у нас есть свой сервис, в котором нужна регистрация. Вот так в упрощённой форме мог бы выглядеть обработчик этой функции без использования паттерна (предположим, что функции для работы с базой данных и типы уже есть):

 ![](https://metalamp.ru/images/6125f5ca7cab074763deba64_8.png)

 Давайте перепишем эту функцию с использованием паттерна Service/Handle. Наша цель — вынести все IO функции в отдельный интерфейс, чтобы логику можно было тестировать локально. Необходимо протестировать, что пользователь будет зарегистрирован только в случае, если логин и email не заняты. Интерфейс с зависимостями в нашем случае — это логирование, функция записи пользователя в базу данных и функции поиска пользователя в базе данных. Кроме этого, используем логгер, который мы переписали с использованием паттерна Service/Handle.

 ![](https://metalamp.ru/images/612626e2fab44b5199ad1f67_9.png)

 ![](https://metalamp.ru/images/6125f61e3c9ac18290d777c6_10.png)

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

 ![](https://metalamp.ru/images/6125f97444f54bcda92b9abe_11.png)

 Также изменилось дерево проекта: раньше был один модуль App.Registration, в котором была функция регистрации. Теперь в этом модуле мы заполняем Handle, а логика находится в новой папке Handlers. Там создаётся модуль с идентичным именем App.Handlers.Registration, который вызывается после заполнения интерфейса.

 ![](https://metalamp.ru/images/6125f9898ec6e9393fe308f4_12.png)

 Кстати, паттерн очень удобно использовать с расширением RecordWildCards. Оно позволяет не передавать handle явно как аргумент в каждую функцию интерфейса. С ним функция registerUser будет выглядеть так:

 ![](https://metalamp.ru/images/6125f99d942ce19136951881_13.png)

 
## Тестирование функций, написанных в стиле паттерна Service/Handle

 Теперь в функцию registerUser можно подставлять другую реализацию, а это значит, её можно локально тестировать. Воспользуемся пакетом **hspec**. Как вы помните, у типов хэндлов есть параметр, который указывает, в какой монаде мы работаем. Раньше мы заполняли поля хэндла IO-функциями, поэтому при передаче заполненного хэндла в логику функция registerUser параметризировалась монадой IO. Теперь, чтобы написать чистые тесты, необходимо избавиться от IO, но все ещë нужна монада: отличное применение Identity. Раньше мы заполняли поля хэндла функциями, которые обращаются в IO: если функция делала запись в базу данных, то результатом было IO (), если функция доставала значение, то результатом было IO Value. Вместо IO будет Identity. Кроме этого, если базы данных нет, то и записывать нечего, поэтому функции записи данных будут игнорировать свой аргумент и возвращать Identity (). Функции, которые возвращают значения из базы данных, будут редактироваться под каждый тест. Если мы проверяем поведение, когда пользователь с указанным логином уже есть, то функция findUserByLogin будет возвращать Just User, когда логин свободен, функция будет возвращать Nothing. Такие данные, которые создаются специально для тестов, называют стабами (stub).

 ![](https://metalamp.ru/images/6125fa143288ecc328ee0f8e_Group 3.png)

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

 
## Резюмируя вышесказанное

 Паттерн Service/Handle позволяет строить логику из интерфейса, абстрагируясь от реализации. Это даёт возможность различным частям нашего проекта не беспокоиться о конкретных механизмах реализации. Благодаря этому можно с гораздо меньшими усилиями переходить на другие библиотеки или использовать разные реализации в разных частях проекта. Кроме этого, появляется возможность писать чистые тесты логики.  
‍  
Более наглядно код из статьи и тесты можно глянуть в репозитории. Кроме этого, там можно найти упражнение, чтобы попрактиковаться с заменой реализации:  
‍[https://github.com/olegromashin/service-handle-article](https://github.com/olegromashin/service-handle-article)

 Материалы для дальнейшего изучения:  
[jaspervdj - Haskell Design Patterns: The Handle Pattern](https://jaspervdj.be/posts/2018-03-08-handle-pattern.html)  
[The Service Pattern - School of Haskell](https://www.schoolofhaskell.com/user/meiersi/the-service-pattern)  
[Exceptions Best Practices in Haskell](https://www.fpcomplete.com/blog/2016/11/exceptions-best-practices-haskell/)

 ![article-logo](https://metalamp.ru/images/article/logo.svg) 
## Другие наши статьи


## Custom Fields

**reading time:** 8

**Article type:** articles

**Article description:** Описание и примеры применения паттерна Service/Handle для начинающих разработчиков.

**Author (copy):** Олег Ромашин

