Encapsulate What Varies (EWV)- Design Principle

Principle Statement

Encapsulate What Varies – as the name suggests – Encapsulate everything that may change, i.e. a design is better when those parts that vary are encapsulated in a separate module.

Rationale

Software design can be evaluated by how well it accommodates future changes. Change is inevitable and software in use will eventually require modification due to evolving business needs, improved problem understanding, and other reasons. A well-designed software allows for such changes with minimal effort while a poorly-designed one makes it difficult to modify.

It’s impossible to predict future requirements but possible to identify likely areas of change during software design. Such areas can be shielded from affecting the rest of the program by hiding them behind an interface. Then, when the implementation changes, software written to the interface doesn’t need to change. This is called encapsulating variation.

Its Two Aspects

This principle comprises two aspects that correspond to SRP and OCP sub-principles. The first aspect is about localizing changes. Any anticipated change should be confined to a single module, minimizing cross-cutting concerns, although complete avoidance may not always be possible.

The second aspect involves introducing abstractions, especially when the varying concept occurs at runtime. An abstract base class or interface encapsulates the varying concept, while several concrete descendant classes specify the concrete variation. The distinction between the two aspects lies in whether the varying concept changes over time during maintenance or at runtime. However, the principle’s advice remains the same: encapsulate the concept that varies.

Example

Let’s say we have an e-commerce application that allows customers to place orders and make payments. One of the requirements of the application is to support multiple payment gateways, such as PayPal, Stripe, and Braintree. Each payment gateway has a different API and requires a different set of parameters to process a payment.

To encapsulate the parts of the system that are subject to change, we can create an abstraction called PaymentGateway that represents a payment gateway and provides a well-defined interface to the rest of the system. The PaymentGateway interface could look like this:

public interface PaymentGateway {
    PaymentResult processPayment(PaymentRequest request);
}
Java

The PaymentGateway interface has a single method, processPayment(), which takes a PaymentRequest object and returns a PaymentResult object. The PaymentRequest object contains all the information required to process a payment, such as the payment amount, the customer’s billing information, and the payment gateway to use.

Now, we can create separate implementations of the PaymentGateway interface for each payment gateway that we want to support. For example, we could create a PayPalPaymentGateway class that implements the PaymentGateway interface and uses the PayPal API to process payments. The PayPalPaymentGateway class would be responsible for encapsulating all the details of interacting with the PayPal API, such as authentication, error handling, and parameter formatting. Here’s an example of what the PayPalPaymentGateway class could look like:

public class PayPalPaymentGateway implements PaymentGateway {
    public PaymentResult processPayment(PaymentRequest request) {
        // Use the PayPal API to process the payment and return a PaymentResult object
        // ...
    }
}
Java

Similarly, we could create other classes that implement the PaymentGateway interface for other payment gateways, such as StripePaymentGateway and BraintreePaymentGateway.

By encapsulating the details of interacting with the payment gateways in separate classes that implement the PaymentGateway interface, we have encapsulated the parts of the system that are subject to change. If we need to add support for a new payment gateway, we can simply create a new class that implements the PaymentGateway interface, without having to modify the rest of the system.

Benefits of this effort

As we have seen in above example, there are several reasons why we should apply the principle of encapsulating what varies:

  1. Flexibility: By encapsulating the parts of the system that are subject to change, we make it easier to modify or replace those parts without affecting the rest of the system. This makes the system more flexible and easier to adapt to changing requirements.
  2. Reusability: By creating abstractions that represent the parts of the system that are subject to change, we make it possible to reuse those abstractions in different contexts. This can save time and effort when developing new features or applications.
  3. Maintainability: By isolating the impact of changes to a specific part of the system, we make it easier to maintain the system over time. This reduces the risk of introducing unintended side effects or breaking existing functionality.
  4. Testability: By encapsulating the parts of the system that are subject to change, we make it easier to test those parts in isolation. This allows us to identify and fix issues more quickly and with greater confidence.
  5. Scalability: By designing the system with encapsulation in mind, we can make it easier to scale the system horizontally or vertically. This can help to ensure that the system remains performant and reliable as the number of users or amount of data grows.

Strategies

Here are a few ways we can enforce the principle of encapsulating what varies:

  1. Use abstraction: Identify the parts of the system that are subject to change and create abstractions that represent them. These abstractions should provide a well-defined interface to the rest of the system and encapsulate the details of their implementation.
  2. Apply the Open/Closed Principle: The Open/Closed Principle states that software entities should be open for extension but closed for modification. This means that we should design our software in a way that allows us to add new functionality without having to modify existing code. One way to achieve this is by using abstraction and polymorphism.
  3. Use Dependency Injection: Dependency Injection is a design pattern that allows us to inject dependencies into a class rather than having the class create them itself. By doing this, we can easily swap out dependencies with different implementations without having to modify the class itself.
  4. Apply Separation of Concerns: Separation of Concerns is a design principle that states that software should be divided into distinct parts, each of which is responsible for a specific aspect of the functionality. By doing this, we can ensure that each part of the system has a single responsibility and is not affected by changes in other parts of the system.
  5. Configuration: We can use configuration files to specify which implementation of an abstraction should be used at runtime. By doing this, we can switch between different implementations without having to recompile the code.

Additional Read: https://learn.microsoft.com/en-gb/archive/blogs/steverowe/encapsulate-what-varies

Happy Reading!!!