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 embeddings in LlamaIndex for domain-specific information retrieval

Discover how to dramatically improve search relevance in specialized domains by implementing custom embeddings in LlamaIndex. This comprehensive guide walks through four practical approaches—from fine-tuning existing models to creating knowledge-enhanced embeddings—with real-world code examples. Learn how domain-specific embeddings can boost precision by 30-45% compared to general-purpose models, as demonstrated in a legal tech case study where search precision jumped from 67% to 89%.

time
15
 min read

Optimizing Databricks Spark jobs using dynamic partition pruning and AQE

Learn how to supercharge your Databricks Spark jobs using Dynamic Partition Pruning (DPP) and Adaptive Query Execution (AQE). This comprehensive guide walks through practical implementations, real-world scenarios, and best practices for optimizing large-scale data processing. Discover how to significantly reduce query execution time and resource usage through intelligent partition handling and runtime optimizations. Perfect for data engineers and architects looking to enhance their Spark job performance in Databricks environments.

time
8
 min read

Implementing custom serialization and deserialization in Apache Kafka for optimized event processing performance

Dive deep into implementing custom serialization and deserialization in Apache Kafka to optimize event processing performance. This comprehensive guide covers building efficient binary serializers, implementing buffer pooling for reduced garbage collection, managing schema versions, and integrating compression techniques. With practical code examples and performance metrics, learn how to achieve up to 65% higher producer throughput, 45% better consumer throughput, and 60% reduction in network bandwidth usage. Perfect for developers looking to enhance their Kafka implementations with advanced serialization strategies.

time
11
 min read