Заместитель — это объект, который выступает прослойкой между клиентом и реальным сервисным объектом. Заместитель получает вызовы от клиента, выполняет свою функцию (контроль доступа, кеширование, изменение запроса и прочее), а затем передаёт вызов сервисному объекту.
Заместитель имеет тот же интерфейс, что и реальный объект, поэтому для клиента нет разницы — работать через заместителя или напрямую.
Концептуальный пример
Этот пример показывает структуру паттерна Заместитель , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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