Home Зачем нужны сервисы и как их готовить
Post
Cancel

Зачем нужны сервисы и как их готовить

Без сервисов

Принципы SOLID — это руководства, которые также могут применяться во время работы над существующим программным обеспечением для его улучшения, например, для удаления «дурно пахнущего кода».

В какой-то момент при написании проекта в коде начинает появляться повторяющийся код.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ClassA {
  func foo1() {
    // загрузка данных из бд
    // изменение объекта в соотвествии с бизнес логикой
    // сохранение изменений в бд
    // вызов rest сервера
  }

  func foo1() {
    // загрузка данных из бд
    // создание записи в бд
    // сохранение изменений в бд
    // вызов rest сервера
  }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ClassA {

  func saveToDB() {
    // сохранение изменений в бд
  }

  func getFromDB() {
    // загрузка данных из бд
  }

  func makeChange() {
    // изменение объекта в соотвествии с бизнес логикой
  }

  func callUrl() {
    // вызов rest сервера
  }

  func foo1() {
    getFromDB()
    makeChange()
    saveToDB()
    callUrl()
  }

  func foo1() {
    getFromDB()
    makeChange()
    saveToDB()
    callUrl()
  }
}

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

Попробуй обосновать какие принципы SOLID здесь нарушены

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

Помни, если тебе приходится размножать код в проекте методом копирования - нужно остановиться, задуматься и вспомнить принцип DRY - Dont Repeat Yourself

Мы помним что копирование плохо. Но что мы можем сделать. Самое простое - взять и сделать один класс, куда помещать все наши очень полезные функции. Отлично, теперь нам не нужно копировать код, все кому нужен функционал - может обратиться к нашему классу, назовем его MegaUtil. Хммм… не покидает чувство, что здесь что-то не так ?

Сможешь сказать почему копирование кода это плохо ?

Да, в общем то, мы проблему из одного места перенесли в другое - наш новый класс так же делает все что только можно - с базой взаимодействует, реализует бизнес логику, осуществляет rest вызовы.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MegaUtil {

  func saveToDB() {
    // сохранение изменений в бд
  }

  func getFromDB() {
    // загрузка данных из бд
  }

  func makeChange() {
    // изменение объекта в соотвествии с бизнес логикой
  }

  func callUrl() {
    // вызов rest сервера
  }
}

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

  1. Работа с БД
  2. Бизнес логика
  3. Взаимодествие с сервером

И получается, чтобы не нарушать Single Responsibility - нам достаточно вместо одного класса создать 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class DatabaseService {
  func saveToDB() {
    // сохранение изменений в бд
  }
  func getFromDB() {
    // загрузка данных из бд
  }
}

class BusinessLogicService {
  func makeChange() {
    // изменение объекта в соотвествии с бизнес логикой
  }
}

class NetworkService {
  func callUrl() {
    // вызов rest сервера
  }
}

Как видно мы используем постфикс Service в конце названия классов. Это потому, что они уже более стали походить на полноценные сервисы. Но чего еще не хватает ?

Использование протоколов

Работая с проектом, возможно, произойдет ситуация когда нам прийдется поменять базу данных, напирмер с Ralm перейти на Core Data. Да, такое очень маловероятно. Но можно придумать и другие примеры, например, вместо сохранения картинки на сервере - необходимо будет сохранять ее на устройстве.

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

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

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

1
2
3
4
protocol DatabaseService {
  func saveToDB(someArg: String)
  func getFromDB() -> Image
}

Как видим все довольно просто. Нужно придумать название контракту, которому будут соответствовать описываемые сервисы

  • DatabaseService. Перечислить методы, которые должны в нем быть. Готово! Теперь мы можем создавая сервис, указывать,
  • что он реализует протокол.
1
2
3
4
5
6
7
8
protocol RealmDatabaseService: DatabaseService {
  func saveToDB(someArg: String) {
    // код для сохранения в Realm базу данных
  }
  func getFromDB() -> Image {
    // код для получения картинки из Realm базы данных
  }
}

Далее можем сделать другую реализацию для Core Data:

1
2
3
4
5
6
7
8
protocol CoreDataDatabaseService: DatabaseService {
  func saveToDB(someArg: String) {
    // код для сохранения в CoreData базу данных
  }
  func getFromDB() -> Image {
    // код для получения картинки из CoreData базы данных
  }
}

Самое интересно, что мы можем сделать сервис-заглушку, которая никуда не будет обращаться и возвращать заранее определенные значения. Это может быть полезно для тестов. Так же, при использовании tuist, когда разработка ведется над какими-то модулями, например, UI, где не требуется реально подключение к базе данных, а нужна какая-та заглушка, чтобы компонент функционировал и можно было бы спокойно его разрабатывать с точки зрения верстки. Обычно такие сервисы называются Mock:

1
2
3
4
5
6
7
8
9
protocol MockDatabaseService: DatabaseService {
  func saveToDB(someArg: String) {
    // здесь ничего не сохраняется. Просто позволяем вызвать этот пустой метод.
  }
  func getFromDB() -> Image {
    // здесь всегда возвращаемся заранее подготовленную картинку.
    return Image(named: "dummy")
  }
}

Что дальше

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

Процесс связывания сервисов между собой называется Dependency Injection. Принцип работы связывания довольно прост - мы регистрируем свою реализацию сервиса. А другие сервисы, зная только протокол, обращаются к регистратору за этим сервисом. Этот сервис им предоставляется, но они не знаю конкретной реализации, знают только что этот сервис точно реализует нужные им функции.

Эта тема будет раскрыта в следующих статьях.

This post is licensed under CC BY 4.0 by the author.
Trending Tags