Pełnomocnik to strukturalny wzorzec projektowy według którego obiekt-usługodawca używany przez klienta jest zastępowany przez obiekt zastępczy, zwany pełnomocnikiem. Pełnomocnik przechwytuje żądania od klienta, wykonuje jakąś pracę (kontrola dostępu, zarządzanie pamięcią podręczną, itp.) a następnie przekazuje żądanie usługodawcy.
Obiekt będący pełnomocnikiem ma ten sam interfejs co usługodawca, co czyni go wymienialnym z obiektem usługodawcy dotychczas przekazywanym klientowi.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Pełnomocnik ze szczególnym naciskiem na następujące kwestie:
Z jakich składa się klas?
Jakie role pełnią te klasy?
W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć następujący przykład, oparty na prawdziwym przypadku użycia Swift.
Example.swift: Przykład koncepcyjny
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: Wynik działania
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.
Przykład z prawdziwego życia
Example.swift: Przykład z prawdziwego życia
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: Wynik działania
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