Шаблон проектирования фасад в Swift

Давайте поговорим о шаблонах проектирования Фасад на реальном примере в Swift.

Фасад — это шаблон проектирования, предоставляющий единый упрощенный интерфейс для набора подсистем.

Это помогает снизить сложность, а также сводит к минимуму зависимость между подсистемами.

Давайте посмотрим на общую структуру реализации Фасад:

class SubsystemA {
    func getNames() -> [String] {
        return ["NameA1", "NameA2"]
    }
}

class SubsystemB {
    func getNames() -> [String] {
        return ["NameB1", "NameB2"]
    }
}

class Facade {
    let subsystemA: SubsystemA
    let subsystemB: SubsystemB

    init(subsystemA: SubsystemA, subsystemB: SubsystemB) {
        self.subsystemA = subsystemA
        self.subsystemB = subsystemB
    }

    func getAllNames() -> [String] {
        return subsystemA.getNames() + subsystemB.getNames()
    }
}

Реализация Фасада обычно имеет двух основных действующих лиц/участников:

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

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

Пример из реальной жизни

Допустим, мы работаем над приложением «Фильмы» и отвечаем за получение списка предстоящих фильмов.

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

Вся эта информация (тип учетной записи пользователя и список фильмов в соответствии с типом учетной записи) должна быть получена из внутренней службы.

Кроме того, давайте представим, что у нас уже есть эти два клиента в нашем приложении:

protocol MovieClient {
    func getUpcomingMovies(accountType: AccountType,
                           completion: @escaping (Result<[Movie], Error>) -> Void)
}

protocol AccountClient {
    func getAccountType(completion: @escaping (Result<AccountType, Error>) -> Void)
}

У AccountClient есть метод для получения типа учетной записи текущего пользователя, а у MoviesClient есть метод для получения списка предстоящих фильмов с учетом определенного типа учетной записи.

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

Вы можете подумать, что любой, кто хочет получить список фильмов, должен просто связать эти два метода (получение типа учетной записи из AccountClient и передача его в MovieClient для получения предстоящих фильмов); но у этого подхода есть пара недостатков:

  • Мы зависим от двух клиентов для выполнения довольно простой задачи (зависимости могут быстро увеличиваться, если выполняемая задача намного сложнее). В любое время, когда мы хотели бы получить список предстоящих фильмов для премиум-аккаунтов или не премиум-аккаунтов, нам нужно было взаимодействовать с обеими зависимостями.
  • При получении списка фильмов нам необходимо знать детали реализации обоих клиентов. У этих клиентов может быть гораздо больше методов, которые нам не интересны и бесполезны для нас при выводе списка фильмов.

Это хорошее место для использования паттерна Facade!

Мы реализуем MovieListFetcher, который будет работать как Фасад для наших двух клиентов:

class MovieListFetcher {
    let movieClient: MovieClient
    let accountClient: AccountClient

    init(movieClient: MovieClient, accountClient: AccountClient) {
        self.movieClient = movieClient
        self.accountClient = accountClient
    }

    func getMovies(completion: @escaping (Result<[Movie], Error>) -> Void) {
        accountClient.getAccountType { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let accountType):
                self.movieClient.getUpcomingMovies(accountType: accountType, completion: completion)
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

Как вы можете заметить, AccountClient и MoviesClient будут нашими подсистемами, а MovieListFetcher будет реализацией Facade для этих двух подсистем.

С помощью шаблона Facade мы в основном абстрагируем интерфейс подсистем, чтобы сделать их намного проще в использовании: нам больше не нужно заботиться о том, как работают AccountClient или MoviesClient, нам просто нужно создать экземпляр MovieListFetcher и вызвать его метод getMovies!

Также важно учитывать, что каждая подсистема сама может быть Фасадом: реализация MovieClient или AccountClient может иметь большое количество подсистем, которые используются внутри.

. . .

Вот и все! Я надеюсь, что этот пример реального использования шаблона Facade может быть полезен вам и вашим будущим проектам.