O padrão Observer no Swift

Apr 02 2023
No desenvolvimento de software, observar mudanças no estado é um padrão comum usado para implementar recursos como programação orientada a eventos, programação reativa e arquitetura Model-View-Controller (MVC). Neste artigo, discutiremos como implementar o padrão de observação no Swift usando os exemplos de código fornecidos.

No desenvolvimento de software, observar mudanças no estado é um padrão comum usado para implementar recursos como programação orientada a eventos, programação reativa e arquitetura Model-View-Controller (MVC). Neste artigo, discutiremos como implementar o padrão de observação no Swift usando os exemplos de código fornecidos.

Padrão de observação

O padrão de observação é um padrão de design que permite a um objeto (o observador) observar mudanças em outro objeto (o sujeito) e agir quando o sujeito muda. O objeto observador se registra com o objeto sujeito para receber notificações quando um evento específico ocorre. Quando o evento ocorre, o objeto sujeito envia uma notificação a todos os observadores registrados e cada observador realiza uma ação com base na notificação.

O código fornecido demonstra como implementar o padrão de observação no Swift usando o exemplo de um recurso de curtida de postagem em um aplicativo de mídia social. A seguir estão os principais componentes do padrão de observação no código:

PostLikeChangeType: Esta enumeração define os tipos de alterações que podem ocorrer no recurso de curtida da postagem, ou seja, gostar ou não. Ele também define o postId associado a cada tipo de alteração.

public enum PostLikeChangeType {
    case like(postId: Int)
    case unlike(postId: Int)

    public var value: Int {
        switch self {
        case .like(let postId):
            return postId
        case .unlike(let postId):
            return postId
        }
    }
}

public typealias PostStatusResultHandler = (Result<EmptyResponse, Error>) -> Void
public typealias PostStatusOnChangeObserver = (_ type: PostLikeChangeType) -> ()

public protocol PostLikeHandlerInterface {
    func postLike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler)
    func postUnlike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler)
    func addPostLikeHandlerObserver(_ observer: AnyObject, completion: @escaping PostStatusOnChangeObserver)
}

public protocol PostLikeHandlerInteractorInterface {
    func postLike(with postId: Int, completion: @escaping PostStatusResultHandler)
    func postUnlike(with postId: Int, completion: @escaping PostStatusResultHandler)
}

public struct PostLikedResponse: Codable {
    public let postIds: [Int]
}

final class PostLikeHandlerInteractor {
    init() {}
}

extension PostLikeHandlerInteractor: PostLikeHandlerInteractorInterface {
    func postLike(with postId: Int, completion: @escaping PostStatusResultHandler) {
        // like
    }

    func postUnlike(with postId: Int, completion: @escaping PostStatusResultHandler) {
        // unlike
    }
}

func addPostLikeHandlerObserver(_ observer: AnyObject, completion: @escaping PostStatusOnChangeObserver) {
    let id = UUID()
    
    observers[id] = .init(
        observer: observer,
        onChangeObserver: { [weak self, weak observer] type in
            guard observer != nil else {
                self?.observers.removeValue(forKey: id)
                return
            }
            completion(type)
        }
    )
}

O método gera um identificador único para o observador usando UUID(). Este identificador será usado para rastrear o observador.

O observador é adicionado a um dicionário chamado observadores. A chave para o dicionário é o identificador exclusivo gerado na etapa 3 e o valor é uma instância de uma estrutura que contém duas propriedades: :
observerUma referência fraca ao objeto passado como parâmetro do observador. Isso é feito para evitar a criação de um forte ciclo de referência entre o observador e o objeto que está sendo observado.
onChangeObserver: Um fechamento que recebe um parâmetro PostStatus e não retorna nada. Esse fechamento é criado usando uma lista de captura que inclui referências fracas a self e observer. Isso é feito para evitar a criação de um forte ciclo de referência entre o fechamento e o objeto que está sendo observado.

Sempre que houver uma alteração no status do post, o encerramento passado como parâmetro de conclusão é chamado com o valor PostStatus atualizado.

Se o objeto observador for desalocado (ou seja, sua contagem de referência cair para zero), sua entrada no dicionário de observadores será removida para evitar a tentativa de chamar um fechamento em um objeto desalocado.

private func notifyObservers(_ observer: AnyObject, type: PostLikeChangeType) {
      let filtered = observers.filter ({ $0.value.observer !== observer })
      filtered.forEach({ $0.value.onChangeObserver(type) })
}

Antes de notificar os observadores, a função filtra o objeto observador passado como argumento para evitar notificações desnecessárias. Isso ocorre porque o objeto observador já sabe sobre a alteração, pois foi ele quem a disparou. Ao filtrar o observador, evitamos notificações redundantes e melhoramos o desempenho.

Uma vez obtida a lista filtrada de observadores, a função itera sobre ela e chama o método onChangeObserver de cada observador, passando o valor enum PostLikeChangeType como um argumento. Isso permite que cada observador reaja à mudança à sua maneira, dependendo de sua implementação.

final class PostLikeHandler: PostLikeHandlerInterface {
    struct LikeObserverArgument {
        weak var observer: AnyObject?
        var onChangeObserver: PostStatusOnChangeObserver
    }

    static let shared: PostLikeHandlerInterface = PostLikeHandler()
    private var observers: [UUID: LikeObserverArgument] = [:]
    private let interactor: PostLikeHandlerInteractorInterface

    init(interactor: PostLikeHandlerInteractorInterface = PostLikeHandlerInteractor()) {
        self.interactor = interactor
    }

    private func notifyObservers(_ observer: AnyObject, type: PostLikeChangeType) {
        let filtered = observers.filter ({ $0.value.observer !== observer })
        filtered.forEach({ $0.value.onChangeObserver(type) })
    }

    func addPostLikeHandlerObserver(_ observer: AnyObject, completion: @escaping PostStatusOnChangeObserver) {
        let id = UUID()

        observers[id] = .init(
            observer: observer,
            onChangeObserver: { [weak self, weak observer] type in
                guard observer != nil else {
                    self?.observers.removeValue(forKey: id)
                    return
                }
                completion(type)
            }
        )
    }
}

extension PostLikeHandler {
    func postLike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler) {
        interactor.postLike(with: postId) { result in
            self.notifyObservers(observer, type: .like(postId: postId))
        }
    }

    func postUnlike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler) {
        interactor.postLike(with: postId) { result in
            self.notifyObservers(observer, type: .like(postId: postId))
        }
    }
}

final class PostListPresenter {
    private var likeHandler: PostLikeHandlerInterface

    init(likeHandler: PostLikeHandlerInterface = PostLikeHandler.shared) {
        self.likeHandler = likeHandler
    }

    func viewDidLoad() {
        addPostLikeHandlerObserver()
    }

    private func addPostLikeHandlerObserver() {
        likeHandler.addPostLikeHandlerObserver(self) { type in
            switch type {
            case .like(let postId):
                // - update related post ui
            case .unlike(let postId):
                // - update related post ui
            }
        }
    }

    private func likeButtonTapped(with postId: Int, isLike: Bool) {
        if isLike {
            likeHandler.postLike(self, with: postId) { result in
                // - update related post ui
            }
        } else {
            likeHandler.postUnlike(self, with: postId) { result in
                // - update related post ui
            }
        }
    }
}

Esse padrão é particularmente útil quando há um relacionamento um-para-muitos entre as classes e são necessárias alterações de transmissão.

Usando o padrão de observação e implementando a injeção de dependência, podemos criar uma base de código testável que evita a duplicação de código e vazamentos de memória ao aplicar alterações no id observado por diferentes classes.

Obrigado por reservar um tempo para ler. Se você tiver algum comentário ou quiser entrar em contato, sinta-se à vontade para entrar em contato. ✨

Referências

https://www.swiftbysundell.com/articles/observers-in-swift-part-2/