10.7. Proxy

  • EN: Proxy

  • PL: Pełnomocnik

  • Type: object

10.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

10.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

10.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

10.7.4. Assignments