Nutshell Series

Object-Oriented Programming Concepts in C#

Object-Oriented Programming (OOP) helps us design applications using real-world concepts.
Below we explore important OOP principles and relationships in C#, along with examples.


Class

A Class is a blueprint that defines properties and methods.

public class Car
{
    public string Brand { get; set; }
    public void Drive()
    {
        Console.WriteLine($"{Brand} is driving.");
    }
}

Object

An Object is an instance of a class.

Car car1 = new Car { Brand = "Toyota" };
car1.Drive(); // Output: Toyota is driving.

Abstraction

Abstraction focuses on essential details while hiding the complexity.

public abstract class Payment
{
    public abstract void ProcessPayment(decimal amount);
}

public class CreditCardPayment : Payment
{
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Paid {amount} using Credit Card.");
    }
}

Encapsulation

Encapsulation hides implementation details using access modifiers.

public class BankAccount
{
    private decimal balance;

    public void Deposit(decimal amount) => balance += amount;
    public decimal GetBalance() => balance; // only controlled access
}

Polymorphism

Polymorphism allows the same method name to perform different tasks.

Overloading

public class Calculator
{
    public int Add(int a, int b) => a + b;
    public double Add(double a, double b) => a + b;
}

Overriding

public class Animal
{
    public virtual void Speak() => Console.WriteLine("Animal sound");
}

public class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Bark");
}

Inheritance

Inheritance lets a class reuse properties and methods from a parent class.

public class Vehicle
{
    public void Start() => Console.WriteLine("Vehicle started");
}

public class Bike : Vehicle
{
    public void RingBell() => Console.WriteLine("Bell rings!");
}

Sealed Class

A sealed class cannot be inherited.

public sealed class Logger
{
    public void Log(string msg) => Console.WriteLine(msg);
}

Multiple Inheritance

C# does not allow multiple inheritance of classes, but interfaces provide it.

public interface IFly
{
    void Fly();
}

public interface ISwim
{
    void Swim();
}

public class Duck : IFly, ISwim
{
    public void Fly() => Console.WriteLine("Duck flying");
    public void Swim() => Console.WriteLine("Duck swimming");
}

Abstract Class

An abstract class cannot be instantiated directly; it must be inherited.

public abstract class Shape
{
    public abstract double Area();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area() => Math.PI * Radius * Radius;
}

Generalization

Generalization groups common features into a parent class.

public class Teacher : Person { }
public class Student : Person { }

public class Person
{
    public string Name { get; set; }
}

Association

Association represents a relationship between classes.

public class Customer
{
    public string Name { get; set; }
}

public class Order
{
    public Customer OrderedBy { get; set; }
}

Aggregation

Aggregation is a weak “whole-part” relationship.

public class Department
{
    public List Employees { get; set; } = new List();
}

public class Employee
{
    public string Name { get; set; }
}

Composition

Composition is a strong “whole-part” relationship. If the whole is destroyed, parts are also destroyed.

public class House
{
    private Room room;
    public House()
    {
        room = new Room(); // Room cannot exist without House
    }
}

public class Room { }

Multiplicity

Multiplicity defines how many objects can participate in a relationship.

public class Library
{
    public List Books { get; set; } = new List();
}

public class Book { }

Interface

An interface defines a contract without implementation.

public interface INotifier
{
    void Send(string message);
}

public class EmailNotifier : INotifier
{
    public void Send(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

Summary Table

Concept Definition Example
Class Blueprint of objects Car
Object Instance of a class car1 = new Car()
Abstraction Focus on essentials Payment (abstract)
Encapsulation Data hiding BankAccount with private balance
Polymorphism Many forms Calculator.Add() overloads, Dog.Speak() override
Inheritance Reuse parent properties Bike inherits Vehicle
Sealed Class Cannot be inherited Logger
Multiple Inheritance Via interfaces Duck : IFly, ISwim
Abstract Class Must be inherited Shape
Generalization Group common features Person parent for Student, Teacher
Association Relationship Order → Customer
Aggregation Weak whole-part Department → Employees
Composition Strong whole-part House → Room
Multiplicity How many objects Library → Books
Interface Contract without implementation INotifier
Nutshell Series

🚀 Software Design Principles Every Developer Should Know

Good software design isn’t just about writing code that works — it’s about writing clean, maintainable, and scalable code.
To achieve this, developers follow a set of guiding principles that help keep complexity low and quality high.
Here’s a consolidated list of the most important software design principles with explanations and C# examples.


1. 🧩 KISS (Keep It Simple, Stupid)

The KISS principle reminds us to keep software as simple as possible. Avoid unnecessary complexity, write clear code, and focus only on what’s needed.

// ❌ Bad: Over-engineered
if (user.Age > 18 && (user.Country == "US" || user.Country == "UK" || user.Country == "CA"))
{
    AllowAccess();
}
else
{
    DenyAccess();
}

// ✅ Good: Simple and clear
if (user.IsAdult())
{
    AllowAccess();
}

2. 🔁 DRY (Don’t Repeat Yourself)

The DRY principle means you should avoid code duplication.
Instead, encapsulate common logic in functions, classes, or constants.

// ❌ Bad: Duplicate logic
Console.WriteLine("Welcome, John");
Console.WriteLine("Welcome, Mary");

// ✅ Good: Reusable method
void Greet(string name)
{
    Console.WriteLine($"Welcome, {name}");
}

Greet("John");
Greet("Mary");

3. 🚫 YAGNI (You Aren’t Gonna Need It)

Don’t implement features “just in case.” Only build what you need now to keep the system simple and maintainable.

// ❌ Bad: Adding unnecessary parameters for future features
decimal CalculateDiscount(decimal price, string type = "standard", int loyaltyPoints = 0, bool seasonal = false, bool futureFeature = false)
{
    // Too much complexity
    return price;
}

// ✅ Good: Only what is needed now
decimal CalculateDiscount(decimal price, string type = "standard")
{
    return price;
}

4. 📐 SOLID Principles

SOLID is a collection of five principles for building maintainable and extensible object-oriented software:

  • Single Responsibility Principle (SRP): A class should have only one reason to change.
  • Open/Closed Principle (OCP): Classes should be open for extension but closed for modification.
  • Liskov Substitution Principle (LSP): Subclasses should be usable in place of their base classes.
  • Interface Segregation Principle (ISP): Clients shouldn’t be forced to depend on methods they don’t use.
  • Dependency Inversion Principle (DIP): Depend on abstractions, not concrete implementations.
// ✅ Example: Dependency Inversion
public interface IMessageService
{
    void Send(string message);
}

public class EmailService : IMessageService
{
    public void Send(string message) 
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

public class Notification
{
    private readonly IMessageService _service;

    public Notification(IMessageService service)
    {
        _service = service;
    }

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

// Usage
var emailService = new EmailService();
var notification = new Notification(emailService);
notification.Notify("Hello, World!");

5. 🤔 Principle of Least Astonishment (POLA)

Software should behave in a way that is consistent with user expectations.
Use familiar terminology, intuitive design, and clear error messages.

// ❌ Bad: Confusing error
throw new Exception("ERR_451_USER_FAIL");

// ✅ Good: Clear error
throw new UnauthorizedAccessException("User authentication failed. Please check your password.");

6. 🧱 Principle of Modularity

Design software as independent, reusable modules. This makes it easier to maintain, test, and scale.

// Example project structure
- Auth/
- Payment/
- Notifications/

7. 🎭 Principle of Abstraction

Hide unnecessary details and expose only essential features.
Abstraction helps simplify usage and prevents users from depending on internal complexity.

public class EmailSender
{
    public void SendEmail(string to, string subject, string body)
    {
        // Hides SMTP details
        Console.WriteLine($"Sending email to {to}: {subject}");
    }
}

8. 🔒 Principle of Encapsulation

Encapsulation hides the internal state of an object and exposes behavior only through well-defined interfaces.

public class BankAccount
{
    private decimal _balance;

    public void Deposit(decimal amount)
    {
        _balance += amount;
    }

    public decimal GetBalance()
    {
        return _balance;
    }
}

9. 📉 Principle of Least Knowledge (Law of Demeter)

A module should know as little as possible about other modules. This reduces coupling and increases flexibility.

// ❌ Bad: Too much knowledge
var creditLimit = order.Customer.Account.CreditLimit;

// ✅ Good: Ask the object directly
var creditLimit = order.GetCustomerCreditLimit();

10. 🔗 Low Coupling & High Cohesion

Low Coupling: Modules should have minimal dependencies on each other.
High Cohesion: Each module should serve a single, well-defined purpose.

// ✅ Example: High cohesion, low coupling
public class InvoiceGenerator
{
    public string Generate(Order order)
    {
        return $"Invoice for {order.Id}";
    }
}

public class EmailService
{
    public void SendInvoice(string invoice)
    {
        Console.WriteLine($"Invoice sent: {invoice}");
    }
}

🎯 Conclusion

These principles are not strict rules but guidelines to help you write clean, scalable, and maintainable software.
By applying KISS, DRY, YAGNI, SOLID, and others consistently, you’ll reduce bugs, simplify maintenance, and build systems that are easier to extend as requirements evolve.

Nutshell Series

🔧 Dependency Injection (DI) Explained: Transient vs Scoped vs Singleton

Dependency Injection (DI) is a design pattern that simplifies how objects and their dependencies are managed in an application. Instead of classes creating their own dependencies, DI provides those dependencies from the outside. This makes applications cleaner, testable, and maintainable.

⚙️ What is Dependency Injection?

At its core, DI is about inversion of control: your classes don’t create what they need; a container provides them. This container decides:

  • How to create objects
  • When to reuse objects
  • How to dispose of them when no longer needed

📌 Service Lifetimes

When you register services in a DI container, you usually choose a lifetime:

  • Transient – A new instance is created every time it’s requested.
  • Scoped – One instance is created per request (or unit of work).
  • Singleton – A single instance is shared across the entire application lifetime.

🔄 Transient

A new instance is created each time the service is requested. Best for lightweight, stateless services.

// Example in C#
services.AddTransient<IEmailService, EmailService>();

📂 Scoped

A new instance is created once per request, but reused within that request. Useful for services like database contexts.

// Example in C#
services.AddScoped<IDbContext, AppDbContext>();

♾ Singleton

A single instance is created and reused for the entire lifetime of the application. Perfect for loggers, configuration readers, and caching providers.

// Example in C#
services.AddSingleton<ILogger, Logger>();

📊 When to Use Each?

  • Transient – For short-lived, stateless operations (e.g., helpers, formatters).
  • Scoped – For services tied to a single request or unit of work (e.g., DbContext).
  • Singleton – For shared state or expensive-to-create services (e.g., loggers, configuration, caching).

🚀 Conclusion

Dependency Injection ensures better code structure, easier testing, and improved flexibility.
Choosing the right lifetime — Transient, Scoped, or Singleton — helps you balance performance with resource management.