For several years, there’s been a notable trend towards streamlining applications and enhancing user interactions, even with intricate systems. But behind the scenes, achieving this seamless user experience often involves intricate algorithms and multiple API interactions. In this piece, I aim to shed light on a few development strategies to declutter your back-end code, making it more straightforward, maintainable, and possibly more enjoyable to work with.
- What is Mediator Pattern in C#? Mediator C# Definition
- Mediator Pattern c# Pros and Cons
- Why use the Mediator Pattern?
- When to use Mediator Pattern?
- Simplified example of system architecture for mediator pattern
- Challenges for mediator design pattern in C# in complex systems
- Service setup for organizing the communication in mediator pattern
- Example of mediator pattern code in C#
- Summary of implementation mediator pattern
What is mediator pattern c#? The C# Mediator Pattern is a design pattern primarily used to reduce the number of direct connections between classes, leading to increased flexibility and easier code maintenance. Its primary purpose is to promote loose coupling by centralizing external communications between classes within a single mediator object. This mediator design pattern c# gained popularity in the 1990s, especially with the release of the “Design Patterns” book by the Gang of Four. It’s particularly useful in scenarios where system components need to interact, but without knowing explicitly about each other, making the system more modular and easier to refactor or extend.
The Mediator Pattern offers several benefits, especially in C#. Firstly, it promotes loose coupling by ensuring that classes don’t communicate directly but through a mediator object, enhancing the maintainability and reusability of the code. By centralizing the interaction logic, changes in communication patterns or business rules can be encapsulated within the mediator, keeping individual components clean and focused. Moreover, it simplifies object protocols by introducing fewer dependencies among classes. On the downside, a poorly designed mediator can become a monolithic class that’s challenging to maintain, known colloquially as the “God Object” problem. Over-reliance on the mediator can also lead to decreased performance if too many objects defer their interactions through it. Lastly, adding new logic might require changing the mediator rather than individual components, which can become problematic in complex systems.
Why use Mediator Pattern in c#? The Mediator Pattern stands out as a solution to ensure efficient and organized communication between multiple classes or objects. One of its primary benefits is the reduction of intricate dependencies between classes, which in turn promotes a more maintainable and decoupled system. By centralizing external communications within a singular mediator object, it fosters clear responsibility divisions and reduces the chance of unintended side effects when one class changes. The pattern also offers more flexibility for extending and modifying systems since introducing new components or altering interactions typically involves updates to the mediator rather than many individual classes. Furthermore, by streamlining interactions through a common interface, the Mediator Pattern can lead to a cleaner and more consistent codebase, making the system easier to understand and debug. Overall, leveraging the Mediator Pattern can result in a more scalable, robust, and agile application architecture.
It happens that we search in Google for the phrase: “How to use mediator pattern c#?” or “How mediator works c#?”. The Mediator Pattern is particularly valuable in situations where a system comprises multiple classes or objects that need to interact, but you want to avoid tight coupling and complex interdependencies. It’s a go-to choice when you notice that a change in one class triggers a cascade of changes in several other classes. This pattern proves beneficial in GUI applications where multiple components need to interact without being directly linked. It’s also relevant in cases where you anticipate frequent changes in class interactions or when extending the system’s components. Employing the Mediator Pattern is wise when you aim to encapsulate the communication logic in a single place, offering a centralized point of control. However, it’s essential to gauge the system’s complexity beforehand, as introducing a mediator in overly simple systems can add unnecessary layers and complexity.
A more straightforward representation of an intricate system might resemble the diagram shown below. We have a device or robot that can transmit telemetry data about its health, operational conditions, or environment. Conversely, there’s a user application that offers valuable insights, potentially aiding a device operator in its management.
From the device’s viewpoint, the process is:
- The robot transmits a telemetry message.
- The Telemetry Data Processor takes on the task of validating this message. To ensure its accuracy, it consults the Device Types Registry to confirm the existence of a matching type.
From the application’s standpoint, the process unfolds as:
- The application intends to showcase specific device type details.
- To retrieve this information, the application sends a request to the Device Type Registry.
Typically in these systems, there exists a common functionality that various receivers can access, likely through diverse communication methods. Moreover, similar to the illustrated diagram, accessing specific functionalities often demands distinct security measures. Ultimately, though, calls to both the Internal API and External API lead to the same data repository, function within the identical device types domain, and utilize consistent models and filtering features.
A strategy for structuring communication in such systems might be:
- For the Internal API, adopt GRPC-based communication. The GRPC protocol is gaining traction due to its efficiency and compact nature, making it well-suited for service-to-service interactions.
- For the External API, rely on the universally recognized REST-based communication.
Clearly, this methodology demands distinct infrastructure and communication stacks. However, the aim should be to keep these infrastructure layers minimalistic, ensuring business logic doesn’t permeate into these communicative segments, all while maintaining non-redundant, modular, and testable code.
A potential and streamlined solution could be the application of the mediator pattern. This pattern, categorized under behavioral patterns, empowers developers to determine how various objects communicate without necessitating direct references between them. Such a strategy promotes a more flexible architecture, autonomous development of separate classes, and efficient component reusability.
The central elements of the strategy illustrated in the previous diagram include:
- Implementing an API-specific infrastructure layer. This could be an ASP.NET Core Web API configuration complemented by certain middleware and a few controller classes, or it might be a GRPC “.proto” file alongside auto-generated service classes.
- Considering the optional incorporation of an API-specific Response Mapping component. This becomes relevant when adjustments to protocol-specific response elements are needed (e.g., http response status codes). In its most basic form, this layer can also address response content mapping, especially when a specific API shouldn’t reveal every segment of a model that might emerge from the execution of a Request Handler.
- Employing the Mediator and Request Handler. The handler emerges as the linchpin of the depicted strategy. It’s the repository of the core business logic. Notably, it remains unaffected by infrastructure or framework, and is reusable, easy to test, and straightforward to maintain.
Here you can find a Mediator Pattern Example (mediator c# example).
A few noteworthy points regarding the discussion:
- The examples provided are in C#. They leverage tools and packages from the .NET ecosystem. However, given today’s abundance of technologies and tools, I’m confident that adopting a similar strategy would be just as straightforward using an entirely different technological framework.
- A pivotal element of this setup is the utilization of components from Jimmy Bogard’s MediatR library. This library is a manifestation of the previously mentioned mediator pattern and serves as the binding agent for the entire solution, seamlessly integrating various components.
- Both the controller class and the GRPC service class are primarily dependent on the IMediator interface. This dependency is likely to remain consistent even when new requirements emerge. To illustrate, implementing a feature like updating a device type would necessitate defining relevant request/response models, creating a handler, and adding an analogous method to the existing ‘Get’ within the controller and/or GRPC service.
- The precise steps to retrieve the data are concealed within the handler, which possesses all essential dependencies. Moreover, should this particular process evolve, any modifications will be confined to this singular class.
Adopting this method of code structuring can offer developers a plethora of advantages. In several extensive projects, constructed on various microservice orchestrators featuring diverse APIs, employing a strategy akin to the one shown enabled my colleagues and me to navigate without getting overwhelmed by a multitude of interfaces, numerous public and private methods, and consistently evolving method signatures. Due to the robust capabilities of our chosen tech stack, our primary focus was predominantly on the actual business needs.
Of course, some might argue that instead of grappling with a myriad of service-specific interfaces, we simply ended up with a plethora of compact, specialized classes. While this observation holds merit, the antidote often lies in appropriate component naming. However, as some in the field might agree, naming is one of the most challenging facets of programming, meriting its own dedicated discussion.