Using Python types for fun and profit: the Proxy Pattern

Introduction

The proxy-pattern is a very useful design pattern. The basic function is to make sure that a caller has no direct access to the instance of an object but rather goes through another class which does have access.

I know this all sounds rather cryptic, so maybe a diagram will help here:

This could be the diagram for some form of a very simplified mobility service.

So, what do we see here?

  1. Drivable interface with exactly one method: drive(int instance)
  2. Two classes, Car and Bicycle which implement the Drivable interface
  3. VehicleProxy class which implements the Drivable interface, but which also holds a concrete class, in this case a Car class, which also implements the Drivable interface.
  4. A concrete VehicleManager class which holds an instance of the VehicleProxy. The class has no knowledge of the extact kind of vehicle of will be driven, nor of any of the implementation details.

This pattern can be very useful in some cases:

  1. If you want to control access to the concrete class
  2. If you want to have conditional access to the concrete classes. In this particular example, the VehicleProxy class could also be given the age of the driver as a parameter. If the driver is younger that 18, he or she is not allowed to drive a care, for example.

We used Python 3.12 in this example, I will update this article as soon as newer version is released.

Implementation in Python

Implementing this pattern in Python turns out to be quite easy.

The first thing we need to do is define a common interface. Since Python has no concept of interfaces, traits or protocols, we use a class with unimplemented methods:

class Drivable:
    def drive(self, dist: int):
        pass

Next we need to implement this class. We do this by subclassing it into two subclasses, Car and Bike:

class Car(Drivable):
    def drive(self, dist: int):
        print(f"Car driving for {dist} km")


class Bike(Drivable):
    def drive(self, dist: int):
        print(f"Bike driving for {dist} km")

Note that both classes implement the drive method, and notice the type-annotation.

Next we implement the VehicleProxy class:

class VehicleProxy:
    _vehicle: Drivable = None

    def __init__(self, vehicle: Drivable):
        self._vehicle = vehicle

    def drive(self, dist: int):
        self._vehicle.drive(dist)

Some notes:

  • The VehicleProxy class has one field of type Drivable
  • We pass an object which implements the Drivable class to the constructor.
  • The drive() method is basically a wrapper around the drive() method in the Drivable class

Notice that every parameter in every method has a type-annotation, apart from self. This makes the code easier to read, and more maintainable.

Now all we need is a VehicleManager class:

class VehicleManager:
    _proxy: VehicleProxy = None

    def __init__(self, proxy: VehicleProxy):
        self._proxy = proxy

    def drive(self, dist: int):
        self._proxy.drive(dist)

Some notes:

  1. There is one field in this class of type VehicleProxy
  2. The parameter to the constructor is of type VehicleProxy. Important note: the VehicleManager does not know about the type of vehicle.
  3. The drive() method calls the same method on the proxy, not on the object doing the driving

Time to test

Now we can test our pattern:

if __name__ == "__main__":
    car: Car = Car()
    car_proxy: VehicleProxy = VehicleProxy(car)

    vehicle_manager = VehicleManager(car_proxy)
    vehicle_manager.drive(10)

Line by line:

  1. We construct a Car object.
  2. Next we construct a VehicleProxy for that object. Note that since Car implements the Drivable class, it can be passed to the VehicleProxy constructor.
  3. Then we pass this proxy to the constructor of the VehicleManager
  4. Lastly we call the drive() method of the VehicleManager. Please pay attentention to the fact that the client has no knowledge about the vehicle being driven.

Conclusion

The implementation was very easy in Python. The type-annotation helped make the code clearer and more stable.

One problem is that the Python interpreter itself does not enforce these type rules. It therefore helps to use an IDE like Jetbrain’s PyCharm which warns about violations of the type rules.

Leave a Reply

Your email address will not be published. Required fields are marked *