Showing posts with label OOPs. Show all posts
Showing posts with label OOPs. Show all posts

SOLID Principles Implementation in C#.

Software development is not just about writing code; it’s about writing clean, maintainable, scalable, and testable code. That’s exactly what the SOLID principles help us achieve.

Whether you’re building a small API or an enterprise-level microservice, understanding SOLID principles makes your code more flexible, easier to extend, and less prone to bugs.

In this article, we’ll break down SOLID principles with simple explanations and practical C# implementations.

SOLID Principle Full Form

What is the SOLID Principle?

The SOLID Principles are a set of five core object-oriented design (OOD) guidelines used in software development to create systems that are more understandable, flexible, maintainable, and scalable. Coined by Michael Feathers and based on principles from Robert C. Martin ("Uncle Bob").

SOLID is an acronym for five object-oriented design principles: 

  • S: Single Responsibility Principle 
  • O: Open/Closed Principle 
  • L: Liskov Substitution Principle 
  • I: Interface Segregation Principle 
  • D: Dependency Inversion Principle 

Let’s go through each principle one by one and look at the problems that arise when we don’t follow the SOLID Principle. After that, we’ll explore how applying these principles can help us write cleaner, more maintainable code.

1. S - Single Responsibility Principle.

The 'S' in the SOLID principles stands for the Single Responsibility Principle (SRP). The Single Responsibility Principle states that a class, module, or function should have only one reason to change, meaning it should have a single, clearly defined purpose or job within the software system.

Bad Example: Imagine an online shopping system where:

  • An order is created
  • Order is saved to the database
  • Customer is emailed

If all of this is in ONE class, it violates SRP.

Example Code:

public class OrderService
{
    public void CreateOrder()
    {
        Console.WriteLine("Order created.");
    }

    public void SaveToDatabase()
    {
        Console.WriteLine("Order saved to DB.");
    }

    public void SendEmail()
    {
        Console.WriteLine("Email sent to customer.");
    }
}

Let's see how we can write better code using SRP.

Good Example: To follow the single responsibility principle, we have created classes in such a way that each class has its own responsibility and reason to change. In our example, we will split the responsibilities into three different classes.

Example Code:
namespace PracticeCode.SOLID
{
    public class OrderCreator
    {
        public void CreateOrder()
        {
            Console.WriteLine("Order created.");
        }
    }

    public class OrderRepository
    {
        public void Save()
        {
            Console.WriteLine("Order saved to DB.");
        }
    }

    public class EmailService
    {
        public void Send()
        {
            Console.WriteLine("Email sent to customer.");
        }
    }

    public class OrderService
    {
        private readonly OrderCreator _orderCreator;
        private readonly OrderRepository _orderRepository;
        private readonly EmailService _emailService;

        public OrderService()
        {
            _orderCreator = new OrderCreator();
            _orderRepository = new OrderRepository();
            _emailService = new EmailService();
        }

        public void ProcessOrder()
        {
            _orderCreator.CreateOrder();
            _orderRepository.Save();
            _emailService.Send();
        }
    }
}
Program.cs
OrderService processor = new OrderService();
processor.ProcessOrder();
Console.ReadLine();
Output:
Order created.
Order saved to DB.
Email sent to customer.

Now each class has only one reason to change. This is the correct example of SRP.

2. O - Open/Closed Principle (OCP)

A class should open for extension but close for modification. It means that you should be able to add new functionality without changing the existing code. This avoids breaking old logic when adding new features.

Bad Example: Multiple types of discount calculations are implemented in a single class. If you want to add a new discount type, you need to modify the same class repeatedly. This violates OCP because modification is required for every new discount.
public class DiscountService
{
    public decimal GetDiscount(string customerType, decimal amount)
    {
        if (customerType == "Regular")
            return amount * 0.05m;

        if (customerType == "Premium")
            return amount * 0.10m;

        return 0;
    }
}

Good Example: We can refactor the above code using Inheritance and Polymorphism. Let's see how we can use the open/close principle to write better code.

Step 1: Create a base class, also called an extension point.
public abstract class Discount
{
    public abstract decimal Calculate(decimal amount);
}

Step 2: Create separate classes for each discount type.
public class RegularDiscount : Discount
{
    public override decimal Calculate(decimal amount)
        => amount * 0.05m;
}

public class PremiumDiscount : Discount
{
    public override decimal Calculate(decimal amount)
        => amount * 0.10m;
}
You can add a new type like:
public class GoldDiscount : Discount
{
    public override decimal Calculate(decimal amount)
        => amount * 0.15m;
}

Step 3: Create a Factory Class to choose the correct discount type.
//----------------Discount Factory----------------//
public static class DiscountFactory
{
    public static Discount GetDiscount(string discountType)
    {
        return discountType.ToLower() switch
        {
            "regular" => new RegularDiscount(),
            "seasonal" => new SeasonalDiscount(),
            _ => throw new ArgumentException("Invalid discount type"),
        };
    }
}

public class Customer
{
    private readonly Discount _discount;

    public Customer(Discount discount)
    {
        _discount = discount;
    }

    public decimal GetFinalAmount(decimal amount)
    {
        return _discount.ApplyDiscount(amount);
    }
}

Step 4: Calling the Factory Method in the Program.cs
class Program
{
    static void Main(string[] args)
    {
        Console.Write("Enter discount type (regular / seasonal): ");
        string type = Console.ReadLine();

        Discount discount = DiscountFactory.CreateDiscount(type);

        Customer customer = new Customer(discount);

        decimal amount = 1000;
        decimal finalAmount = customer.GetFinalAmount(amount);

        Console.WriteLine($"Final amount for {type} discount: {finalAmount}");
    }
}
Output:
Enter discount type (regular / seasonal): seasonal
Final amount for seasonal discount: 800

Now our old code will never change, even if we want to implement any new discount type in the future. This is what the open/closed principle wants.

3. L - Liskov Substitution Principle (LSP)

A child class should be replaceable for its parent class without breaking the program. It means wherever you use a base class, you should be able to use its derived class without causing errors or unexpected behavior. If substituting a child class changes the behavior, it violates LSP.

Bad Example: If the Payment class has Refund(), a CashPayment class that throws an exception for Refund violates LSP.

Example Code: We have a base class with Pay and Refund options that can be overridden by derived classes.
public class Payment
{
    public virtual void Pay(decimal amount)
    {
        Console.WriteLine($"Payment of {amount} done.");
    }

    public virtual void Refund(decimal amount)
    {
        Console.WriteLine($"Refund of {amount} processed.");
    }
}

Now, when we create the CashPayment and CardPayment derived classes, we inherit from the Payment class. CardPayment works fine because it has both Pay and Refund options, but CashPayment does not have a refund option.
public class CashPayment : Payment
{
    public override void Refund(decimal amount)
    {
        throw new NotSupportedException("Cash cannot be refunded!");
    }
}

Here, it will throw a runtime error when you create a child object and assign it to the Parent, because the child class is not substitutable for the Parent class.
Payment payment = new CashPayment();
payment.Refund(100);   // 💥 Crash at runtime!

CashPayment violates LSP because it cannot safely replace Payment’s refund behavior, causing the program to break when substituted.

Good Example: We FIX this by splitting responsibilities using interfaces.
We are splitting the Payment system into two interfaces, IPayment and IRfundable, and different methods inherit and implement them based on their functionality.
namespace PracticeCode.SOLID
{
    public interface IPayment
    {
        void Pay(decimal amount);
    }
    public interface IRfundable
    {
        void Refund(decimal amount);
    }

    public class CardPayment: IPayment, IRfundable
    {
        public void Pay(decimal amount)
            => Console.WriteLine($"Card Amount Paid: {amount}");

        public void Refund(decimal amount)
            => Console.WriteLine($"Card Amount Refund: {amount}");
    }

    public class CashPayment: IPayment
    {
        public void Pay(decimal amount)
            => Console.WriteLine($"Cash Amount Paid: {amount}");
    }

    public class PaymentProcess
    {
        public void ProcessPayment(IPayment payment, decimal amount)
        {
            payment.Pay(amount);
        }
        public void RefundPayment(IRfundable refund, decimal amount)
        {
            refund.Refund(amount);
        }
    }
}

We will create a PaymentProcess object and call the methods defined inside with a child class object. As there is no Refund for Cash payment, we will get a compile-time error when we try to call the refund method (Program.cs).
//LSP
var processPayment = new PaymentProcess();
processPayment.ProcessPayment(new CardPayment(), 100);
processPayment.ProcessPayment(new CashPayment(), 100);
processPayment.RefundPayment(new CardPayment(), 100);
//processPayment.RefundPayment(new CashPayment(), 100); //Compile time error

This is how we can easily replace a child class object with a parent class without breaking the application by following LSP.

4. I - Interface Segregation Principle (ISP)

A class should not be forced to implement methods it does not use. Instead of one large interface, create many smaller interfaces. This keeps code clean, avoids empty methods, and removes unnecessary dependencies.

Bad Example: Having a big IWorker interface with Work() and Eat() forces robots to implement Eat(), which they don’t need.
public interface IWorker
{
    void Work();
    void Eat();
}

public class Robot : IWorker
{
    public void Work() 
    {
        Console.WriteLine("Robot working.");
    }

    public void Eat() 
    {
        throw new NotImplementedException("Robots don't eat!");
    }
}

Robots do NOT eat, but are forced to implement Eat(). This violates ISP because the interface forces unnecessary methods.

Good Example: Instead, splitting interfaces into IWorkable and IFeedable keeps everything clean and avoids unnecessary code.
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

// Now classes implement only what they need
public class Human : IWorkable, IFeedable
{
    public void Work() => Console.WriteLine("Human working.");
    public void Eat()  => Console.WriteLine("Human eating.");
}

public class Robot : IWorkable
{
    public void Work() => Console.WriteLine("Robot working.");
}

Now use them in the Program.cs
namespace PracticeCode.SOLID
{
    class Program
    {
        static void Main(string[] args)
        {
            // Using IWorkable interface
            IWorkable humanWorker = new Human();
            IWorkable robotWorker = new Robot();

            humanWorker.Work();   // Output: Human working.
            robotWorker.Work();   // Output: Robot working.

            Console.WriteLine();

            // Using IFeedable interface
            IFeedable humanEater = new Human();
            humanEater.Eat();     // Output: Human eating.

            // Robot does NOT implement IFeedable
            // So you cannot do this:
            // IFeedable robotEater = new Robot();  // ❌ Compile-time error (Good!)

            Console.ReadLine();
        }
    }
}

Human implements both interfaces; Robot only implements IWorkable.

5. D - Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. This makes your code flexible, testable, maintainable, and loosely coupled.

Bad Example: NotificationService depends directly on EmailService

public class EmailService
{
    public void Send(string message)
    {
        Console.WriteLine("EMAIL: " + message);
    }
}

public class NotificationService
{
    private readonly EmailService _emailService = new EmailService(); // ❌ Hard-coded dependency

    public void Notify(string message)
    {
        _emailService.Send(message);
    }
}

Because NotificationService directly depends on EmailService, it becomes tightly coupled, hard to extend, hard to test, and breaks when EmailService changes, clearly violating DIP.

Good Example: Instead of NotificationService calling EmailService directly,
We depend on IMessageService so we can switch between Email, SMS, or WhatsApp without modifying NotificationService.

Step 1: Create an abstraction (Interface) and implement Email & SMS services.
public interface IMessageService
{
    void Send(string message);
}

public class EmailService : IMessageService
{
    public void Send(string message)
    {
        Console.WriteLine("EMAIL: " + message);
    }
}

public class SmsService : IMessageService
{
    public void Send(string message)
    {
        Console.WriteLine("SMS: " + message);
    }
}

Step 2: High-level class depends on abstraction.
public class NotificationService
{
    private readonly IMessageService _messageService;

    public NotificationService(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void Notify(string message)
    {
        _messageService.Send(message);
    }
}

Step 3: Use it in the Program.cs
class Program
{
    static void Main(string[] args)
    {
        IMessageService service = new SmsService();     // choose service
        NotificationService notifier = new NotificationService(service);

        notifier.Notify("Order shipped!");               // SMS: Order shipped!

        Console.ReadLine();
    }
}

I hope you are now clear about the real-life use case of SOLID principles. By applying SOLID correctly in your C# projects, you make your architecture:

  • Scalable
  • Maintainable
  • Testable
  • Professional

If you're preparing for interviews, mastering SOLID is one of the biggest advantages you can have.

Constructor and Destructor in C++.

Constructor and Destructor in C++ are the special member functions of the class and they are essential components of Object Oriented Programming. Constructors are special functions within a class that initialize the object when it is created, providing default or custom values to its attributes. On the other hand, destructors are used to clean up resources allocated by the object during its lifetime, and they are automatically invoked when the object goes out of scope or is explicitly deleted.

Constructor and Destructor in C++

What is a Constructor in C++?

A constructor in C++ is a special member function of a class that is automatically invoked when an object of that class is created. Its primary purpose is to initialize the object's attributes or perform any necessary setup. Constructors have the same name as the class and do not have a return type. 


There are many types of constructors available in C++. When an object is instantiated, the appropriate constructor is automatically called, ensuring that the object is properly initialized before it is used. Constructors play a crucial role in object-oriented programming by establishing the initial state of objects.


How many types of constructors in C++?

There are basically four types of constructors present in C++ and these are:

  • Default Constructor.
  • Parameterized Constructor.
  • Copy Constructor.
  • Dynamic Constructor.
Let's discuss each of them in detail one by one,


Default Constructor.

A default constructor in C++ is a constructor that takes no parameters. It is automatically generated by the compiler if no constructor is explicitly defined in the class. It is called when an object is created with no arguments.

Syntax:
class ClassName {
public:
    ClassName() {
        // Constructor code
    }
};

Example Code for Default Constructor:
// C++ code implementation for default constructor
#include <iostream>
using namespace std;

class MyClass {
private:
    int rollNo;
    string name;

public:
    // Default Constructor
    MyClass() {
        std::cout << "Default Constructor Called" << std::endl;
        rollNo = 0;
        name = "NULL";
    }

    void display(){
        cout << "Student Name: " << name << endl;
        cout << "Roll No: " << rollNo << endl;
    }
};

int main() {
    // Creating an object invokes the default constructor
    MyClass obj;  

    obj.display();
    return 0;
}
Output:
Default Constructor Called
Student Name: NULL
Roll No: 0

In the above example, we created a class MyClass with a default constructor to initialize the initial values to data members of the class, and the default constructor is called as soon as we create the object of that class obj
Note: It's important to explicitly define a default constructor when custom initialization is required or when other constructors are defined in the class. (alert-passed)

Parameterized Constructor.

A parameterized constructor in C++ is a constructor that accepts parameters during its invocation. It allows you to initialize the object with specific values based on the provided arguments. 

Syntax:
class ClassName {
public:
    ClassName(type1 param1, type2 param2, ...) {
        // Constructor code
    }
};

Example Code for Parameterized Constructor:
// C++ code implementation for Parameterized constructor
#include <iostream>
using namespace std;

class Car {
private:
    string brand;
    int year;

public:
    // Parameterized Constructor
    Car(string b, int y) {
        cout << "Parameterized Constructor Called" << endl;
        brand = b;
        year = y;
    }

    void display() {
        cout << "Brand: " << brand << ", Year: " << year << endl;
    }
};

int main() {
    // Creating an object using the parameterized constructor
    Car myCar("Toyota", 2022);
    myCar.display();  
    return 0;
}
Output:
Parameterized Constructor Called
Brand: Toyota, Year: 2022

In the above example, the parameterized constructor takes parameters (in this case, a string and an integer) during its declaration. The constructor is called when an object is created, allowing custom initialization based on the provided arguments.

Copy Constructor.

A copy constructor in C++ is a special type of constructor that is used to create a new object as a copy of an existing object of the same class. It is invoked when an object is passed by value or returned by value.
Note: If a copy constructor is not explicitly defined, the compiler generates a default copy constructor. (alert-success)
Syntax:
class ClassName {
public:
    // Copy Constructor
    ClassName(const ClassName& obj) {
        // Constructor code
    }
};

Example Code for Copy Constructor:
// C++ code implementation for Copy constructor
#include <iostream>
using namespace std;

class Student {
private:
    string name;
    int age;

public:
    // Parameterized Constructor
    Student(string n, int a) : name(n), age(a) {
        cout << "Parameterized Constructor Called" << endl;
    }

    // Copy Constructor
    Student(const Student& obj) : name(obj.name), age(obj.age) {
        cout << "Copy Constructor Called" << endl;
    }

    void display() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    // Creating an object using the parameterized constructor
    Student originalStudent("John", 20);
    originalStudent.display();  

    // Creating a new object using the copy constructor
    Student copiedStudent = originalStudent;
    copiedStudent.display();  
    
    return 0;
}
Output:
Name: John, Age: 20
Copy Constructor Called
Name: John, Age: 20

In the above example, the copy constructor takes a reference to an object of the same class (const ClassName& obj) as its parameter. It initializes the members of the new object with the values of the corresponding members of the existing object.

Dynamic Constructor.

In C++, a dynamic constructor is not a distinct type of constructor. Constructors are typically classified based on their parameters and use cases, such as default constructors, parameterized constructors, and copy constructors. However, there might be a misunderstanding or miscommunication about the term "dynamic constructor."

If you are referring to dynamic memory allocation within a constructor (e.g., using new), this is a common practice in C++ but doesn't give rise to a separate category of a constructor. 

What is a Destructor in C++?

A destructor in C++ is a special member function of a class that is executed whenever an object of the class goes out of scope or is explicitly deleted. It is used to release resources or perform cleanup operations associated with the object before it is destroyed.

Syntax:
class ClassName {
public:
    // Constructor(s)
    ClassName(); // Default constructor
    ClassName(parameters); // Parameterized constructor

    // Destructor
    ~ClassName(); 
};

Key points about Destructor:
  • A destructor has the same name as the class but is prefixed with a tilde (~).
  • Unlike constructors, which can be overloaded, a class can have only one destructor.
  • The destructor is automatically invoked when an object goes out of scope or is explicitly deleted.
  • It is used for releasing resources, closing files, releasing memory, or performing any cleanup necessary before the object is destroyed.

Example Code for Destructor in C++:
// C++ code implementation for Destructor
#include <iostream>
using namespace std;

class MyClass {
public:
    // Constructor
    MyClass() {
        std::cout << "Constructor Called" << std::endl;
    }

    // Destructor
    ~MyClass() {
        std::cout << "Destructor Called" << std::endl;
    }
};

int main() {
    // Creating an object
    MyClass obj; 

    // Object goes out of scope, destructor is called
    return 0;
}
Output:
Constructor Called
Destructor Called
Note: In the example, the destructor prints a message for demonstration purposes. In real-world scenarios, destructors are often used to deallocate memory, close files, or release other resources.(alert-passed)

 (getButton) #text=(Access Specifiers in C++) #icon=(link) #color=(#2339bd)

DON'T MISS

Tech News
© all rights reserved
made with by AlgoLesson