Шаблон проектирования линзы

Шаблон проектирования линзы #

Васильев Андрей Михайлович, 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 = ...

diagram


Модификация структур данных #

Для получения информации по предмету необходимо найти курс в списке, найти предмет, обратиться к полям класса для получения:

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 и одной из указанных функций


Шаблон проектирования линзы #

Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства

diagram

  • Код действия зависит только от корневого объекта и линзы
  • Скрывает сложности по изменению сложных структур данных

Пример линзы для сложной структуры данных #

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 используется шаблон проектирования строитель, который позволяет создать линзу для использования с конкретным типом

diagram


Шаблон проектирования строитель #

Для создания некоторых объектов пользователю необходимо указать множество аргументов конструктора, причём каждый из них тоже может быть сложным

Чтобы упросить создание конкретного компонента в строитель выделяют:

  • логику по сбору всех аргументов для создания объекта
  • метод для создания объекта целевого класса

При использовании строителя создание объекта выглядит следующим образом:

  1. Создать объект-строитель
  2. Вызвать несколько методов, формирующих набор данных для конструктора класса
  3. Вызвать метод по созданию целевого объекта

Строители в библиотеке 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()

© A. M. Васильев, 2024, CC BY-SA 4.0, andrey@crafted.su