Шаблон проектирования линзы #
Васильев Андрей Михайлович, 2024
Версии презентации
Работа со сложными структурами данных #
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
Рассмотрим следующие структуры данных
data class AcademicClass(val id: Int, val name: String,
val teacher: String)
data class Course(val id: Int, val name: String,
val clasess: List<AcademicClass>)
data class Speciality(val id: Int, val name: String,
val courses: List<Course>)
val speciality: Speciality = ...
Модификация структур данных #
Для получения информации по предмету необходимо найти курс в списке, найти предмет, обратиться к полям класса для получения:
val class = speciality.courses[1].classes[5]
Для редактирования потребуется выполнить несколько копирований:
val newAcademicClass = speciality.courses[1].classes[5]
.copy(name = "Безопасность жизнедеятельности")
val newAcademicClasses = speciality.courses[1].classes.toMutableList()
.apply { set(5, newAcademicClass) }
val newCourse = speciality.courses[1].copy(classes = newAcademicClasses)
val newCourses = speciality.courses.toMutableList()
.apply { set(1, newCourse) }
val newSpeciality = speciality.copy(courses = newCourses)
Можно создать новую версию неизменяемых данных с нужным состоянием, остаётся только сохранить новую копию в общем хранилище приложения
Сложные структуры сложны #
Сильная вложенность структур данных несёт следующие проблемы:
- Надо корректно указать путь к данным несколько раз
- Необходимо написать много кода
- Возможны технические ошибки
- Код, которому нужен доступ только к конечным данным, начинает зависеть от всех промежуточных объектов, что усложняет изменение структур данных в приложении
Решение с помощью функций #
Мы можем ввести функции, позволяющие решить данные задачи:
fun getName(speciality: Speciality, courseId: Int, classId: Int): String
fun setName(name: String, speciality: Speciality,
courseId: Int, classId: Int): Speciality
Код будет зависеть только от класса Speciality и одной из указанных функций
Шаблон проектирования линзы #
Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства
- Код действия зависит только от корневого объекта и линзы
- Скрывает сложности по изменению сложных структур данных
Пример линзы для сложной структуры данных #
class ClassNameLens(
private val courseId: Int,
private val classId: Int,
) {
fun getName(speciality: Speciality): String { ... }
fun setName(name: String, speciality: Speciality): Speciality { ... }
}
val speciality: Speciality = ...
val classNameLens = ClassNameLens(1, 5)
classNameLens.getName(speciality)
val newSpeciality = classNameLens.setName("Веб-разработка", speciality)
- Линзы предоставляют простой и понятный интерфейс
- Линзы предоставляют функции по чтению или функции чтения и записи
- Линзы могут выполнять преобразование данных при чтении или записи: типы данных внутри объекта могут быть, к примеру, строками, а интерфейс линзы будет предоставлять их в виде целых чисел
Отличие линзы и операции #
- Оба механизма предоставляют простой интерфейс для доступа к данным
- Оба механизма предоставляют слой абстракции от сложных интерфейсов
Ключевое отличие — линзы обычно создаются путём комбинирования простых операций, которые обычно предоставляет разработчик конкретной или обобщённой библиотеки
В рамках http4k используется шаблон проектирования строитель, который позволяет создать линзу для использования с конкретным типом
Шаблон проектирования строитель #
Для создания некоторых объектов пользователю необходимо указать множество аргументов конструктора, причём каждый из них тоже может быть сложным
Чтобы упросить создание конкретного компонента в строитель выделяют:
- логику по сбору всех аргументов для создания объекта
- метод для создания объекта целевого класса
При использовании строителя создание объекта выглядит следующим образом:
- Создать объект-строитель
- Вызвать несколько методов, формирующих набор данных для конструктора класса
- Вызвать метод по созданию целевого объекта
Строители в библиотеке http4k тоже являются неизменямыми, поэтому при вызове методов создаются новые строители с изменённым захваченным состоянием
Пример строителя на Kotlin #
class FoodOrder private constructor(
val bread: String = "Серый хлеб",
val condiments: String?,
val meat: String?,
) {
data class Builder(
val bread: String? = null,
val condiments: String? = null,
val meat: String? = null,)
fun bread(bread: String) = copy(bread = bread)
fun condiments(condiments: String) = copy(condiments = condiments)
fun meat(meat: String) = copy(meat = meat)
fun build() = FoodOrder(bread, condiments, meat)
}
}
- Конструктор класса FoodOrder невозможно вызвать снаружи
- Внутри метода
build()
можно проверить состояние строителя перед созданием
val foodOrder = FoodOrder.Builder().bread("Лаваш").meat("Сало")
.condiments("Подсолнечное масло").build()