Practical Approach for Design Patterns

Learn how to implement structural design patterns for reusability and maintainability in modern development. In this blog, we code examples of Facade and Decorator patterns to optimize your software design.

GraphQL has a role beyond API Query Language- being the backbone of application Integration
background Coditation

Practical Approach for Design Patterns

Design patterns provide optimized and reusable solutions for frequently encountered programming challenges. They are not ready-made classes or libraries that can be plugged into a system, but rather blueprints that need to be adapted to suit specific requirements. These patterns can be implemented in different programming languages, depending on their capabilities, and they are not limited to one language. It's essential to keep in mind that while the wrong application of a design pattern can have undesirable effects when used appropriately, it can significantly enhance the performance of a system.

Benefits of design patterns:
It is important to remember that design patterns are not a magic cure that will solve all problems; rather, they are a tool to be used as part of your arsenal; they should be chosen carefully depending on the situation and adapted to the specific requirements of a project.

  • It acts as a blueprint, or common language so that when encountering common problems, developers can reuse the patterns in different contexts rather than reinventing the wheel every time a problem arises.
  • By providing a clear and consistent way to structure the code, design patterns can help make code more maintainable. This can make the code easier to understand and update in the future.
  • Design patterns can help to make code more scalable by allowing you to structure code in a way that allows it to handle an increasing amount of load or users.
  • By complying with well-established design patterns, the developer can use a solution that has already been tested and reviewed by the community, reducing the likelihood of introducing bugs and issues in the software and resulting in higher quality.

So in one word, design patterns provide us with reusabilitymaintainabilityflexibility, and scalability.

There are more than 23 patterns defined under the Gang of Four design pattern. It's recommended that you learn it through practice. We'll go over commonly used design patterns in depth and learn some examples of how they can be used.


Let us cover Structural design patterns.

Structural Design Patterns and Its Usages

What is a Structural Design Pattern? 

Patterns that deal with object composition and object relationships are known as structural design patterns. These patterns all revolve around connecting objects to form larger structures. Some of the most well-known structural designs

  • Facade Pattern

The facade pattern is a design pattern that creates a simpler interface for a complex system. It is often used to hide the complexity of a system behind a single, easy-to-use interface.
Suppose you have one complex system that has different subsystems underneath. so we can have one simplified interface to hide those implementations and expose only one interface.

Let’s see this with an example,
Let's create a complex system with three subsystems: CreateUser, SendUserMessage, and NotifyAdmin, as well as a function that instantiates all of these subsystems.


// Complex System struct which encloses different types of subsystems
type ComplexSystem struct {
	createUser      *CreateUser
	sendUserMessage *SendUserMessage
	notifyAdmin     *NotifyAdmin
}

//NewComplexSystem create 
func NewComplexSystem() *ComplexSystem {
	return &ComplexSystem{
		createUser:      &CreateUser{},
		sendUserMessage: &SendUserMessage{},
		notifyAdmin:     &NotifyAdmin{},
	}
}

type CreateUser struct{}
type SendUserMessage struct{}
type NotifyAdmin struct{}

Now, let us write receiver functions with the name "Call" that call the logic of each subsystem, as well as a single struct that encloses the object to the complex struct.


func (s CreateUser) Call() {
	fmt.Println("User Created")
}
func (s SendUserMessage) Call() {
	fmt.Println("Message sent to user")
}
func (s NotifyAdmin) Call() {
	fmt.Println("Notification sent to admin")
}
type ComplexSystemFacade struct {
	complexSystem *ComplexSystem 
}

Now that we've defined the subsystem and wrapped the complex system in a facade, let's write a single function that calls all of these methods. The scenario is that there is a system that creates a user when it calls createUser. Send a message to the user, then notify the admin that the user has been created. Now we'll write a single function that calls all of these methods, encapsulating all of the underlying implementations as well as the driver function for the code in the main block.


func (c ComplexSystemFacade) CreateUser() {
	c.complexSystem.createUser.Call()
	c.complexSystem.sendUserMessage.Call()
	c.complexSystem.notifyAdmin.Call()
}

func main() {
	facade := ComplexSystemFacade{
		complexSystem: NewComplexSystem(),
	}
	facade.CreateUser()
}

  • Decorator Pattern 

The decorator pattern is a structural design pattern that allows behaviour to be added statically or dynamically to an individual object without affecting the behaviour of other objects in the same class.
The decorator pattern is used to add new behaviour to an existing class by enclosing it in a decorator class. The decorator class is intended to implement the same interface as the original class, allowing it to be used as a drop-in replacement.
Let's look at a real-world coding example: I was working on a project where it was necessary to log metrics to new relics, so each time the function is called, I need to pass the context and metrics to new relics, so at the end of each function I was required to call the new relic APIs to log metrics, but the issue is that the interface has multiple implementations, resulting in redundant code for each function that implements the interface.
Let’s take an interface that has Method1() in it, and there will be two implementations for the interface with the same method. 


type IService interface {
	Method1()
}
type Service1 struct{}

func (s Service1) Method1() {
	fmt.Println("Service1 method called!")
}

type Service2 struct{}

func (s Service2) Method1() {
	fmt.Println("Service2 method called!")
}

Now, we can write decorator for two services and add logic on top of it


type ServiceDecorator struct {
	service IService
}

func (s ServiceDecorator) Method1() {
	fmt.Println("Decorator logic is called for ", reflect.TypeOf(s.service))
	s.service.Method1()
}

And finally, the main method is to call the decorator.


func main() {
	s1 := Service1{}
	serviceWithDecorator1 := ServiceDecorator{service: s1}
	serviceWithDecorator1.Method1()

	s2 := Service2{}
	serviceWithDecorator2 := ServiceDecorator{service: s2}
	serviceWithDecorator2.Method1()
}

So before calling the actual method, we have built up a pile of logic that is common across the objects that implement the IService method.

Conclusion

Finally, structural design patterns are an effective tool for software developers to use when designing and building systems. These patterns provide a common language as well as a set of best practices for organizing and structuring code to make it more reusable, maintainable, and extendable. The Adapter, Bridge, Composite, Decorator, and Facade patterns are among the most popular structural patterns. Each of these patterns addresses a specific set of challenges and can be used to improve the overall design of a system in a variety of situations. Developers can create systems that are more robust, scalable, and easy to understand and maintain by understanding and applying these patterns.

Hello, I am Akshay Navale, a highly driven and passionate software developer, avid learner, and tech enthusiast, always striving to do better. My passion for technology drives me to continuously learn and improve.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.

Latest Articles

Implementing Custom Instrumentation for Application Performance Monitoring (APM) Using OpenTelemetry

Application Performance Monitoring (APM) has become crucial for businesses to ensure optimal software performance and user experience. As applications grow more complex and distributed, the need for comprehensive monitoring solutions has never been greater. OpenTelemetry has emerged as a powerful, vendor-neutral framework for instrumenting, generating, collecting, and exporting telemetry data. This article explores how to implement custom instrumentation using OpenTelemetry for effective APM.

Mobile Engineering
time
5
 min read

Implementing Custom Evaluation Metrics in LangChain for Measuring AI Agent Performance

As AI and language models continue to advance at breakneck speed, the need to accurately gauge AI agent performance has never been more critical. LangChain, a go-to framework for building language model applications, comes equipped with its own set of evaluation tools. However, these off-the-shelf solutions often fall short when dealing with the intricacies of specialized AI applications. This article dives into the world of custom evaluation metrics in LangChain, showing you how to craft bespoke measures that truly capture the essence of your AI agent's performance.

AI/ML
time
5
 min read

Enhancing Quality Control with AI: Smarter Defect Detection in Manufacturing

In today's competitive manufacturing landscape, quality control is paramount. Traditional methods often struggle to maintain optimal standards. However, the integration of Artificial Intelligence (AI) is revolutionizing this domain. This article delves into the transformative impact of AI on quality control in manufacturing, highlighting specific use cases and their underlying architectures.

AI/ML
time
5
 min read