Flutter Design Patterns: 15 — Proxy
An overview of the Proxy design pattern and its implementation in Dart and Flutter
In the last article, I have analysed a relatively simple design pattern comparing to the other patterns in the series — Prototype. In this article, I would like to analyse and implement a structural design pattern which is very practical and could be used in a lot of cases when developing applications with Dart and Flutter — it is Proxy.
Table of Contents
- What is the Proxy design pattern?
- Other articles in this series
- Your contribution
What is the Proxy design pattern?
Proxy, also known as Surrogate, belongs to the category of structural design patterns. The intention of this design pattern is described in the GoF book:
Provide a surrogate or placeholder for another object to control access to it.
The key idea in this pattern is to work through a separate proxy object that performs additional functionality when accessing an (already existing) object. For instance, the user’s rights should be validated before accessing the object or the object’s creation is very expensive so it makes sense to defer its creation until the object is actually needed. Also, if you need to execute something either before or after the primary logic of the class, the proxy lets you do this without changing that class. Since the proxy implements the same interface as the original class, it can be passed to any client that expects a real service object.
To understand the Proxy design pattern better, let’s dive in by analysing its structure, types and implementation in more detail!
The general structure of the Proxy design pattern looks like this:
- ServiceInterface — defines the common interface for Service and Proxy that the proxy service could be used instead of the real one;
- Service — defines the real object which contains some useful business logic. This is the service that the proxy represents;
- Proxy — implements the same interface as the real service. Also, has a reference field that points to a service object which allows controlling the access to it. The Proxy class may be responsible for creating and deleting the real service’s object;
- Client — should work with both services and proxies via the same interface. This way you can pass a proxy into any code that expects a service object.
There are dozens of ways to utilize the Proxy pattern:
- Lazy initialization (virtual proxy) — instead of creating the object when the app launches, you can delay the object’s initialization to a time when it’s really needed;
- Access control (protection proxy) — the proxy can pass the request to the service object only if the client’s credentials match some criteria;
- Local execution of a remote service (remote proxy) — the proxy passes the client request over the network, handling all of the nasty details of working with the network;
- Logging requests (logging proxy) — the proxy can log each request before passing it to the service;
- Caching request results (caching proxy) — the proxy can implement caching for recurring requests that always yield the same results. The proxy may use the parameters of requests as the cache keys.
For the following example of the Proxy design pattern, we will implement the caching proxy.
In my opinion, in any application which loads resources from an external service, it is quite a common problem to improve the performance and reduce load times of the data. It is possible to optimise and at least partially resolve it by implementing the caching layer.
Let’s say we have a list of customers with some basic information — customer’s id and name. Any additional customer data should be loaded from an external web service. When providing the general list of customers, additional information is not loaded nor used. However, it could be accessed by selecting a specific customer and loading its data from the customer details service. To reduce the number of requests sent to the external service, it makes sense to introduce a caching layer to the application and provide the already loaded information from the cache for future requests.
To achieve this, the Proxy design pattern is a great choice! Let’s check the class diagram first and then implement a proxy for the customer details service.
The class diagram below shows the implementation of the Proxy design pattern:
Customer class is used to store information about the customer. One of its properties is the CustomerDetails which stores additional data about the customer e.g. its email, hobby and position.
ICustomerDetailsService is an abstract class which is used as an interface for the customer details service:
- getCustomerDetails() — an abstract method which returns details for the specific customer.
CustomerDetailsService is the “real” customer details service which implements the abstract class ICustomerDetailsService and its methods.
CustomerDetailsServiceProxy is a proxy service which contains the cache (dictionary object) and sends the request to the real CustomerDetailsService only if the customer details object is not available in the cache.
ProxyExample initialises and contains the proxy object of the real customer details service. When a user selects the option to see more details about the customer, the dialog window appears and loads details about the customer. If the details object is already stored inside the cache, the proxy service returns that object instantly. Otherwise, a request is sent to the real customer details service and the details object is returned from there.
A simple class to store information about the customer: its id, name and details. Also, the constructor generates random id and name values when initialising the Customer object.
A simple class to store information about customer details: id to map the details information with the corresponding customer, e-mail address, hobby and the current position (job title).
An interface which defines the getCustomerDetails() method to be implemented by the customer details service and its proxy. Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.
A specific implementation of the ICustomerDetailsService interface — the real customer details service. The getCustomerDetails() method mocks the real behaviour of the service and generates random values of customer details.
A specific implementation of the ICustomerDetailsService interface — a proxy for the real customer details service. Before making a call to the customer details service, the proxy service checks whether the customer details are already fetched and saved in the cache. If yes, the customer details object is returned from the cache, otherwise, a request is sent to the real customer service and its value is saved to the cache and returned.
First of all, a markdown file is prepared and provided as a pattern’s description:
ProxyExample contains the proxy object of the real customer details service. When the user wants to see customer details, the showDialog() method is triggered (via the showCustomerDetails() method) which opens the dialog window of type CustomerDetailsDialog and passes the proxy object via its constructor as well as the selected customer’s information — the Customer object.
The CustomerDetailsDialog class uses the passed proxy service on its state’s initialisation, hence loading details of the selected customer.
The CustomerDetailsDialog class does not care about the specific type of customer details service as long as it implements the ICustomerDetailsService interface. As a result, an additional caching layer could be used by sending the request through the proxy service, hence improving the general performance of the application, possibly saving some additional network data and reducing the number of requests sent to the real customer details service as well. Also, if you want to call the real customer details service directly, you can just simply pass it via the CustomerDetailsDialog constructor — no additional changes are needed in the UI code since both the real service and its proxy implements the same interface.
As you can see in the example, when trying to load the specific customer’s details for the first time, it takes some time for the information to load from the service. However, when the same information is accessed once again, it is provided from the cache stored in the proxy service, hence the request is not sent to the real customer details service — the customer details information is provided instantly.
All of the code changes for the Proxy design pattern and its example implementation could be found here.
Other articles in this series
👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the series.
📢 Share this article with your friends, colleagues in social media.
➕ Follow me on Medium.
⭐ Star the Github repository.