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

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

Пример из «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» и, при желании, с ним вполне можно ознакомиться. (Хотя, конечно, с тех пор много воды утекло, и к этому коду прикладывались и другие программисты.)
Другие мои посты о шаблонах проектирования:
- «Фабричный метод» и «Абстрактная фабрика» во вселенной «Swift» и «iOS»,
- «Архитектурный шаблон «Итератор» («Iterator») во вселенной «Swift»,
- «Архитектурный шаблон «Посетитель» (“Visitor”) во вселенной «iOS» и «Swift».
А это мой «Twitter», чтобы удовлетворить гипотетический интерес к моей публично-профессиональной активности.
