Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Core Object-Oriented Design Principles in C#

Tech 1

1. Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. In practical terms, this means a module should be responsible for a single functional area or logic within the application. This principle serves as a foundation for achieving high cohesion and low coupling. The primary challenge lies in accurately defining and separating these responsibilities, which involves determining the correct granularity of functionality. By adhering to SRP, code becomes easier to read and maintain, while also reducing the complexity of individual classes and limiting the ripple effect of changes.

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. The goal is to enhance reusability and maintainability by minimizing changes to existing code, particularly core or low-level modules. When requirements evolve, new functionality should be added through new code rather than altering existing, tested implementations. This is often achieved using interfaces and abstract classes. The core philosophy is that system behavior is modified by adding new code modules that extend the abstraction layer without altering the existing logic within that layer.

3. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle asserts that if a program is using a base class, it should be able to use any of its subclasses without the program knowing it. In other words, subtypes must be perfectly substitutable for their base types. This principle is crucial for inheritance reuse; if a derived class cannot replace the base class without breaking the application's functionality, the inheritance hierarchy is incorrectly designed. LSP complements the Open/Closed Principle by ensuring that abstractions are robust enough to allow for safe extension through substitution.

4. Dependency Inversion Principle (DIP)

This principle relies on two key rules: high-level modules should not depend on low-level modules, and both should depend on abstractions. Furthermore, abstractions should not depend on details; details should depend on abstractions. The implementation effectively decouples modules by forcing them to rely on interfaces or abstract classes rather than concrete implementations.

To implement DIP, a class (the client) defines an injection point for a service (dependency) via an interface. An external controller or main program instantiates the specific service implementation and injects it into the client. This ensures the client does not depend directly on the concrete service class, facilitating loose coupling.

Dependency Inversion Example

using System;

namespace DependencyInversionExample
{
    public interface IReportGenerator
    {
        string Generate();
    }

    public class PdfReport : IReportGenerator
    {
        public string Generate()
        {
            return "Generating PDF Report...";
        }
    }

    public class ExcelReport : IReportGenerator
    {
        public string Generate()
        {
            return "Generating Excel Report...";
        }
    }

    public class ReportingSystem
    {
        // Dependency is injected via the interface
        public void PrintReport(IReportGenerator generator)
        {
            Console.WriteLine(generator.Generate());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ReportingSystem system = new ReportingSystem();
            
            // Injecting different concrete implementations
            system.PrintReport(new PdfReport());
            system.PrintReport(new ExcelReport());

            Console.ReadKey();
        }
    }
}

5. Interface Segregation Principle (ISP)

The Interface Segregation Principle suggests that clients should not be forced to depend on interfaces they do not use. This promotes the creation of smaller, more specific interfaces rather than large, monolithic ones. By splitting interfaces based on functionality, developers reduce coupling between classes. A class should only implement the methods that are relevant to its specific role, avoiding the burden of implementing unnecessary methods defined in a "fat" interface.

Interface Segregation Example

namespace InterfaceSegregationExample
{
    // Segregated Interfaces
    public interface IPrintable
    {
        void Print();
    }

    public interface IScannable
    {
        void Scan();
    }

    // Basic printer only needs to print
    public class SimplePrinter : IPrintable
    {
        public void Print()
        {
            Console.WriteLine("Printing document...");
        }
    }

    // Multi-function device implements both interfaces
    public class MultiFunctionPrinter : IPrintable, IScannable
    {
        public void Print()
        {
            Console.WriteLine("Printing document...");
        }

        public void Scan()
        {
            Console.WriteLine("Scanning document...");
        }
    }
}

6. Law of Demeter (LoD)

Also known as the Principle of Least Knowledge, the Law of Demeter states that an object should have limited knowledge of other objects. It minimizes coupling between classes by restricting interactions to immediate "friends" (closely related classes). A method should not invoke methods on objects that are returned by another method. In essence, talk only to your immediate friends. This strict encapsulation ensures that changes in one subsystem have minimal impact on unrelated parts of the system.

Law of Demeter Example

using System;
using System.Collections.Generic;

namespace LawOfDemeterExample
{
    // Represents a Student in a classroom
    public class Student
    {
        public string Name { get; set; }
        public Student(string name) => Name = name;
    }

    // Manages students locally (demarcation of subsystems)
    public class Classroom
    {
        private List _students = new List();

        public void AddStudent(Student student) => _students.Add(student);

        // Provides data access rather than exposing the list directly
        public List GetStudentNames()
        {
            List names = new List();
            foreach(var s in _students)
            {
                names.Add(s.Name);
            }
            return names;
        }
    }

    // High-level manager that coordinates classrooms
    public class SchoolManager
    {
        private List _classrooms = new List();

        public SchoolManager()
        {
            InitializeSchool();
        }

        private void InitializeSchool()
        {
            var classA = new Classroom();
            classA.AddStudent(new Student("Alice"));
            classA.AddStudent(new Student("Bob"));
            _classrooms.Add(classA);
        }

        public void DisplayAllStudents()
        {
            foreach (var room in _classrooms)
            {
                // SchoolManager interacts only with Classroom, not Student directly
                var names = room.GetStudentNames();
                foreach (var name in names)
                {
                    Console.WriteLine($"Student: {name}");
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SchoolManager school = new SchoolManager();
            school.DisplayAllStudents();
            Console.ReadKey();
        }
    }
}

7. Composite Reuse Principle (CRP)

The Composite Reuse Principle advocates for favoring object composition over inheritance to achieve code reuse. While inheritance creates a tight "is-a" relationship, composition creates a "has-a" relationship that is generally more flexible. Overusing inheritance can lead to brittle systems where changes in the parent class propagate undesirably to child classes. Composition allows behavior to be composed dynamically at runtime, reducing class explosion and maintaining better encapsulation.

Composite Reuse Example

using System;

namespace CompositeReuseExample
{
    public interface ITheme
    {
        string ApplyStyle();
    }

    public class DarkTheme : ITheme
    {
        public string ApplyStyle() => "Dark Theme Applied";
    }

    public class LightTheme : ITheme
    {
        public string ApplyStyle() => "Light Theme Applied";
    }

    // Base component uses composition for behavior
    public abstract class UIComponent
    {
        protected ITheme _theme;

        public UIComponent(ITheme theme)
        {
            _theme = theme;
        }

        public abstract void Render();
    }

    public class Button : UIComponent
    {
        public Button(ITheme theme) : base(theme) { }

        public override void Render()
        {
            Console.WriteLine($"Rendering Button with {_theme.ApplyStyle()}");
        }
    }

    public class Panel : UIComponent
    {
        public Panel(ITheme theme) : base(theme) { }

        public override void Render()
        {
            Console.WriteLine($"Rendering Panel with {_theme.ApplyStyle()}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Compose behavior dynamically
            ITheme dark = new DarkTheme();
            var darkButton = new Button(dark);
            darkButton.Render();

            ITheme light = new LightTheme();
            var lightPanel = new Panel(light);
            lightPanel.Render();

            Console.ReadKey();
        }
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.