Прокачайте свой Swift с @dynamicMemberLookup

от автора

Swift — это мощный язык программирования, который сочетает в себе безопасность типов и выразительность. Однако, несмотря на свою строгую типизацию, язык предоставляет разработчикам возможность использовать динамический доступ к свойствам объекта с помощью атрибута dynamicMemberLookup. Это может быть полезно, например, для работы с динамическими данными или при создании DSL (Domain-Specific Language). С помощью этого атрибута мы можем обращаться к свойствам экземпляра типа, даже если эти свойства явно в нем не определены.

При работе с этим атрибутом важно понимать, что он применим только к типам (struct, enum, class, actor, protocol), поэтому, например, данный код вызовет ошибку компиляции:

class MyClass { } @dynamicMemberLookup extension MyClass { } // '@dynamicMemberLookup' attribute cannot be applied to this declaration

Для использования dynamicMemberLookup необходимо выполнить всего две вещи:

  1. Отметить тип соотвествующим атрибутом (@dynamicMemberLookup)

  2. Реализовать subscript, через который мы будем получать интересующие нас данные

Упрощение работы с динамическими данными

Атрибут dynamicMemberLookup хорошо применим при работе с динамическими структурами данных, то есть такими, чье внутреннее строение формируется по какому-либо правилу, но количество элементов, их взаиморасположение и взаимосвязи могут динамически изменяться во время выполнения программы (например Dictionary). Использование dynamicMemberLookup позволяет обращаться к свойствам объекта, как если бы они были статически определены. Это позволяет сделать код более читаемым и удобным.

Рассмотрим применение атрибута через вот такой базовый пример интерпретации словаря в JSON структуру с возможностью использовать точечную нотацию для получения значения по ключу:

@dynamicMemberLookup struct JSON {      // Внутренний словарь для хранения ключей и значений     private var data: [String: Any]          init(from data: [String : Any]) {         self.data = data     }      // Необходимый для использования атрибута сабскрипт     subscript(dynamicMember member: String) -> Any? {         data[member]     } }

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

let json = JSON(from: ["name": "Malil", "age": 21]) print(json.name) // "Malil" print(json.age)  // 21
Скрытый текст

В этом случае будет отсутствовать какое-либо автодополнение кода, поскольку свойства name и age не определены для объекта и извлекаются динамически. Поэтому при запросе ключа можно допустить ошибку в именовании.

По большей части, это все является синтаксическим сахаром. В subscript мы определили аргумент типа String, по которому достаем из словаря data значение и возвращаем его. Компилятор просто дает нам возможность более красиво извлекать данные, поэтому эти две записи будут эквивалентны по результату:

json[dynamicMember: "name"] // "Malil" json.name // "Malil"

Гибкость и расширяемость API

С помощью dynamicMemberLookup у нас есть возможность легко добавлять новые свойства или изменять существующие без необходимости вносить изменения в интерфейс наших типов. Это позволяет создавать более гибкие и расширяемые API.

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

// Структура с параметрами конфигурации сервиса struct ServiceConfiguration {     var maxResuls: Int }  // Класс сервиса class ServiceImpl {          var configuration: ServiceConfiguration      init(configuration: ServiceConfiguration) {         self.configuration = configuration     } }

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

let service = ServiceImpl(configuration: ...) service.configuration.maxResuls = 30

На первый взгляд, всё выглядит замечательно. Однако, если углубиться в детали, становится очевидно, что вместо прямого обращения к сервису мы вынуждены использовать посредника — свойство configuration в цепочке вызовов. Было бы более удобно просто сказать сервису: «Теперь максимальное количество результатов, которое ты можешь вернуть, равно X».

Чтобы сделать API этого сервиса более интуитивным и удобным, мы воспользуемся атрибутом dynamicMemberLookup. Для безопасного доступа к интересующим нас свойствам объекта ServiceConfiguration мы применим WritableKeyPath, который позволит не только безопасно обращаться к свойствам, но и записывать в них значения (если вам интересно узнать больше о том, что такое KeyPath и как с ним работать, обязательно загляните в документацию). Итого получим следующее:

@dynamicMemberLookup  class ServiceImpl {          private var configuration: ServiceConfiguration          init(configuration: ServiceConfiguration) {         self.configuration = configuration     }        // Сабскрипт для чтения и изменения свойств `configuration` через `WritableKeyPath`     subscript<T>(dynamicMember keyPath: WritableKeyPath<ServiceConfiguration, T>) -> T {         get { configuration[keyPath: keyPath] }         set { configuration[keyPath: keyPath] = newValue }     } }

Теперь свойство configuration можно отметить как private , сделав вовсе недоступным для обращения. Вместо этого, мы можем напрямую обратиться к любому свойству ServiceConfiguration прямо из экземпляра сервиса, а благодаря использованию KeyPath у нас еще и сохраняется автодополнение кода, что делает его полностью безопасным:

let service = ServiceImpl(configuration: ...)  // Эта запись изменяет `maxResuls` у свойства `configuration` внутри `ServiceImpl` service.maxResuls = 30

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

Решение на самом деле очень простое. Мы уже знаем, что необходимо для реализации возможностей dynamicMemberLookup. Все, что нужно сделать в данном случае, — это отметить сам протокол этим атрибутом и добавить в его контракт нужный нам subscript. Таким образом, интерфейс сервиса и его реализация могут выглядеть следующим образом:

// Протокол сервиса @dynamicMemberLookup  protocol Service: AnyObject {     init(configuration: ServiceConfiguration)     subscript<T>(dynamicMember keyPath: WritableKeyPath<ServiceConfiguration, T>) -> T { get set } }  // Реализация сервиса class ServiceImpl: Service {          private var configuration: ServiceConfiguration          required init(configuration: ServiceConfiguration) {         self.configuration = configuration     }          subscript<T>(dynamicMember keyPath: WritableKeyPath<ServiceConfiguration, T>) -> T {         get { configuration[keyPath: keyPath] }         set { configuration[keyPath: keyPath] = newValue }     } }

В результате, даже если наша зависимость будет иметь тип протокола, мы все так же можем динамически обращаться к свойствам ServiceConfiguration из экземпляра сервиса, как и в предыдущем примере:

let service: Service = ServiceImpl(configuration: ...) service.maxResuls = 30

Заключение

Атрибут dynamicMemberLookup в Swift открывает интересные возможности для работы с типами, позволяя нам динамически извлекать свойства и создавать более выразительные API. Это упрощает чтение и понимание кода и делает его более элегантным. Тем не менее, как и с любой функциональностью, важно применять этот атрибут с умом, чтобы избежать ненужного усложнения наших типов.

Если вам интересно углубиться в детали, я рекомендую ознакомиться с предложением SE-0195, где вы найдете мотивацию и контекст, стоящие за добавлением этого атрибута в наш любимый язык.


ссылка на оригинал статьи https://habr.com/ru/articles/841690/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *