Без сервисов
Принципы 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 сервера
}
}
Перечисляя назначения этого класса я на самом деле перечисляю доменные области данного класса. Здесь можно выделить их три:
- Работа с БД
- Бизнес логика
- Взаимодествие с сервером
И получается, чтобы не нарушать 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. Принцип работы связывания довольно прост - мы регистрируем свою реализацию сервиса. А другие сервисы, зная только протокол, обращаются к регистратору за этим сервисом. Этот сервис им предоставляется, но они не знаю конкретной реализации, знают только что этот сервис точно реализует нужные им функции.
Эта тема будет раскрыта в следующих статьях.