La Procuration est un patron de conception structurel qui fournit un objet qui agit comme un substitut pour un objet du service utilisé par un client. Une procuration reçoit les demandes d’un client, effectue des tâches (contrôle des accès, mise en cache, etc.) et passe ensuite la demande à un objet du service.
L’objet Procuration possède la même interface qu’un service, ce qui le rend interchangeable avec un vrai objet lorsqu’il est passé à un client.
Exemple conceptuel
Dans cet exemple, nous allons voir la structure de la Procuration et répondre aux questions suivantes :
Que contiennent les classes ?
Quels rôles jouent-elles ?
Comment les éléments du patron sont-ils reliés ?
Après avoir étudié la structure du patron, vous pourrez plus facilement comprendre l’exemple suivant qui est basé sur un cas d’utilisation réel en Swift.
Example.swift: Exemple conceptuel
import XCTest
/// The Subject interface declares common operations for both RealSubject and
/// the Proxy. As long as the client works with RealSubject using this
/// interface, you'll be able to pass it a proxy instead of a real subject.
protocol Subject {
func request()
}
/// The RealSubject contains some core business logic. Usually, RealSubjects are
/// capable of doing some useful work which may also be very slow or sensitive -
/// e.g. correcting input data. A Proxy can solve these issues without any
/// changes to the RealSubject's code.
class RealSubject: Subject {
func request() {
print("RealSubject: Handling request.")
}
}
/// The Proxy has an interface identical to the RealSubject.
class Proxy: Subject {
private var realSubject: RealSubject
/// The Proxy maintains a reference to an object of the RealSubject class.
/// It can be either lazy-loaded or passed to the Proxy by the client.
init(_ realSubject: RealSubject) {
self.realSubject = realSubject
}
/// The most common applications of the Proxy pattern are lazy loading,
/// caching, controlling the access, logging, etc. A Proxy can perform one
/// of these things and then, depending on the result, pass the execution to
/// the same method in a linked RealSubject object.
func request() {
if (checkAccess()) {
realSubject.request()
logAccess()
}
}
private func checkAccess() -> Bool {
/// Some real checks should go here.
print("Proxy: Checking access prior to firing a real request.")
return true
}
private func logAccess() {
print("Proxy: Logging the time of request.")
}
}
/// The client code is supposed to work with all objects (both subjects and
/// proxies) via the Subject interface in order to support both real subjects
/// and proxies. In real life, however, clients mostly work with their real
/// subjects directly. In this case, to implement the pattern more easily, you
/// can extend your proxy from the real subject's class.
class Client {
// ...
static func clientCode(subject: Subject) {
// ...
subject.request()
// ...
}
// ...
}
/// Let's see how it all works together.
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: Résultat de l’exécution
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.
Analogie du monde réel
Example.swift: Analogie du monde réel
import XCTest
class ProxyRealWorld: XCTestCase {
/// Proxy Design Pattern
///
/// Intent: Provide a surrogate or placeholder for another object to control
/// access to the original object or to add other responsibilities.
///
/// Example: There are countless ways proxies can be used: caching, logging,
/// access control, delayed initialization, etc.
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: Résultat de l’exécution
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