Давайте поговорим о делегировании, основанном на замыканиях, зацикливаниях и универсальных типах (generics).
Ок, текущая статья будет описывать делегирование и то, как мы можем сделать его лучше, используя язык Swift. Давайте сразу перейдём к типичному примеру делегирования в стиле Cocoa.
В первую очередь, мы напишем протокол делегата для класса ImageDownloaderDelegate.
protocol ImageDownloaderDelegate: class {
func imageDownloader(_ imageDownloader: ImageDownloader, didDownload image: UIImage)
}
Далее, мы реализуем класс ImageDownloader:
class ImageDownloader {
weak var delegate: ImageDownloaderDelegate?
func downloadImage(for url: URL) {
download(url: url) { image in
self.delegate?.imageDownloader(self, didDownload: image)
}
}
}
Заметьте, что делегат помечен словом weak для недопущения зацикливания. Если вы не знакомы с этой темой рекомендую вам ознакомиться с переводом подробной статьи автора NatashaTheRobot «iOS: How To Make Weak Delegates In Swift».
И теперь мы напишем пользователя для нашего класса ImageDownloader.
class Controller {
let downloader = ImageDownloader()
var image: UIImage?
init() {
downloader.delegate = self
}
func updateImage() {
downloader.downloadImage(for: /* some image url */)
}
}
extension Controller: ImageDownloaderDelegate {
func imageDownloader(_ imageDownloader: ImageDownloader, didDownload image: UIImage) {
self.image = image
}
}
Всё написано так, как надо. У нас есть аккуратный, знакомый нам API, поэтому мы не беспокоимся об утечках памяти, так как наш делегат имеет свойство weak. Данный пример является полностью рабочим, и не содержит никаких ошибок.
"О чем же статья?" - спросите вы.
Современный Swift
На текущий день, данный паттерн делегирования в стиле Cocoa становится всё менее популярным в употреблении среди Swift разработчиков. Причины на виду: код не выглядит по-современному, так как требуется написать существенное количество шаблонного кода и коду не хватает гибкости, например, в случае с похожей реализацией делегата у нас возникнут трудности в реализации такого делегата для каких нибудь общих конструкций.
Данные причины привели к тому, что с каждым днем разработчики начинают использовать паттерн «Делегирование через замыкание». Давайте теперь перепишем наш пример в соответствии с этим паттерном.
Избавимся от протокола ImageDownloaderDelegate и его делегата в классе ImageDownloader, вместо них напишем свойство с типом замыкания:
class ImageDownloader {
var didDownload: ((UIImage) -> Void)?
func downloadImage(for url: URL) {
download(url: url) { image in
self.didDownload?(image)
}
}
}
И перепишем класс Controller:
class Controller {
let downloader = ImageDownloader()
var image: UIImage?
init() {
downloader.didDownload = { image in
self.image = image
}
}
func updateImage() {
downloader.downloadImage(for: /* some image url */)
}
}
Теперь код стал более компактным и читабельным. Несмотря на это, вы наверяка заметили что тут что-то не так: это была потенциальная утечка памяти.
Избавившись от свойства класса ImageDownloader delegate, мы так же утратили его слабую ссылку, которая была у него благодоря записи weak. Сейчас Controller содержит ссылку на ImageController который, в свою очередь, обратно ссылается на Controller через свое замыкание didDownload. Это классический пример зацикливания и связанной с этим утечки памяти.
Вам наверняка могло прийти в голову решение этой проблемы - необходимо использовать [weak self].
class Controller {
let downloader = ImageDownloader()
var image: UIImage?
init() {
downloader.didDownload = { [weak self] image in
self?.image = image
}
}
func updateImage() {
downloader.downloadImage(for: /* some image url */)
}
}
И код работает так как надо, но...
С точки зрения проектирования API, данный подход только усугубил ситуацию. Ранее спроектированое API само по себе было безопасным в плане отсутсвия утечек памяти. Теперь за возможными местами утечек нужно следить разработчику, который работает с API.
Ведь теперь забыть указать [weak self], очень просто. Я более чем уверен, что много кодовых баз, в том числе и боевых, страдают от этого простого, но вездесущего недуга.
Swift API Design Guidelines гласит:
Такие сущности как: методы и свойства обьявляются только один раз, а они используются многократно.
И это важно. Мы не можем постоянно надеяться на самих себя в плане написания [weak self] каждый раз, когда необходимо это сделать. Также мы не можем ожидать такого поведения от разработичков которые воспользуются нашим API. Наша обязанность как проектировщиков API - позаботиться о безопасном использовании нашего кода.
И Swift поможет нам в этом.
Давайте обратимся к сути проблемы: в 99% случаях, когда мы назначаем делегирущий коллбэк, мы должны объявлять лист захвата [weak self], но ничто не мешает нам не объявлять его. Не будет никаких ошибок, ни предупреждений - ничего. А что если мы будем навязывать правильный стиль написания замыкания?
Вот о чём я пытаюсь сказать самым простым способом:
class ImageDownloader {
private var didDownload: ((UIImage) -> Void)?
func setDidDownload(delegate: Object, callback: @escaping (Object, UIImage) -> Void) {
self.didDownload = { [weak delegate] image in
if let delegate = delegate {
callback(delegate, image)
}
}
}
func downloadImage(for url: URL) {
download(url: url) { image in
self.didDownload?(image)
}
}
}
Теперь наше свойство didDownload стало приватным, и вместо пользователя вызвать setDidDownload(delegate:callback:), который обёртывает входящий объект делегата как слабую ссылку, что является правильным поведением. Вот как будет это выглядеть для Controller:
class Controller {
let downloader = ImageDownloader()
var image: UIImage?
init() {
downloader.setDidDownload(delegate: self) { (self, image) in
self.image = image
}
}
func updateImage() {
downloader.downloadImage(for: /* some image url */)
}
}
Теперь наш код без зацикливаний и утечек памяти! Он выглядит аккуратно и читабельно. Не нужно больше указывать лист захвата [weak self].
Данный подход также используется в модуле UndoManager фреймворка Foundation и в немногих других Cocoa API (ссылка).
Забегая вперёд
Реализацию ImageDownloader выше можно упростить, использовав шаблонные типы из Swift:
struct DelegatedCall {
private(set) var callback: ((Input) -> Void)?
mutating func delegate(to object: Object, with callback: @escaping (Object, Input) -> Void) {
self.callback = { [weak object] input in
guard let object = object else {
return
}
callback(object, input)
}
}
Количество шаблонного кода уменьшилось, и мы теперь имеем очень небольшой API
class ImageDownloader {
var didDownload = DelegatedCall()
func downloadImage(for url: URL) {
download(url: url) { image in
self.didDownload.callback?(image)
}
}
}
Код контроллера, в том числе, стал более аккуратным:
class Controller {
let downloader = ImageDownloader()
var image: UIImage?
init() {
downloader.didDownload.delegate(to: self) { (self, image) in
self.image = image
}
}
func updateImage() {
downloader.downloadImage(for: /* some image url */)
}
}
Теперь у нас всего 14 строчек кода. Компактный вид спасает от многих непреднамеренных утечек памяти.
Это то, за что я люблю Swift и его систему типов. Он дает мне повод бороться с типичными недочётами, с помощью креативного подхода. Вид API DelegatedCall говорит сам за себя. Вы видите то, что можно писать чистый, выразительный и безопасный код.
Данная техника совмещает в себе лучшее: делегирование в стиле Cocoa и делегирование через замыкания. Я использовал данный прием много раз, что решил оформить этот приём в самостоятельный пакет Delegated. Ознакомиться с ним вы можете по этой ссылке: https://github.com/dreymonde/Delegated
Ссылка на оригинал статьи.
Автор статьи: Oleg Dreyman
Перевел статью: Дмитрий Петухов (mail@dphov.com)