
Заместитель на Swift
Заместитель — это объект, который выступает прослойкой между клиентом и реальным сервисным объектом. Заместитель получает вызовы от клиента, выполняет свою функцию (контроль доступа, кеширование, изменение запроса и прочее), а затем передаёт вызов сервисному объекту.
Заместитель имеет тот же интерфейс, что и реальный объект, поэтому для клиента нет разницы — работать через заместителя или напрямую.
Сложность:
Популярность:
Применимость: Паттерн Заместитель применяется в Swift коде тогда, когда надо заменить настоящий объект его суррогатом, причём незаметно для клиентов настоящего объекта. Это позволит выполнить какие-то добавочные поведения до или после основного поведения настоящего объекта.
Признаки применения паттерна: Класс заместителя чаще всего делегирует всю настоящую работу своему реальному объекту. Заместители часто сами следят за жизненным циклом своего реального объекта.
Концептуальный пример
Этот пример показывает структуру паттерна Заместитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest /// Интерфейс Субъекта объявляет общие операции как для Реального Субъекта, так /// и для Заместителя. Пока клиент работает с Реальным Субъектом, используя этот /// интерфейс, вы сможете передать ему заместителя вместо реального субъекта. protocol Subject { func request() } /// Реальный Субъект содержит некоторую базовую бизнес-логику. Как правило, /// Реальные Субъекты способны выполнять некоторую полезную работу, которая к /// тому же может быть очень медленной или точной – например, коррекция входных /// данных. Заместитель может решить эти задачи без каких-либо изменений в коде /// Реального Субъекта. class RealSubject: Subject { func request() { print("RealSubject: Handling request.") } } /// Интерфейс Заместителя идентичен интерфейсу Реального Субъекта. class Proxy: Subject { private var realSubject: RealSubject /// Заместитель хранит ссылку на объект класса РеальныйСубъект. Клиент может /// либо лениво загрузить его, либо передать Заместителю. init(_ realSubject: RealSubject) { self.realSubject = realSubject } /// Наиболее распространёнными областями применения паттерна Заместитель /// являются ленивая загрузка, кэширование, контроль доступа, ведение /// журнала и т.д. Заместитель может выполнить одну из этих задач, а затем, /// в зависимости от результата, передать выполнение одноимённому методу в /// связанном объекте класса РеальныйСубъект. func request() { if (checkAccess()) { realSubject.request() logAccess() } } private func checkAccess() -> Bool { /// Некоторые реальные проверки должны проходить здесь. print("Proxy: Checking access prior to firing a real request.") return true } private func logAccess() { print("Proxy: Logging the time of request.") } } /// Клиентский код должен работать со всеми объектами (как с реальными, так и /// заместителями) через интерфейс Субъекта, чтобы поддерживать как реальные /// субъекты, так и заместителей. В реальной жизни, однако, клиенты в основном /// работают с реальными субъектами напрямую. В этом случае, для более простой /// реализации паттерна, можно расширить заместителя из класса реального /// субъекта. class Client { // ... static func clientCode(subject: Subject) { // ... subject.request() // ... } // ... } /// Давайте посмотрим как всё это будет работать. class ProxyConceptual: XCTestCase { func test() { print("Client: Executing the client code with a real subject:") let realSubject = RealSubject() Client.clientCode(subject: realSubject) print("\nClient: Executing the same client code with a proxy:") let proxy = Proxy(realSubject) Client.clientCode(subject: proxy) } }
Output.txt: Результат выполнения
Client: Executing the client code with a real subject: RealSubject: Handling request. Client: Executing the same client code with a proxy: Proxy: Checking access prior to firing a real request. RealSubject: Handling request. Proxy: Logging the time of request.
Пример из реальной жизни
Example.swift: Пример из реальной жизни
import XCTest class ProxyRealWorld: XCTestCase { /// Паттерн Заместитель /// /// Назначение: Позволяет подставлять вместо реальных объектов специальные /// объекты-заменители. Эти объекты перехватывают вызовы к оригинальному /// объекту, позволяя сделать что-то до или после передачи вызова оригиналу. /// /// Пример: Существует бесчисленное множество направлений, где могут быть /// использованы заместители: кэширование, логирование, контроль доступа, /// отложенная инициализация и т.д. func testProxyRealWorld() { print("Client: Loading a profile WITHOUT proxy") loadBasicProfile(with: Keychain()) loadProfileWithBankAccount(with: Keychain()) print("\nClient: Let's load a profile WITH proxy") loadBasicProfile(with: ProfileProxy()) loadProfileWithBankAccount(with: ProfileProxy()) } func loadBasicProfile(with service: ProfileService) { service.loadProfile(with: [.basic], success: { profile in print("Client: Basic profile is loaded") }) { error in print("Client: Cannot load a basic profile") print("Client: Error: " + error.localizedSummary) } } func loadProfileWithBankAccount(with service: ProfileService) { service.loadProfile(with: [.basic, .bankAccount], success: { profile in print("Client: Basic profile with a bank account is loaded") }) { error in print("Client: Cannot load a profile with a bank account") print("Client: Error: " + error.localizedSummary) } } } enum AccessField { case basic case bankAccount } protocol ProfileService { typealias Success = (Profile) -> () typealias Failure = (LocalizedError) -> () func loadProfile(with fields: [AccessField], success: Success, failure: Failure) } class ProfileProxy: ProfileService { private let keychain = Keychain() func loadProfile(with fields: [AccessField], success: Success, failure: Failure) { if let error = checkAccess(for: fields) { failure(error) } else { /// Note: /// At this point, the `success` and `failure` closures can be /// passed directly to the original service (as it is now) or /// expanded here to handle a result (for example, to cache). keychain.loadProfile(with: fields, success: success, failure: failure) } } private func checkAccess(for fields: [AccessField]) -> LocalizedError? { if fields.contains(.bankAccount) { switch BiometricsService.checkAccess() { case .authorized: return nil case .denied: return ProfileError.accessDenied } } return nil } } class Keychain: ProfileService { func loadProfile(with fields: [AccessField], success: Success, failure: Failure) { var profile = Profile() for item in fields { switch item { case .basic: let info = loadBasicProfile() profile.firstName = info[Profile.Keys.firstName.raw] profile.lastName = info[Profile.Keys.lastName.raw] profile.email = info[Profile.Keys.email.raw] case .bankAccount: profile.bankAccount = loadBankAccount() } } success(profile) } private func loadBasicProfile() -> [String : String] { /// Gets these fields from a secure storage. return [Profile.Keys.firstName.raw : "Vasya", Profile.Keys.lastName.raw : "Pupkin", Profile.Keys.email.raw : "vasya.pupkin@gmail.com"] } private func loadBankAccount() -> BankAccount { /// Gets these fields from a secure storage. return BankAccount(id: 12345, amount: 999) } } class BiometricsService { enum Access { case authorized case denied } static func checkAccess() -> Access { /// The service uses Face ID, Touch ID or a plain old password to /// determine whether the current user is an owner of the device. /// Let's assume that in our example a user forgot a password :) return .denied } } struct Profile { enum Keys: String { case firstName case lastName case email } var firstName: String? var lastName: String? var email: String? var bankAccount: BankAccount? } struct BankAccount { var id: Int var amount: Double } enum ProfileError: LocalizedError { case accessDenied var errorDescription: String? { switch self { case .accessDenied: return "Access is denied. Please enter a valid password" } } } extension RawRepresentable { var raw: Self.RawValue { return rawValue } } extension LocalizedError { var localizedSummary: String { return errorDescription ?? "" } }
Output.txt: Результат выполнения
Client: Loading a profile WITHOUT proxy Client: Basic profile is loaded Client: Basic profile with a bank account is loaded Client: Let's load a profile WITH proxy Client: Basic profile is loaded Client: Cannot load a profile with a bank account Client: Error: Access is denied. Please enter a valid password