21 июня 2019

Архитектурный шаблон «Строитель» во вселенной «Swift» и «iOS»/«macOS»

ПрограммированиеРазработка под iOSООПSwiftРазработка под MacOS

В этот раз я бы хотел немного поговорить о еще одном порождающем шаблоне проектирования из арсенала «Банды четырех» – «Строителе» («Builder»). Так вышло, что в ходе получения своего (пусть и не слишком обширного) опыта, я довольно часто видел, чтобы паттерн использовался в «Java»-коде вообще и в «Android»-приложениях в частности. В «iOS» же проектах, будь они написаны на «Swift» или «Objective-C», шаблон встречался мне довольно редко. Тем не менее, при всей своей простоте, в подходящих случаях он может оказаться довольно удобным и, как модно говорить, мощным.


image


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


image


Пример из «Foundation»


В случаях, когда нужный «URL» не зафиксирован, а конструируется из составляющих (например, адреса хоста и относительного пути до ресурса), вы наверняка пользовались удобным механизмом URLComponents из библиотеки «Foundation».


URLComponents – это, по большей части, просто класс, объединяющий множество переменных, которые хранят значения тех или иных компонентов «URL», а также свойство url, которое возвращает соответствующий текущему набору компонентов «URL». Например:


var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.user = "admin"
urlComponents.password = "qwerty"
urlComponents.host = "somehost.com"
urlComponents.port = 80
urlComponents.path = "/some/path"
urlComponents.queryItems = [URLQueryItem(name: "page", value: "0")]

_ = urlComponents.url
// https://admin:qwerty@somehost.com:80/some/path?page=0

По сути, приведенный выше пример использования – это реализация паттерна «Строитель». URLComponents в этом случае выступает в роли собственно строителя, присвоение различным его свойствам (scheme, host и пр.) значений – это инициализация будущего объекта по шагам, а вызов свойства url – это подобие финализируещего метода.


В комментариях развернулись жаркие баталии о «RFC»-документах, описывающих «URL» и «URI», поэтому, чтобы быть более точным, предлагаю для примера считать, что мы говорим только об «URL» удаленных ресурсов, и не принимаем во внимание такие «URL»-схемы, как, скажем, «file».


Вроде бы все хорошо, если пользоваться таким кодом нечасто и знать все его тонкости. Но что, если что-нибудь забыть? Например, что-нибудь важное, как адрес хоста? Как вы думаете, что будет результатом выполнения следующего кода?


var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.path = "/some/path"
_ = urlComponents.url

Мы работаем со свойствами, а не методами, и никаких ошибок «выброшено» точно не будет. «Финализирующее» свойство url возвращает опциональное значение, так может быть, мы получим nil? Нет, мы получим вполне полноценный объект типа URL с бессмысленным значением – «https:/some/path». Поэтому мне пришло в голову поупражняться написанием собственного «строителя» на основе описанного выше «API».


(Здесь должен был быть «эмодзи» «велосипед», но «Хабр» его не отображает)


Не смотря на сказанное выше, я считаю URLComponents хорошим и удобным «API» для сборки «URL» из составных частей и, наоборот, «парсинга» составных элементов известного «URL». Поэтому на его основе мы сейчас и напишем собственный тип, собирающий «URL» из частей и обладающий (предположим) нужным нам в данный момент «API».


Во-первых, хочется избавиться от разрозненной инициализации путем присваивания новых значений всем нужным свойствам. Вместо этого реализуем возможность создания экземпляра строителя и присвоение значений всем свойствам с помощью методов, вызываемых по цепочке. Цепочка заканчивается финализирующим методом, результатом вызова которого и будет соответствующий экземпляр URL. Возможно, вы встречали на своем жизненном пути что-нибудь вроде StringBuilder у «Java» – примерно к такому «API» мы сейчас и будем стремиться.


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


final class URLBuilder { }

Объявим методы, задающие параметры будущего «URL», с учетом перечисленных выше требований:


final class URLBuilder {

    private var scheme = "https"
    private var user: String?
    private var password: String?
    private var host: String?
    private var port: Int?
    private var path = ""
    private var queryItems: [String : String]?

    func with(scheme: String) -> URLBuilder {
        self.scheme = scheme
        return self
    }

    func with(user: String) -> URLBuilder {
        self.user = user
        return self
    }

    func with(password: String) -> URLBuilder {
        self.password = password
        return self
    }

    func with(host: String) -> URLBuilder {
        self.host = host
        return self
    }

    func with(port: Int) -> URLBuilder {
        self.port = port
        return self
    }

    func with(path: String) -> URLBuilder {
        self.path = path
        return self
    }

    func with(queryItems: [String : String]) -> URLBuilder {
        self.queryItems = queryItems
        return self
    }

}

Заданные параметры мы сохраняем в частных свойствах класса для дальнейшего использования финализируюим методом.


Еще одна дань «API», на котором мы основываем наш класс – это свойство path, которое, в отличие от всех соседних свойств, не является опциональным, а в случае отсутствия относительного пути хранит в качестве своего значения пустую строку.


Чтобы написать этот, собственно, финализирующий метод необходимо подумать еще о нескольких вещах. Во-первых, «URL» обладает некоторыми частями, без которых он, как было обозначено в начале, перестает иметь смысл – это scheme и host. Первого мы «наградили» значением по умолчанию, поэтому забыв о нем, мы все равно получим, скорее всего, ожидаемый результат.


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


Еще один интересный момент связан со свойствами user и password: они имеют смысл только в том случае, если используются одновременно. Но что, если программист забудет присвоить одно из этих двух значений?


И, наверное, последнее, что необходимо учесть – это то, что результатом финализирующего метода мы хотим иметь значение свойства url URLComponents, а оно, в данном случае, не очень кстати является опциональным. Хотя при любом сочетании заданных значений свойств nil мы не получим. (Значение будет отсутствовать только у пустого, только что созданного, экземпляра URLComponents.) Чтобы преодолеть это обстоятельство, можно использовать ! – оператор «forced unwrapping». Но вообще-то не хотелось бы поощрять его использование, поэтому в нашем примере мы немного абстрагируемся от знаний тонкостей «Foundation» и будем считать обсуждаемую ситуацию системной ошибкой, возникновение которой не зависит от нашего кода.


Итак:


extension URLBuilder {

    func build() throws -> URL {
        guard let host = host else {
            throw URLBuilderError.emptyHost
        }

        if user != nil {
            guard password != nil else {
                throw URLBuilderError.inconsistentCredentials
            }
        }
        if password != nil {
            guard user != nil else {
                throw URLBuilderError.inconsistentCredentials
            }
        }

        var urlComponents = URLComponents()
        urlComponents.scheme = scheme
        urlComponents.user = user
        urlComponents.password = password
        urlComponents.host = host
        urlComponents.port = port
        urlComponents.path = path
        urlComponents.queryItems = queryItems?.map {
            URLQueryItem(name: $0, value: $1)
        }

        guard let url = urlComponents.url else {
            throw URLBuilderError.systemError // Impossible?
        }

        return url
    }

    enum URLBuilderError: Error {
        case emptyHost
        case inconsistentCredentials
        case systemError
    }

}

Вот, пожалуй, и все! Теперь покомпонентное создание «URL» из примера в начале может выглядеть так:


_ = try URLBuilder()
    .with(user: "admin")
    .with(password: "Qwerty")
    .with(host: "somehost.com")
    .with(port: 80)
    .with(path: "/some/path")
    .with(queryItems: ["page": "0"])
    .build()

// https://admin:Qwerty@somehost.com:80/some/path?page=0

Разумеется, использование try вне блока do-catch или без оператора ? при возникновении ошибки заставит программу завершиться аварийно. Но мы предоставили «клиенту» возможность обрабатывать ошибки так, как он сочтет необходимым.


Да, и еще одна полезная особенность пошагового конструирования с помощью этого шаблона – возможность помещать шаги в разных частях кода. Не самый частый «кейс», но тем не менее. Спасибо akryukov за напоминание!


Заключение


Шаблон экстремально прост для понимания, а все простое – как известно, гениально. Или наоборот? Ну, неважно. Главное, что я, не покривив душой могу сказать, что он (шаблон), уже случалось, выручал меня в решении задач по созданию больших и сложных процессов инициализации. Например, процесс подготовки сессии связи с сервером в библиотеке, которую я писал для одного сервиса почти два года назад. Кстати, код – «open source» и, при желании, с ним вполне можно ознакомиться. (Хотя, конечно, с тех пор много воды утекло, и к этому коду прикладывались и другие программисты.)


Другие мои посты о шаблонах проектирования:



А это мой «Twitter», чтобы удовлетворить гипотетический интерес к моей публично-профессиональной активности.

Теги:программированиеоопшаблоны проектированияпаттерны проектированияswiftios developmentios разработкаmacosmacos разработка
Хабы: Программирование Разработка под iOS ООП Swift Разработка под MacOS
+7
3,5k 38
Комментарии 14
Лучшие публикации за сутки