Design Patterns

Author Author:
Innokrea Team
Date of publication: 2024-10-14
Caterogies: Programming

Hello! Today, as Innokrea, we’ll talk to you about what design patterns are and why every developer should have them in their virtual toolbox of development tools. If you’re interested, we invite you to read further, and if you haven’t yet checked out our previous articles on SOLID principles, we highly encourage you to do so, as they are closely related to the topic of design patterns.

 

What are Design Patterns?

Design patterns are tried-and-tested solutions to frequently occurring problems in programming. They mainly deal with what classes a developer should create to solve a particular problem (in other words, how to distribute responsibility between components) and how to connect them. By using design patterns, we ensure that our code adheres to the SOLID principles and is easy to extend with additional functionalities. Design patterns encourage developers to write code that follows SOLID principles. Without them, writing large systems becomes more difficult due to the increasing complexity of dependencies between components. In practice, this means that a single change in the code can affect components that it shouldn’t. Maintaining such code becomes very challenging, time-consuming, and costly, increasing the risk of introducing errors and creating technical debt.

Design patterns not only simplify software development but also improve collaboration between developers. If every team member knows patterns that can be applied to solve common problems, communication within the team and work on the project become much more efficient.

 

Types of Design Patterns

There are dozens of design patterns addressing various programming problems, but we generally distinguish three main categories:

  • Creational Patterns – These facilitate object creation and separate the process of creating objects from their usage. Examples include solutions like Singleton, Factory Method, or Builder. These can be compared to the production process – sometimes it’s best to use a factory, while other times you may want to create products individually, tailoring each element.
  • Structural Patterns – These define ways to connect classes to make them easily extendable. Examples include Adapter or Decorator patterns. It’s like using adapters for plugs in a foreign country or simplifying hotel processes through a designated person who coordinates various services on behalf of the client.
  • Behavioral Patterns – These primarily describe interactions between various objects – how they communicate and collaborate to complete a specific task. Examples include the Observer or Strategy patterns. These solutions can be compared to subscribing to a newsletter or choosing a mode of transportation (strategy) to get to the office, such as by bike or car.

There are also other types of design patterns, such as those related to architecture or multithreading programming.

 

Implementation – Strategy

Let’s try implementing one of the behavioral patterns – the Strategy pattern. It allows you to define a family of algorithms/strategies, encapsulate them, and make them interchangeable during the program’s execution. This pattern provides flexibility, enabling the client to choose an algorithm without changing the code. The example we’ll use in this article involves payment mechanisms for an online store. Imagine that our program is an application for an online shop that processes user payments, among other things. We must allow for the use of various payment mechanisms, and our solution should be extensible to include new payment providers without significant modifications to the code. This is where the Strategy design pattern comes to the rescue.

 

Design Patterns - Class Diagram

Figure 1 – Class Diagram for an Online Store Application

 

Our application has several classes responsible for simulating the operation of the store. Of course, this is not a complete and real system, but it is meant to show the context of applying the strategy pattern. We can see the Store class, which holds an IPaymentStrategy object, an interface with the pay method. Thanks to using an interface, the PaypalPayment and StripePayment classes can be used in place of IPaymentStrategy. A customer wanting to make a payment chooses a payment method, and the code in the Store class, through the setPaymentStrategy and executePayment methods, is able to make a payment using any implemented method. This allows us to easily change the payment method through abstraction. The Store class does not depend directly on any specific implementation, only on the interface.

 

Design Patterns - Sequence Diagram

Figure 2 – Sequence Diagram for the Implemented Use Case of the Strategy Pattern

 

The Java code implementing the above use case is presented below.

public interface IPaymentStrategy {
   void pay(Double amount, String toAccountId);
}

public class PaypalPayment implements  IPaymentStrategy{
   @Override
   public void pay(Double amount, String toAccountId) {
       // Payment logic implementation
      System.out.println("Paypal payment: "+amount+" paid to "+toAccountId);
   }
}

public class StripePayment implements  IPaymentStrategy{
   @Override
   public void pay(Double amount, String toAccountId) {
      // Payment logic implementation
      System.out.println("Stripe payment: "+amount+" paid to "+toAccountId);
   }
}

public class Store {
   // Online store class
   Double cartValueAmount = 30.0;
   String shopAccountId = "123321";
   IPaymentStrategy paymentStrategy;
   
   public Store(IPaymentStrategy paymentStrategy) {
      this.paymentStrategy = paymentStrategy;
   }

   public void setPaymentStrategy(IPaymentStrategy paymentStrategy){
      this.paymentStrategy = paymentStrategy;
   }

   public void executePayment() {
      System.out.println("Executing payment strategy with class: " + paymentStrategy.getClass().getName());   this.paymentStrategy.pay(this.cartValueAmount,this.shopAccountId);
      System.out.println("Payment successful!\n");
   }
}

public class Client {
   public static void main(String[] args) {
      System.out.print("Welcome to the shop! \n \n");
      IPaymentStrategy paymentStrategy1 = new PaypalPayment();
      IPaymentStrategy paymentStrategy2 = new StripePayment();
      Store onlineStore = new Store(paymentStrategy1);
      onlineStore.executePayment();
      onlineStore.setPaymentStrategy(paymentStrategy2);
      onlineStore.executePayment();
   }
}

 

Implementation – Template Method

The second behavioral pattern we will implement is the Template Method pattern, used to define the skeleton of an algorithm in a base class while allowing subclasses to override specific steps without modifying the algorithm’s structure. Let’s continue with the example of the online store—this time in the context of order processing, where the process might look like this: select a product, check if it is a gift, wrap it if needed, and deliver.

 

Template Method pattern

Figure 3 – Template Method pattern in the context of the store

 

Imagine a scenario where the store needs to process orders in a certain way, which may look like this: select a product, check if it is marked as a gift, wrap it in gift paper if it is, and deliver it. It’s important to note that this process is quite abstract, and its specific implementation may vary depending on the context. For this purpose, we will use the Template Method pattern, which allows us to define an abstract algorithm and then apply its very specific implementation. We will do this using the abstract class OrderProcessTemplate and its two implementations, OnlineOrderProcess and StoreOrderProcess, which will define the methods for processing online orders and in-store pickup orders.

 

Sequence diagram for the implementation of the Template Method pattern

Figure 4 – Sequence diagram for the implementation of the Template Method pattern in the above example

 

We can see how calling the processOrder method triggers specific implementations of methods in the inheriting subclasses, namely OnlineOrderProcess and StoreOrderProcess. This way, we can create additional “processors” for other cases without modifying other classes. Let’s take a look at the Java code that implements these concepts.

public class StoreOrderProcess extends OrderProcessTemplate {
  public StoreOrderProcess() {
    super();
  }

  @Override
  protected void selectProduct() {
     System.out.println("Selecting product from the inventory.");
  }

  @Override
  protected void deliver() {
    System.out.println("Customer will pick up the product from the store.");
  }
}

public abstract class OrderProcessTemplate {
  public OrderProcessTemplate() {
  }

  public final void processOrder() {
    selectProduct();
    if (isGift()) {
      wrapGift();
    }
    deliver();
  }

  protected abstract void selectProduct();
  protected abstract void deliver();

  protected boolean isGift() {
    return false;
  }

  public class OnlineOrderProcess extends OrderProcessTemplate {
    public OnlineOrderProcess() {
      super();
    }

    @Override
    protected void selectProduct() {
      System.out.println("Selecting product from online catalog.");
    }

    @Override
    protected void deliver() {
      System.out.println("Delivering product to the customer's address.");
    }

    @Override
    protected boolean isGift() {
      return true;
    }
  }

  private void wrapGift() {
    System.out.println("Gift wrapping completed.");
  }
}

public class StoreOrderProcess extends OrderProcessTemplate {

  public StoreOrderProcess() {
    super();
  }

  @Override
  protected void selectProduct() {
    System.out.println("Selecting product from the inventory.");
  }

  @Override
  protected void deliver() {
    System.out.println("Customer will pick up the product from the store.");
  }
}

 

Let’s take note of the role of the abstract class, which provides implementations for the processOrder and isGift methods, while on the other hand, it defines two abstract methods, selectProduct and deliver, which must be overridden in the subclass.

 

Combining Strategy and Template Method

If we combine the two previous examples to demonstrate the application of both patterns simultaneously, it might look like the diagram below.

 

Design Patterns - Class diagram 2

Figure 5 – Class diagram for the combined Strategy and Template Method patterns

 

 

Design Patterns - Sequence diagram 3

Figure 6 – Sequence diagram for the combined use case

 

If you’re curious about the code for the final solution, we’ve included the entire repository as an attachment.

 

Conclusion

Today, we’ve explained what design patterns are, their role, and why it’s worth using them. We’ve also presented a few implementations that will help you explore this topic further on your own. If you’re interested, feel free to check out the code or follow the links in the sources for more information. See you in the next post!

All the examples provided are primarily educational and do not fully represent real-world applications of these patterns, but they do help in understanding how they work and how they should be applied.

 

The code can be downloaded on our gitlab!

 

Sources:

https://mermaid.js.org/intro/

https://refactoring.guru/

See more on our blog:

Design Patterns

Design Patterns

Programmer, this article is for you! Grab a handful of useful information about design patterns.

Programming

Blockchain – Payments in the World of Cryptocurrencies

Blockchain – Payments in the World of Cryptocurrencies

Blockchain - discover the world of transactions, cryptocurrencies, and electronic payments.

FinancialSecurity

FastAPI – How to Build a Simple REST API in Python? – Part 3

FastAPI – How to Build a Simple REST API in Python? – Part 3

REST API using FastAPI framework. The last part of articles about API in Python. Start your FastAPI adventure with us today!

Programming