7.7. Proxy

  • EN: Proxy

  • PL: Pełnomocnik

  • Type: object

The Proxy design pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. This pattern is used when you need to create a wrapper to cover the main object’s complexity.

Here's a simple example of the Proxy pattern in Python:

>>> class RealSubject:
...     def request(self) -> str:
...         return "RealSubject: Handling request."
...
>>> class Proxy:
...     _real_subject: RealSubject = None
...
...     def __init__(self, real_subject: RealSubject) -> None:
...         self._real_subject = real_subject
...
...     def request(self) -> str:
...         if self.check_access():
...             self._real_subject.request()
...             self.log_access()
...
...     def check_access(self) -> bool:
...         print("Proxy: Checking access prior to firing a real request.")
...         return True
...
...     def log_access(self) -> None:
...         print("Proxy: Logging the time of request.")
...
>>> real_subject = RealSubject()
>>> proxy = Proxy(real_subject)
>>> proxy.request()
Proxy: Checking access prior to firing a real request.
Proxy: Logging the time of request.

In this example, RealSubject is a class that contains some core business logic. Usually, this class has methods that do some useful work. Proxy is a class that has the same interface as the RealSubject. It is used to control access to the real subject and to perform actions either before or after the request gets through to the RealSubject. The request method in the Proxy class performs some actions, checks whether the request should be sent to the RealSubject, and logs the request data.

7.7.1. Pattern

  • Create a proxy, or agent for a remote object

  • Agent takes message and forwards to remote object

  • Proxy can log, authenticate or cache messages

../../_images/designpatterns-proxy-pattern-1.png
../../_images/designpatterns-proxy-pattern-2.png

7.7.2. Problem

  • Creating Ebook object is costly, because we have to read it from the disk and store it in memory

  • It will load all ebooks in our library, just to select one

design-patterns/structural/img/designpatterns-proxy-problem.png

from dataclasses import dataclass, field


@dataclass
class Ebook:
    filename: str

    def __post_init__(self):
        self._load()

    def _load(self) -> None:
        print(f'Loading the ebook {self.filename}')

    def show(self) -> None:
        print(f'Showing the ebook {self.filename}')

    def get_filename(self) -> None:
        return self.filename


@dataclass
class Library:
    ebooks: dict[str, Ebook] = field(default_factory=dict)

    def add(self, ebook: Ebook) -> None:
        self.ebooks[ebook.get_filename()] = ebook

    def open(self, filename: str) -> None:
        self.ebooks.get(filename).show()


if __name__ == '__main__':
    library: Library = Library()
    filenames: list[str] = ['ebook-a.pdf', 'ebook-b.pdf', 'ebook-c.pdf']  # Read from database

    for filename in filenames:
        library.add(Ebook(filename))

    library.open('ebook-a.pdf')
    # Loading the ebook ebook-a.pdf
    # Loading the ebook ebook-b.pdf
    # Loading the ebook ebook-c.pdf
    # Showing the ebook ebook-a.pdf

7.7.3. Solution

  • Lazy evaluation

  • Open/Close Principle

../../_images/designpatterns-proxy-solution.png

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Proxy:
    pass


class Ebook(ABC):
    @abstractmethod
    def show(self) -> None:
        pass

    @abstractmethod
    def get_filename(self) -> None:
        pass


@dataclass
class RealEbook(Ebook):
    filename: str

    def __post_init__(self):
        self.load()

    def load(self) -> None:
        print(f'Loading the ebook {self.filename}')

    def show(self) -> None:
        print(f'Showing the ebook {self.filename}')

    def get_filename(self) -> None:
        return self.filename


@dataclass
class EbookProxy(Ebook):
    filename: str
    ebook: RealEbook | None = None

    def show(self) -> None:
        if self.ebook is None:
            self.ebook = RealEbook(self.filename)
        self.ebook.show()

    def get_filename(self) -> None:
        return self.filename


@dataclass
class Library:
    ebooks: dict[str, RealEbook] = field(default_factory=dict)

    def add(self, ebook: RealEbook) -> None:
        self.ebooks[ebook.get_filename()] = ebook

    def open(self, filename: str) -> None:
        self.ebooks.get(filename).show()


if __name__ == '__main__':
    library: Library = Library()
    filenames: list[str] = ['ebook-a.pdf', 'ebook-b.pdf', 'ebook-c.pdf']  # Read from database

    for filename in filenames:
        library.add(EbookProxy(filename))

    library.open('ebook-a.pdf')
    # Loading the ebook ebook-a.pdf
    # Showing the ebook ebook-a.pdf

Proxy with Authorization and Logging:

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Proxy:
    pass


class Ebook(ABC):
    @abstractmethod
    def show(self) -> None:
        pass

    @abstractmethod
    def get_filename(self) -> None:
        pass


@dataclass
class RealEbook(Ebook):
    filename: str

    def __post_init__(self):
        self.load()

    def load(self) -> None:
        print(f'Loading the ebook {self.filename}')

    def show(self) -> None:
        print(f'Showing the ebook {self.filename}')

    def get_filename(self) -> None:
        return self.filename


@dataclass
class EbookProxy(Ebook):
    filename: str
    ebook: RealEbook | None = None

    def show(self) -> None:
        if self.ebook is None:
            self.ebook = RealEbook(self.filename)
        self.ebook.show()

    def get_filename(self) -> None:
        return self.filename


@dataclass()
class LoggingEbookProxy(Ebook):
    filename: str
    ebook: RealEbook | None = None

    def show(self) -> None:
        if self.ebook is None:
            self.ebook = RealEbook(self.filename)
        print('Logging')
        self.ebook.show()

    def get_filename(self) -> None:
        return self.filename


@dataclass
class Library:
    ebooks: dict[str, RealEbook] = field(default_factory=dict)

    def add(self, ebook: RealEbook) -> None:
        self.ebooks[ebook.get_filename()] = ebook

    def open(self, filename: str) -> None:
        self.ebooks.get(filename).show()


if __name__ == '__main__':
    library: Library = Library()
    filenames: list[str] = ['ebook-a.pdf', 'ebook-b.pdf', 'ebook-c.pdf']  # Read from database

    for filename in filenames:
        library.add(LoggingEbookProxy(filename))

    library.open('ebook-a.pdf')
    # Loading the ebook ebook-a.pdf
    # Logging
    # Showing the ebook ebook-a.pdf

7.7.4. Assignments