01
FebDesign Patterns in DotNet
Understanding Design Patterns in .NET
Design patterns in .NET provide reusable solutions to common software design problems, making development more efficient and maintainable. These patterns help structure code, improve scalability, and enhance flexibility in applications. But what exactly are design patterns in .NET? They are proven coding techniques that help developers write clean, organized, and efficient code.
In this .NET tutorial, you’ll learn the importance of design patterns in .NET, their key benefits, and how they improve software architecture. We’ll explore different types of patterns, including creational, structural, and behavioral. Ready to understand how design patterns can enhance your .NET development skills? Let’s dive in!
What are Design Patterns in .NET?
Let's understand design patterns in .NET with simple points:
- A design pattern in .NET is a reusable solution to common software design problems that developers face.
- It is not a complete code that you can directly use. Instead, it provides a structured approach to solve problems effectively.
- You can think of it like a blueprint that guides you to create flexible and maintainable software.
- Design patterns work like cooking recipes. They help you get consistent results without reinventing the wheel every time.
Read More: |
Different Types of Software Design Principles |
What is an IoC Container or DI Container |
Why is the Design Pattern important?
Design patterns in .NET help you write structured, scalable, and efficient code. They provide best practices to solve challenges systematically and elegantly.
- Better Code Readability: Other developers can easily understand your code.
- Faster Development: You don’t have to reinvent solutions, making coding quicker.
- Less Complexity: Patterns help keep your code well-organized and simple.
- Easy Refactoring: Your code stays modular, so future changes become easy.
- Promotes Best Practices: Design patterns encourage structured and disciplined coding.
Let’s see why design patterns in .NET are important with an example:
Problem
Imagine you are building an e-commerce checkout system. Users can apply different types of discounts, such as percentage-based, fixed amount, or no discount. Over time, new discount types may be added. How do you design this system in a way that makes adding new discounts easy without modifying existing code?
Solution
The Strategy Design Pattern is perfect here. It allows you to create different discount strategies and switch between them dynamically.
Types of Software Design Patterns in .NET
- Creational Design Pattern in .NET
- Structural Design Pattern in .NET
- Behavioral Design Pattern in .NET
Creational Design Pattern in .NET
- Creational design patterns help you manage object creation efficiently.
- They improve flexibility and efficiency in how objects are created.
- Instead of directly instantiating objects, they provide better ways to construct them.
- With creational design patterns, you keep object creation in one place, making the code more manageable.
- This approach ensures that how objects are created does not interfere with the system’s overall design.
There are five types of creational design patterns in .NET:
- Singleton Design Pattern
- Factory Method Design Pattern
- Abstract Factory Design Pattern
- Builder Design Pattern
- Prototype Design Pattern
Let's start with the Singleton design pattern.
1. Singleton Design Pattern
- Singleton Design Pattern ensures that only one instance of a class exists in the application.
- It is useful when you need a single shared object, such as a configuration manager.
Example
using System;
// Singleton class
public class ScholarHatSingleton
{
private static ScholarHatSingleton instance;
private static readonly object lockObj = new object();
// Private constructor prevents instantiation
private ScholarHatSingleton() { }
// Public method to provide a single instance
public static ScholarHatSingleton GetInstance()
{
if (instance == null)
{
lock (lockObj)
{
if (instance == null)
{
instance = new ScholarHatSingleton();
}
}
}
return instance;
}
// Method to display a message
public void ShowMessage()
{
Console.WriteLine("Welcome to ScholarHat Singleton!");
}
}
// Main class
public class SingletonExample
{
public static void Main()
{
// Getting the single instance
ScholarHatSingleton singleton = ScholarHatSingleton.GetInstance();
// Displaying the message
singleton.ShowMessage();
}
}
Output
Welcome to ScholarHat Singleton!
Explanation
- This code follows the Singleton design pattern. The ScholarHatSingleton class ensures that only one instance is created.
- It provides a global access point using the GetInstance() method.
- The ShowMessage() method displays a message to confirm the singleton instance.
- The Main class gets the singleton instance and calls ShowMessage().
2. Factory Method Design Pattern
The Factory Method Design Pattern is a creational design pattern that defines a method for creating objects but lets subclasses decide which class to instantiate. It’s perfect when you need to create objects from a family of related classes without specifying their exact types.
Imagine you’re building a system that generates different types of documents (e.g., text, spreadsheet). The Factory Method pattern can help you create these documents without tightly coupling your code to specific document classes.
Example: Factory Method in .NET
// Product Interface
public interface IDocument {
void Generate();
}
// Concrete Products
public class TextDocument : IDocument {
public void Generate() {
Console.WriteLine("Generating a Text Document.");
}
}
public class SpreadsheetDocument : IDocument {
public void Generate() {
Console.WriteLine("Generating a Spreadsheet Document.");
}
}
// Factory Class
public class DocumentFactory {
public static IDocument CreateDocument(string documentType) {
if (documentType.Equals("TEXT", StringComparison.OrdinalIgnoreCase)) {
return new TextDocument();
} else if (documentType.Equals("SPREADSHEET", StringComparison.OrdinalIgnoreCase)) {
return new SpreadsheetDocument();
}
return null;
}
}
// Main Class
class Program {
static void Main(string[] args) {
// Creating Text Document using Factory
IDocument textDocument = DocumentFactory.CreateDocument("TEXT");
textDocument.Generate();
// Creating Spreadsheet Document using Factory
IDocument spreadsheetDocument = DocumentFactory.CreateDocument("SPREADSHEET");
spreadsheetDocument.Generate();
}
}
Output
Generating a Text Document.
Generating a Spreadsheet Document.
Explanation
- The IDocument interface defines the Generate method, which is implemented by concrete classes like TextDocument and SpreadsheetDocument.
- The DocumentFactory class is responsible for creating instances of these concrete classes based on the input.
- This pattern allows you to create objects without knowing their specific types, making your code more flexible and easier to maintain.
3. Abstract Factory Design Pattern
The Abstract Factory Design Pattern provides an interface for creating families of related objects without specifying their concrete classes. It’s ideal when you need to ensure that the objects you create work well together.
For example, if you’re building a UI framework, you might need to create buttons and text boxes that are consistent with a specific theme (e.g., Light Theme, Dark Theme). The Abstract Factory pattern can help you achieve this.
Example: Abstract Factory in .NET
// Abstract Product Interfaces
public interface IButton {
void Render();
}
public interface ITextBox {
void Display();
}
// Concrete Products for Light Theme
public class LightButton : IButton {
public void Render() {
Console.WriteLine("Rendering a Light Theme Button.");
}
}
public class LightTextBox : ITextBox {
public void Display() {
Console.WriteLine("Displaying a Light Theme TextBox.");
}
}
// Concrete Products for Dark Theme
public class DarkButton : IButton {
public void Render() {
Console.WriteLine("Rendering a Dark Theme Button.");
}
}
public class DarkTextBox : ITextBox {
public void Display() {
Console.WriteLine("Displaying a Dark Theme TextBox.");
}
}
// Abstract Factory Interface
public interface IThemeFactory {
IButton CreateButton();
ITextBox CreateTextBox();
}
// Concrete Factories
public class LightThemeFactory : IThemeFactory {
public IButton CreateButton() {
return new LightButton();
}
public ITextBox CreateTextBox() {
return new LightTextBox();
}
}
public class DarkThemeFactory : IThemeFactory {
public IButton CreateButton() {
return new DarkButton();
}
public ITextBox CreateTextBox() {
return new DarkTextBox();
}
}
// Main Class
class Program {
static void Main(string[] args) {
// Creating Light Theme Factory
IThemeFactory lightFactory = new LightThemeFactory();
IButton lightButton = lightFactory.CreateButton();
ITextBox lightTextBox = lightFactory.CreateTextBox();
lightButton.Render();
lightTextBox.Display();
// Creating Dark Theme Factory
IThemeFactory darkFactory = new DarkThemeFactory();
IButton darkButton = darkFactory.CreateButton();
ITextBox darkTextBox = darkFactory.CreateTextBox();
darkButton.Render();
darkTextBox.Display();
}
}
Output
Rendering a Light Theme Button.
Displaying a Light Theme TextBox.
Rendering a Dark Theme Button.
Displaying a Dark Theme TextBox.
Explanation
- The IButton and ITextBox interfaces define the methods for rendering UI components.
- Concrete classes like LightButton, LightTextBox, DarkButton, and DarkTextBox implement these interfaces.
- The IThemeFactory interface provides methods for creating buttons and text boxes, while LightThemeFactory and DarkThemeFactory are concrete factories that produce these components.
- This pattern ensures that the components you create are consistent with a specific theme, making your code more modular and easier to extend.
4. Builder Design Pattern in .NET
The Builder Design Pattern helps you create complex objects step by step. It separates the construction process from the object structure, making your code clean and easy to manage. When you have objects with many optional parts, this pattern helps you build them efficiently.
Example
// Product Class
public class ScholarHatCourse {
public string Name { get; }
public string Duration { get; }
public string Level { get; }
// Private Constructor
private ScholarHatCourse(CourseBuilder builder) {
Name = builder.Name;
Duration = builder.Duration;
Level = builder.Level;
}
// Builder Class
public class CourseBuilder {
public string Name { get; private set; }
public string Duration { get; private set; }
public string Level { get; private set; }
public CourseBuilder SetName(string name) {
Name = name;
return this;
}
public CourseBuilder SetDuration(string duration) {
Duration = duration;
return this;
}
public CourseBuilder SetLevel(string level) {
Level = level;
return this;
}
public ScholarHatCourse Build() {
return new ScholarHatCourse(this);
}
}
}
// Main Class
class Program {
static void Main() {
// Creating a ScholarHat course using the Builder Pattern
var dotnetCourse = new ScholarHatCourse.CourseBuilder()
.SetName(".NET Development")
.SetDuration("5 months")
.SetLevel("Intermediate")
.Build();
Console.WriteLine("Course Name: " + dotnetCourse.Name);
Console.WriteLine("Duration: " + dotnetCourse.Duration);
Console.WriteLine("Level: " + dotnetCourse.Level);
}
}
Output
Course Name: .NET Development
Duration: 5 months
Level: Intermediate
Explanation
- This code uses the Builder Design Pattern to create objects in a structured way.
- The ScholarHatCourse class has a private constructor, so you must use the builder to create an instance.
- The CourseBuilder class provides methods to set values for properties and then build the final object.
- In the Main class, we use the builder to create a course and print its details.
5. Prototype Design Pattern in .NET
- Prototype Design Pattern helps you create new objects by copying an existing object instead of building from scratch.
- It is useful when creating objects that are expensive or complex. Copying an existing object is faster and more efficient.
Example
// Abstract Prototype Class
public abstract class ScholarHatCourse
{
public string CourseName { get; set; }
public string CourseLevel { get; set; }
// Clone method to create a copy
public abstract ScholarHatCourse Clone();
// Display course details
public void ShowCourseDetails()
{
Console.WriteLine($"Course Name: {CourseName}");
Console.WriteLine($"Course Level: {CourseLevel}");
}
}
// Concrete Prototype Class for C# Course
public class CSharpCourse : ScholarHatCourse
{
public CSharpCourse()
{
CourseName = "C# Programming";
CourseLevel = "Intermediate";
}
public override ScholarHatCourse Clone()
{
return (ScholarHatCourse)this.MemberwiseClone();
}
}
// Concrete Prototype Class for .NET Course
public class DotNetCourse : ScholarHatCourse
{
public DotNetCourse()
{
CourseName = ".NET Development";
CourseLevel = "Beginner";
}
public override ScholarHatCourse Clone()
{
return (ScholarHatCourse)this.MemberwiseClone();
}
}
// Main Program
class Program
{
static void Main()
{
// Creating original C# course
CSharpCourse csharpCourse = new CSharpCourse();
csharpCourse.ShowCourseDetails();
// Cloning the C# course
ScholarHatCourse clonedCSharpCourse = csharpCourse.Clone();
clonedCSharpCourse.ShowCourseDetails();
// Creating original .NET course
DotNetCourse dotNetCourse = new DotNetCourse();
dotNetCourse.ShowCourseDetails();
// Cloning the .NET course
ScholarHatCourse clonedDotNetCourse = dotNetCourse.Clone();
clonedDotNetCourse.ShowCourseDetails();
}
}
Output
Course Name: C# Programming
Course Level: Intermediate
Course Name: C# Programming
Course Level: Intermediate
Course Name: .NET Development
Course Level: Beginner
Course Name: .NET Development
Course Level: Beginner
Explanation
- This program follows the Prototype Design Pattern.
- ScholarHatCourse defines an abstract Clone() method and has properties like CourseName and CourseLevel.
- CSharpCourse and DotNetCourse extend ScholarHatCourse and implement Clone(), allowing object duplication.
- The Main method creates original courses and their clones, proving that objects can be copied without relying on their concrete classes.
2. Structural Design Patterns in .NET
Structural design patterns help you organize and manage object interactions in a flexible and reusable way. They make your code more maintainable by defining simple relationships between classes and objects.
There are seven types of structural design patterns:
- Adapter Design Pattern
- Bridge Design Pattern
- Composite Design Pattern
- Decorator Design Pattern
- Facade Design Pattern
- Flyweight Design Pattern
- Proxy Design Pattern
1. Adapter Design Pattern
- Adapter Design Pattern lets you connect two incompatible interfaces so they can work together.
- Think of it like a power adapter that allows you to plug a foreign device into your local socket.
Example in .NET (C#)
// Target Interface
public interface IScholarHatCourse {
void GetCourseDetails();
}
// Adaptee Class (Existing Class)
public class PythonCourseDetails {
public void ShowCourseDetails() {
Console.WriteLine("ScholarHat offers a comprehensive Python course.");
}
}
// Adapter Class
public class PythonCourseAdapter : IScholarHatCourse {
private PythonCourseDetails _pythonCourseDetails;
public PythonCourseAdapter(PythonCourseDetails pythonCourseDetails) {
_pythonCourseDetails = pythonCourseDetails;
}
public void GetCourseDetails() {
_pythonCourseDetails.ShowCourseDetails();
}
}
// Main Class
class Program {
static void Main() {
// Existing Python course details (Adaptee)
PythonCourseDetails pythonDetails = new PythonCourseDetails();
// Adapter makes PythonCourseDetails compatible with IScholarHatCourse interface
IScholarHatCourse courseAdapter = new PythonCourseAdapter(pythonDetails);
// Using the adapter to get course details
courseAdapter.GetCourseDetails();
}
}
Output
ScholarHat offers a comprehensive Python course.
Explanation
- This code follows the Adapter Design Pattern.
- The client expects to use the IScholarHatCourse interface.
- The existing PythonCourseDetails class (adaptee) has a method to display course details but doesn’t follow the required interface.
- The PythonCourseAdapter acts as a bridge, making PythonCourseDetails work with IScholarHatCourse.
- The Program class shows how the adapter makes everything work smoothly.
Want to learn more? Check out other .NET Design Patterns.
2. Bridge Design Pattern in .NET
The Bridge Design Pattern helps separate an object’s abstraction from its implementation. This way, both can evolve independently. Think of it like a remote control and a TV. The remote (abstraction) works independently of the TV’s brand or model (implementation).
Example
// Implementor Interface
interface ICourseContent {
void GetContent();
}
// Concrete Implementor 1
class VideoContent : ICourseContent {
public void GetContent() {
Console.WriteLine("ScholarHat provides video content for the course.");
}
}
// Concrete Implementor 2
class ArticleContent : ICourseContent {
public void GetContent() {
Console.WriteLine("ScholarHat provides article content for the course.");
}
}
// Abstraction
abstract class ScholarHatCourse {
protected ICourseContent content;
public ScholarHatCourse(ICourseContent content) {
this.content = content;
}
public abstract void ShowCourseDetails();
}
// Refined Abstraction 1
class JavaCourse : ScholarHatCourse {
public JavaCourse(ICourseContent content) : base(content) {}
public override void ShowCourseDetails() {
Console.WriteLine("Java Programming Course:");
content.GetContent();
}
}
// Refined Abstraction 2
class PythonCourse : ScholarHatCourse {
public PythonCourse(ICourseContent content) : base(content) {}
public override void ShowCourseDetails() {
Console.WriteLine("Python Programming Course:");
content.GetContent();
}
}
// Main Class
class Program {
static void Main() {
// Creating a Java course with video content
ScholarHatCourse javaCourse = new JavaCourse(new VideoContent());
javaCourse.ShowCourseDetails();
// Creating a Python course with article content
ScholarHatCourse pythonCourse = new PythonCourse(new ArticleContent());
pythonCourse.ShowCourseDetails();
}
}
Output
Java Programming Course:
ScholarHat provides video content for the course.
Python Programming Course:
ScholarHat provides article content for the course.
Explanation
- You are using the Bridge Design Pattern to separate course structure from content type.
- ICourseContent is an interface for different content types like VideoContent and ArticleContent.
- ScholarHatCourse is an abstraction that holds a reference to a content type.
- JavaCourse and PythonCourse extend ScholarHatCourse and use different content implementations.
- This makes it easy to add new courses or content types without changing existing code!
3. Composite Design Pattern in .NET
Composite Design Pattern helps you work with tree structures where objects can be treated as individuals or groups. With this pattern, you can handle single objects and collections in the same way. Sounds simple, right?
Example
using System;
using System.Collections.Generic;
// Component Interface
interface ICourse {
void ShowCourseDetails();
}
// Leaf Class
class SingleCourse : ICourse {
private string _courseName;
public SingleCourse(string courseName) {
_courseName = courseName;
}
public void ShowCourseDetails() {
Console.WriteLine("Course: " + _courseName);
}
}
// Composite Class
class CourseBundle : ICourse {
private List _courses = new List();
public void AddCourse(ICourse course) {
_courses.Add(course);
}
public void RemoveCourse(ICourse course) {
_courses.Remove(course);
}
public void ShowCourseDetails() {
foreach (var course in _courses) {
course.ShowCourseDetails();
}
}
}
// Main Class
class Program {
static void Main() {
// Single courses (Leafs)
ICourse csharpCourse = new SingleCourse("C# Programming");
ICourse dotnetCourse = new SingleCourse(".NET Development");
ICourse sqlCourse = new SingleCourse("SQL Database");
// Course bundle (Composite)
CourseBundle devBundle = new CourseBundle();
devBundle.AddCourse(csharpCourse);
devBundle.AddCourse(dotnetCourse);
// Full bundle including development and database courses
CourseBundle fullBundle = new CourseBundle();
fullBundle.AddCourse(devBundle);
fullBundle.AddCourse(sqlCourse);
// Display details of the full bundle
Console.WriteLine("ScholarHat Full Course Bundle:");
fullBundle.ShowCourseDetails();
}
}
Output
ScholarHat Full Course Bundle:
Course: C# Programming
Course: .NET Development
Course: SQL Database
Explanation
- You just saw the Composite Design Pattern in action.
- The ICourse interface defines the blueprint.
- SingleCourse handles individual courses.
- CourseBundle groups multiple courses together, letting you treat both single courses and bundles in the same way.
Want to dive deeper? Check out our complete design patterns guide!
4. Decorator Design Pattern
Decorator Design Pattern allows you to add extra responsibilities to an object at runtime without changing its structure. This pattern is perfect when you want to enhance or modify the behavior of an object without altering its code.
Example
// Base Interface
public interface ICourse {
string GetDescription();
double GetCost();
}
// Concrete Component
public class BasicCourse : ICourse {
// Implementing the methods
public string GetDescription() {
return "Basic Course";
}
public double GetCost() {
return 100.00;
}
}
// Decorator
public abstract class CourseDecorator : ICourse {
protected ICourse course;
// Constructor to assign ICourse instance
public CourseDecorator(ICourse course) {
this.course = course;
}
public string GetDescription() {
return course.GetDescription();
}
public double GetCost() {
return course.GetCost();
}
}
// Concrete Decorator 1
public class PremiumContentDecorator : CourseDecorator {
// Constructor to pass ICourse instance
public PremiumContentDecorator(ICourse course) {
base(course);
}
// Override the methods
public string GetDescription() {
return course.GetDescription() + " with Premium Content";
}
public double GetCost() {
return course.GetCost() + 50.00;
}
}
// Main Class
public class DecoratorPatternExample {
public static void Main(string[] args) {
ICourse course = new BasicCourse();
Console.WriteLine("Description: " + course.GetDescription());
Console.WriteLine("Cost: $" + course.GetCost());
ICourse premiumCourse = new PremiumContentDecorator(course);
Console.WriteLine("\nDescription: " + premiumCourse.GetDescription());
Console.WriteLine("Cost: $" + premiumCourse.GetCost());
}
}
Output
Description: Basic Course
Cost: $100.0
Description: Basic Course with Premium Content
Cost: $150.0
Explanation
- This example demonstrates how to use the Decorator Pattern in .NET to add functionality to objects dynamically.
- The BasicCourse class represents a basic course, while PremiumContentDecorator adds extra premium content and modifies the cost.
- Notice how decorators extend an object's behavior without changing the original class. Isn't that amazing?
5. Facade Design Pattern
Facade Design Pattern simplifies complex systems by providing a unified interface, making it easier to interact with. This pattern acts like a front desk that manages communication with a complex backend, shielding you from its complexity.
Example
// Subsystem Classes
public class CourseRegistration {
public void RegisterCourse(string courseName) {
Console.WriteLine("Registering for course: " + courseName);
}
}
public class PaymentProcessing {
public void ProcessPayment(double amount) {
Console.WriteLine("Processing payment of $" + amount);
}
}
public class Certification {
public void IssueCertificate(string courseName) {
Console.WriteLine("Issuing certificate for course: " + courseName);
}
}
// Facade
public class CourseFacade {
private CourseRegistration registration;
private PaymentProcessing payment;
private Certification certification;
public CourseFacade() {
registration = new CourseRegistration();
payment = new PaymentProcessing();
certification = new Certification();
}
public void EnrollInCourse(string courseName, double amount) {
registration.RegisterCourse(courseName);
payment.ProcessPayment(amount);
certification.IssueCertificate(courseName);
}
}
// Main Class
public class FacadePatternExample {
public static void Main(string[] args) {
CourseFacade facade = new CourseFacade();
facade.EnrollInCourse("Java Programming", 100.00);
}
}
Output
Registering for course: Java Programming
Processing payment of $100.0
Issuing certificate for course: Java Programming
Explanation
- This example shows the Facade Pattern, which simplifies how you interact with complex subsystems.
- The CourseFacade class provides a simple interface to register for a course, process payments, and issue certificates. Isn't that cool? You only need one method call to do everything!
6. Flyweight Design Pattern
The Flyweight Design Pattern reduces memory usage by sharing data across similar objects. This pattern comes in handy when you are working with many similar objects, as it minimizes object creation and saves memory.
Example
using System;
using System.Collections.Generic;
// Flyweight Interface
public interface ICourse {
void Display(string studentName);
}
// Concrete Flyweight
public class ConcreteCourse : ICourse {
private string courseName;
public ConcreteCourse(string courseName) {
this.courseName = courseName;
}
public void Display(string studentName) {
Console.WriteLine($"Course: {courseName} for Student: {studentName}");
}
}
// Flyweight Factory
public class CourseFactory {
private Dictionary courseMap = new Dictionary();
public ICourse GetCourse(string courseName) {
if (!courseMap.ContainsKey(courseName)) {
courseMap[courseName] = new ConcreteCourse(courseName);
}
return courseMap[courseName];
}
}
// Main Class
public class FlyweightPatternExample {
public static void Main(string[] args) {
CourseFactory factory = new CourseFactory();
ICourse javaCourse = factory.GetCourse("Java Programming");
javaCourse.Display("Alice");
ICourse pythonCourse = factory.GetCourse("Python Programming");
pythonCourse.Display("Bob");
ICourse javaCourse2 = factory.GetCourse("Java Programming");
javaCourse2.Display("Charlie");
}
}
Output
Course: Java Programming for Student: Alice
Course: Python Programming for Student: Bob
Course: Java Programming for Student: Charlie
Explanation
- This example demonstrates the Flyweight Pattern, which saves memory by sharing objects instead of creating new ones each time.
- The CourseFactory class manages a pool of ConcreteCourse objects, creating them only when necessary.
- By reusing existing objects for the same course, it reduces redundancy and improves efficiency. Isn’t that a smart way to handle multiple similar objects?
7. Proxy Design Pattern
The Proxy Design Pattern gives you a way to control access to an object by using a placeholder or surrogate. This pattern is perfect for managing resource access, adding security, or improving performance by delaying expensive tasks until they're absolutely needed.
Here’s how the structure looks for implementing the Proxy Design Pattern in .NET:
Example
using System;
// Subject Interface
public interface ICourse
{
void Enroll();
}
// Real Subject
public class RealCourse : ICourse
{
private string courseName;
public RealCourse(string courseName)
{
this.courseName = courseName;
}
public void Enroll()
{
Console.WriteLine($"Enrolling in course: {courseName}");
}
}
// Proxy
public class CourseProxy : ICourse
{
private RealCourse realCourse;
private string courseName;
public CourseProxy(string courseName)
{
this.courseName = courseName;
}
public void Enroll()
{
if (realCourse == null)
{
realCourse = new RealCourse(courseName);
}
realCourse.Enroll();
}
}
// Main Class
public class ProxyPatternExample
{
public static void Main(string[] args)
{
ICourse course = new CourseProxy("Java Programming");
course.Enroll(); // RealCourse is created and enrolled in
}
}
Output
Enrolling in course: Java Programming
Explanation
- This example showcases the Proxy Pattern, where access to an object is controlled via a surrogate object.
- The CourseProxy class doesn’t create the RealCourse instance right away. Instead, it waits until the Enroll method is called. This way, you manage resource usage and add an extra layer of control.
- Isn’t it smart to delay creation and control resource-heavy operations in this way? It’s like having a guard that only lets you in when you’re truly ready.
3. Behavioral Design Patterns in .NET
Behavioral design patterns focus on how objects interact and communicate with each other in software development.These patterns help manage communication between objects and ensure they work together to perform specific tasks.
Here are some common behavioral patterns:
- Observer Pattern
- Strategy Pattern
- Command Pattern
- State Pattern
- Template Method Pattern
- Chain of Responsibility Pattern
- Mediator Pattern
- Memento Pattern
1. Observer Design Pattern
Observer Design Pattern allows an object to notify other objects when its state changes. This is useful when you want certain parts of your application to react to changes without directly coupling the parts together. For example, imagine a news feed where subscribers get updates whenever new articles are posted.
Example
using System;
using System.Collections.Generic;
public interface ICourseSubject {
void AddObserver(ICourseObserver observer);
void RemoveObserver(ICourseObserver observer);
void NotifyObservers();
}
public class Course : ICourseSubject {
private List observers = new List();
private string courseName;
public Course(string courseName) {
this.courseName = courseName;
}
public void AddObserver(ICourseObserver observer) {
observers.Add(observer);
}
public void RemoveObserver(ICourseObserver observer) {
observers.Remove(observer);
}
public void NotifyObservers() {
foreach (ICourseObserver observer in observers) {
observer.Update(courseName);
}
}
public void ChangeCourseName(string newCourseName) {
this.courseName = newCourseName;
NotifyObservers();
}
}
public interface ICourseObserver {
void Update(string courseName);
}
public class Student : ICourseObserver {
private string studentName;
public Student(string studentName) {
this.studentName = studentName;
}
public void Update(string courseName) {
Console.WriteLine($"{studentName} received update: {courseName}");
}
}
public class ObserverPatternExample {
public static void Main(string[] args) {
Course course = new Course("Java Programming");
Student student1 = new Student("Alice");
Student student2 = new Student("Bob");
course.AddObserver(student1);
course.AddObserver(student2);
course.ChangeCourseName("Advanced Java Programming");
}
}
Output
Alice received update: Advanced Java Programming
Bob received update: Advanced Java Programming
Explanation
- This example introduces the Observer Pattern, where the Course class notifies all registered Student instances when its name changes.
- When the Course name is updated, the system automatically informs all observers without requiring direct interaction.
- This pattern allows loose coupling, making the system more flexible to changes.
2. Strategy Design Pattern
Strategy Design Pattern lets you choose an algorithm during runtime without changing the code that uses it. Ever thought about multiple ways to do the same thing? Like choosing different sorting methods based on your needs? That's where this pattern shines. It gives you the flexibility to pick the best algorithm dynamically.
Read More: |
Data Structures Sorting: Types and Examples Explained |
Sort in Python- An Easy Way to Learn |
Example
// Strategy Interface
public interface IPaymentStrategy {
void Pay(double amount);
}
// Concrete Strategies
public class CreditCardPayment : IPaymentStrategy {
public void Pay(double amount) {
Console.WriteLine($"Paid ${amount} using Credit Card.");
}
}
public class PayPalPayment : IPaymentStrategy {
public void Pay(double amount) {
Console.WriteLine($"Paid ${amount} using PayPal.");
}
}
// Context
public class ShoppingCart {
private IPaymentStrategy paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy strategy) {
paymentStrategy = strategy;
}
public void Checkout(double amount) {
paymentStrategy.Pay(amount);
}
}
// Main Class
public class StrategyPatternExample {
public static void Main(string[] args) {
ShoppingCart cart = new ShoppingCart();
cart.SetPaymentStrategy(new CreditCardPayment());
cart.Checkout(100.00);
cart.SetPaymentStrategy(new PayPalPayment());
cart.Checkout(200.00);
}
}
Output
Paid $100.0 using Credit Card.
Paid $200.0 using PayPal.
Explanation
- This example shows the Strategy Pattern, where you can switch between different algorithms (in this case, payment methods) without altering the rest of the code.
- The ShoppingCart class can easily change between CreditCardPayment and PayPalPayment strategies, making it flexible and adaptable to your needs.
- This approach keeps your code clean and extensible, allowing you to add new payment methods without changing the entire shopping cart logic.
3. Command Design Pattern
Command Design Pattern turns simple requests or operations into objects, making them easier to execute, undo, or queue. Ever needed to keep track of actions, like undoing your last move in a text editor? This pattern is perfect for those scenarios!
Example
// Command Interface
public interface ICommand {
void Execute();
}
// Concrete Command
public class EnrollInCourseCommand : ICommand {
private string courseName;
public EnrollInCourseCommand(string courseName) {
this.courseName = courseName;
}
public void Execute() {
Console.WriteLine($"Enrolled in course: {courseName}");
}
}
// Invoker
public class CourseRegistration {
private ICommand command;
public void SetCommand(ICommand command) {
this.command = command;
}
public void PerformAction() {
command.Execute();
}
}
// Main Class
public class CommandPatternExample {
public static void Main(string[] args) {
ICommand enrollCommand = new EnrollInCourseCommand("Java Programming");
CourseRegistration registration = new CourseRegistration();
registration.SetCommand(enrollCommand);
registration.PerformAction();
}
}
Output
Enrolled in course: Java Programming
Explanation
- This example demonstrates the Command Pattern, which encapsulates a request into an object. This makes handling, customizing, or queuing actions much easier.
- The CourseRegistration class acts as the invoker. It triggers the EnrollInCourseCommand, which specifically handles the task of enrolling in a course.
- This pattern provides flexibility, allowing you to manage and execute commands with greater control and precision.
4. State Design Pattern
State Design Pattern lets an object change its behavior when its internal state changes. Ever thought about how a vending machine knows whether it's idle, accepting coins, or dispensing items? This pattern is a perfect fit for managing such state-dependent behavior.
Example
// State Interface
public interface ICourseState {
void Handle();
}
// Concrete States
public class NotStartedState : ICourseState {
public void Handle() {
Console.WriteLine("Course not started yet.");
}
}
public class InProgressState : ICourseState {
public void Handle() {
Console.WriteLine("Course is in progress.");
}
}
public class CompletedState : ICourseState {
public void Handle() {
Console.WriteLine("Course is completed.");
}
}
// Context
public class Course {
private ICourseState state;
public void SetState(ICourseState state) {
this.state = state;
}
public void Request() {
state.Handle();
}
}
// Main Class
public class StatePatternExample {
public static void Main(string[] args) {
Course course = new Course();
course.SetState(new NotStartedState());
course.Request();
course.SetState(new InProgressState());
course.Request();
course.SetState(new CompletedState());
course.Request();
}
}
Output
Course not started yet.
Course is in progress.
Course is completed
Explanation
- This example demonstrates the State Pattern, where an object's behavior changes based on its internal state.
- The Course class transitions between three states: NotStartedState, InProgressState, and CompletedState. Each state implements the Handle() method to give the right behavior.
- This approach makes state management easier and more flexible, allowing the system to adapt to different conditions.
5. Template Method Design Pattern
Template Method Design Pattern defines the overall structure of an algorithm in a base class but lets subclasses provide their own implementation for specific steps. Think of it like following a cooking recipe where the steps are fixed, but the ingredients can vary based on the dish you're making.
Example
// Abstract Class
public abstract class CourseTemplate {
public final void Enroll() {
SelectCourse();
MakePayment();
SendConfirmation();
}
protected abstract void SelectCourse();
protected abstract void MakePayment();
protected void SendConfirmation() {
Console.WriteLine("Confirmation sent.");
}
}
// Concrete Class 1
public class JavaCourse : CourseTemplate {
protected override void SelectCourse() {
Console.WriteLine("Java Programming course selected.");
}
protected override void MakePayment() {
Console.WriteLine("Payment made for Java course.");
}
}
// Concrete Class 2
public class PythonCourse : CourseTemplate {
protected override void SelectCourse() {
Console.WriteLine("Python Programming course selected.");
}
protected override void MakePayment() {
Console.WriteLine("Payment made for Python course.");
}
}
// Main Class
public class TemplateMethodPatternExample {
public static void Main(string[] args) {
CourseTemplate javaCourse = new JavaCourse();
javaCourse.Enroll();
CourseTemplate pythonCourse = new PythonCourse();
pythonCourse.Enroll();
}
}
Output
Java Programming course selected.
Payment made for Java course.
Confirmation sent.
Python Programming course selected.
Payment made for Python course.
Confirmation sent.
Explanation
- This example introduces the Template Method Pattern, which outlines the steps of an algorithm in a base class but leaves the details to be defined by subclasses.
- The CourseTemplate class defines the Enroll() method, with steps like SelectCourse() and MakePayment(). Concrete classes (JavaCourse and PythonCourse) provide their own versions of these steps.
- This design keeps a consistent structure while allowing for customization, just like following a fixed recipe but using different ingredients.
6. Chain of Responsibility Design Pattern
Chain of Responsibility Design Pattern passes a request along a chain of handlers until one of them handles it. This pattern is helpful when you want to process requests in sequence, like handling user inputs or processing different stages of a request in a web application.
Example
// Handler Interface
interface CourseHandler {
void setNextHandler(CourseHandler handler);
void handleRequest(String request);
}
// Concrete Handlers
class EnrollmentHandler implements CourseHandler {
private CourseHandler nextHandler;
@Override
public void setNextHandler(CourseHandler handler) {
this.nextHandler = handler;
}
@Override
public void handleRequest(String request) {
if (request.equals("Enroll")) {
System.out.println("Handling enrollment.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class PaymentHandler implements CourseHandler {
private CourseHandler nextHandler;
@Override
public void setNextHandler(CourseHandler handler) {
this.nextHandler = handler;
}
@Override
public void handleRequest(String request) {
if (request.equals("Payment")) {
System.out.println("Handling payment.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class CertificationHandler implements CourseHandler {
private CourseHandler nextHandler;
@Override
public void setNextHandler(CourseHandler handler) {
this.nextHandler = handler;
}
@Override
public void handleRequest(String request) {
if (request.equals("Certificate")) {
System.out.println("Handling certification.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
// Main Class
public class ChainOfResponsibilityPatternExample {
public static void main(String[] args) {
CourseHandler enrollment = new EnrollmentHandler();
CourseHandler payment = new PaymentHandler();
CourseHandler certification = new CertificationHandler();
enrollment.setNextHandler(payment);
payment.setNextHandler(certification);
enrollment.handleRequest("Payment");
enrollment.handleRequest("Certificate");
}
}
Output
Handling payment.
Handling certification.
Explanation
- This example demonstrates the Chain of Responsibility Pattern, where requests are passed through a series of handlers.
- Each handler (like EnrollmentHandler, PaymentHandler, and CertificationHandler) checks if it can handle the request. If it can't, it forwards the request to the next handler in the chain.
- This pattern allows flexible request handling and dynamic changes to the sequence of processing handlers.
7. Mediator Design Pattern
Mediator Design Pattern centralizes communication between objects, reducing the dependency between them. This pattern is useful for managing complex interactions in a system, such as coordinating components in a chat application to handle user messages and notifications.
Example
import java.util.ArrayList;
import java.util.List;
// Mediator Interface
interface CourseMediator {
void registerStudent(Student student);
void notifyStudents(String message);
}
// Concrete Mediator
class Course implements CourseMediator {
private List students = new ArrayList<>();
@Override
public void registerStudent(Student student) {
students.add(student);
}
@Override
public void notifyStudents(String message) {
for (Student student : students) {
student.update(message);
}
}
}
// Colleague
class Student {
private String name;
private CourseMediator mediator;
public Student(String name, CourseMediator mediator) {
this.name = name;
this.mediator = mediator;
mediator.registerStudent(this);
}
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
// Main Class
public class MediatorPatternExample {
public static void main(String[] args) {
Course course = new Course();
Student student1 = new Student("Alice", course);
Student student2 = new Student("Bob", course);
course.notifyStudents("Course registration opens tomorrow.");
}
}
Output
Alice received message: Course registration opens tomorrow.
Bob received message: Course registration opens tomorrow.
Explanation
- This example demonstrates the Mediator Pattern, which centralizes communication between objects.
- The Course class acts as an intermediary, facilitating interactions between Student instances. Instead of communicating directly, students rely on the Course to broadcast messages, simplifying communication and reducing object dependencies.
8. Memento Design Pattern
The Memento Design Pattern captures and restores an object's state without exposing its internal structure. This pattern is helpful in saving and retrieving an object's state, like implementing an undo feature in a text editor where you can revert to a previous version of the document.
Example
// Memento
class CourseMemento {
private String courseName;
public CourseMemento(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
}
// Originator
class Course {
private String courseName;
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public CourseMemento save() {
return new CourseMemento(courseName);
}
public void restore(CourseMemento memento) {
this.courseName = memento.getCourseName();
}
@Override
public String toString() {
return "Course name: " + courseName;
}
}
// Caretaker
class CourseCaretaker {
private CourseMemento memento;
public void saveMemento(CourseMemento memento) {
this.memento = memento;
}
public CourseMemento getMemento() {
return memento;
}
}
// Main Class
public class MementoPatternExample {
public static void main(String[] args) {
Course course = new Course();
CourseCaretaker caretaker = new CourseCaretaker();
course.setCourseName("Java Programming");
System.out.println(course);
caretaker.saveMemento(course.save());
course.setCourseName("Advanced Java Programming");
System.out.println(course);
course.restore(caretaker.getMemento());
System.out.println(course);
}
}
Output
Course name: Java Programming
Course name: Advanced Java Programming
Course name: Java Programming
Explanation
- This example demonstrates the Memento Pattern, which captures and restores an object's state while preserving its underlying structure.
- The Course class can save its current state as a CourseMemento and then restore it later.
- The CourseCaretaker saves the remembrance, allowing the Course to return to a prior state as necessary, effectively handling state preservation and restoration.
When to Use Design Pattern in Java?
Design patterns are useful in several situations. You should consider using them when you face common, reoccurring problems that need well-established solutions. They also help in improving flexibility, reusability, and maintaining your code. By using design patterns, you can follow important design principles and enhance communication within your team. Sometimes, they can also improve the performance of your code by making it more efficient.
- Reoccurring Problems: Use design patterns when you face the same design challenges over and over again. They provide reliable solutions.
- Flexibility and Reusability: Design patterns help in structuring your code to be more flexible and reusable, making it easier to adapt to changing requirements.
- Design Principles: They help in implementing key design principles such as encapsulation, separation of concerns, and dependency inversion, improving modularity and reducing dependencies.
- Communication: Design patterns provide a shared vocabulary, making communication and collaboration within the team more efficient.
- Performance: Some patterns can enhance the performance of your application by minimizing resource usage or improving code efficiency.
When Not to Use Design Patterns in Java
Although design patterns are powerful, they are not always the best choice. In certain situations, they can add unnecessary complexity or reduce performance. For simple problems or small projects, using a design pattern might overcomplicate things. You should also avoid using patterns if you don’t fully understand them, as this could introduce bugs and maintenance challenges.
- Simple Problems: Avoid using design patterns for simple problems. They can make the solution more complicated than it needs to be.
- Small Projects: For small projects with limited scope, design patterns may add extra layers of complexity that aren’t needed.
- Premature Optimization: Don’t use design patterns just to anticipate future issues. Focus on solving the current problem.
- Lack of Understanding: If you or your team aren’t familiar with a pattern, using it incorrectly could lead to bugs and maintenance difficulties.
- Performance Concerns: Some patterns may introduce performance overhead. If performance is crucial, avoid patterns that add unnecessary complexity.
How to Choose the Right Design Pattern?
Choosing a design pattern depends on the problem you are solving and the requirements for solving it. Consider the following points:
- Problem Type: Match the design pattern to the type of problem you are dealing with, such as creational, structural, or behavioral issues.
- Flexibility: Make sure the pattern allows for flexibility and scalability as requirements change in the future.
- Complexity: Ensure that the complexity of the pattern does not make your design harder to understand or maintain, but rather simplifies it.
- Common Solutions: Look at well-used patterns in similar situations and leverage those proven solutions.
Choosing the right design pattern will enhance the maintainability and effectiveness of your solution.
Read more: |
.Net Design Patterns Interview Questions, You Must Know! |
Most Frequently Asked Software Architect Interview Questions and Answers |
Summary
In conclusion, a design pattern in .NET is a powerful solution for addressing common software design problems. Design patterns in .NET enable developers to write structured, reusable, and maintainable code, leading to improved software quality. By mastering each design pattern in .NET, developers can enhance their problem-solving capabilities and build more reliable applications.
Step into the future with ScholarHat’s Free Technology Courses! Learn from industry experts, master the latest technologies, and gain hands-on experience to boost your career. Also, explore our Software Architecture and Design Certification Training for a deeper understanding of other .NET concepts. Join us now and lead the way in tech!
Did You Know? Quiz - .NET Design Patterns
Q1: "Design patterns in .NET help solve common software design problems using best practices."
- True
- False
Q2: "Singleton, Factory, and Observer are examples of .NET design patterns."
- True
- False
Q3: "The Factory Pattern is used to enforce a single instance of a class throughout an application."
- True
- False
Q4: "The Repository Pattern in .NET helps manage data access by abstracting the database operations."
- True
- False