Οι αρχές SOLID προτάθηκαν από τον Robert C. Martin (“Uncle Bob”) στα τέλη της δεκαετίας του 1990, για να βοηθήσουν τους προγραμματιστές να γράφουν ευέλικτο και επαναχρησιμοποιήσιμο αντικειμενοστραφή κώδικα.
Το ακρωνύμιο SOLID προκύπτει από τις αρχές:
S – Single Responsibility Principle (SRP)
O – Open/Closed Principle (OCP)
L – Liskov Substitution Principle (LSP)
I – Interface Segregation Principle (ISP)
D – Dependency Inversion Principle (DIP)
Στόχος των SOLID principles είναι να μειώσουν την σύζευξη (coupling), να αυξήσουν την συνοχή (cohesion) και να κάνουν τον κώδικα πιο ευέλικτο σε αλλαγές.
Αναλυτικά:
1️⃣S – Single Responsibility Principle (SRP)
Αρχή: Κάθε κλάση ή interface πρέπει να έχει μία και μόνο ευθύνη.
Πρόβλημα χωρίς SRP:
Αν μία κλάση κάνει πολλές δουλειές (π.χ. υπολογισμούς, αποθήκευση σε βάση, δημιουργία reports), κάθε αλλαγή σε μία από αυτές τις λειτουργίες μπορεί να σπάσει τις άλλες.
Παράδειγμα:
// Κάθε κλάση έχει μία ευθύνη
public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}
// Υπολογισμός μισθού
public interface ISalaryCalculator
{
    decimal Calculate(Employee employee);
}
public class SalaryCalculator : ISalaryCalculator
{
    public decimal Calculate(Employee employee) => employee.Salary * 1.1m;
}
// Αποθήκευση σε βάση
public interface IEmployeeRepository
{
    void Save(Employee employee);
}
public class EmployeeRepository : IEmployeeRepository
{
    public void Save(Employee employee)
    {
        // save to DB
    }
}
Επεξήγηση:
Κάθε κλάση/interface έχει μία και μόνο ευθύνη, πράγμα που κάνει τον κώδικα πιο καθαρό και πιο εύκολο στη συντήρηση.
2️⃣ O – Open/Closed Principle (OCP)
Αρχή: Οι κλάσεις πρέπει να είναι ανοικτές για επέκταση αλλά κλειστές για τροποποίηση.
Πρόβλημα χωρίς OCP:
Αν τροποποιούμε υπάρχοντα κώδικα για να προσθέσουμε λειτουργικότητα, υπάρχει κίνδυνος να σπάσουμε υπάρχουσα λειτουργία.
Παράδειγμα:
public interface IDiscount
{
    decimal Apply(decimal price);
}
public class VipDiscount : IDiscount
{
    public decimal Apply(decimal price) => price * 0.8m;
}
public class RegularDiscount : IDiscount
{
    public decimal Apply(decimal price) => price * 0.9m;
}
Επεξήγηση:
Μπορούμε να προσθέσουμε νέα είδη εκπτώσεων (π.χ. SeasonalDiscount) χωρίς να αλλάξουμε τον υπάρχοντα κώδικα, επεκτείνοντας τη λειτουργικότητα με ασφάλεια.
3️⃣ L – Liskov Substitution Principle (LSP)
Αρχή: Οι υποκλάσεις ή υλοποιήσεις interface πρέπει να μπορούν να αντικαταστήσουν τις βασικές χωρίς να σπάει η λειτουργικότητα.
Πρόβλημα χωρίς LSP:
Αν μία υποκλάση δεν μπορεί να εκτελέσει σωστά λειτουργία της βασικής κλάσης, προκαλεί σφάλματα runtime.
Παράδειγμα με interface:
public interface IFlyable
{
    void Fly();
}
public class Sparrow : IFlyable
{
    public void Fly() { /* πετάει */ }
}
// Penguin δεν υλοποιεί IFlyable
public class Penguin
{
    // δεν πετάει, δεν αναγκάζουμε κώδικα που δεν έχει νόημα
}
Επεξήγηση:
Κανένα αντικείμενο δεν αναγκάζεται να υλοποιήσει μέθοδο που δεν μπορεί να κάνει. Ο κώδικας είναι πιο ασφαλής και σταθερός.
4️⃣ I – Interface Segregation Principle (ISP)
Αρχή: Τα interfaces πρέπει να είναι μικρά και συγκεκριμένα, όχι γενικά και μεγάλα.
Πρόβλημα χωρίς ISP:
Αν ένα interface έχει πολλές μεθόδους που δεν αφορούν όλες τις κλάσεις, οι υλοποιήσεις αναγκάζονται να γράψουν κώδικα που δεν χρειάζονται.
Παράδειγμα:
public interface IWorkable
{
    void Work();
}
public interface IFeedable
{
    void Eat();
}
public class HumanWorker : IWorkable, IFeedable
{
    public void Work() { /* δουλεύει */ }
    public void Eat() { /* τρώει */ }
}
public class RobotWorker : IWorkable
{
    public void Work() { /* δουλεύει */ }
}
Επεξήγηση:
Κάθε κλάση υλοποιεί μόνο τις λειτουργίες που πραγματικά χρειάζεται. Αποφεύγουμε περιττό κώδικα.
5️⃣ D – Dependency Inversion Principle (DIP)
Αρχή: Οι υψηλού επιπέδου μονάδες δεν πρέπει να εξαρτώνται από χαμηλού επιπέδου. Και οι δύο πρέπει να εξαρτώνται από abstraction (interface).
Πρόβλημα χωρίς DIP:
Αν μια κλάση υψηλού επιπέδου εξαρτάται από συγκεκριμένη υλοποίηση χαμηλού επιπέδου, κάθε αλλαγή στη χαμηλού επιπέδου κλάση σπάει τον κώδικα.
Παράδειγμα:
public interface ILogger
{
    void Log(string message);
}
public class FileLogger : ILogger
{
    public void Log(string message) { /* γράφει σε αρχείο */ }
}
public class DatabaseLogger : ILogger
{
    public void Log(string message) { /* γράφει σε DB */ }
}
public class EmployeeManager
{
    private readonly ILogger _logger;
    public EmployeeManager(ILogger logger)
    {
        _logger = logger;
    }
    public void AddEmployee(Employee e)
    {
        _logger.Log("Employee added");
    }
}
Επεξήγηση:
Η EmployeeManager εξαρτάται από το abstraction ILogger, όχι από συγκεκριμένη υλοποίηση. Μπορούμε να αλλάξουμε logger χωρίς να αγγίσουμε τον κώδικα της EmployeeManager.
Παράδειγμα — Απλό σενάριο logging
Χωρίς Dependency Injection (❌ Κακό)
Η κλάση δημιουργεί μόνη της το logger που χρειάζεται:
public class EmployeeManager
{
    private FileLogger _logger = new FileLogger(); // ❌ Σφιχτό δέσιμο (tight coupling)
    public void AddEmployee(Employee e)
    {
        _logger.Log("Employee added");
    }
}
📉 Τι πρόβλημα υπάρχει εδώ;
- Αν θέλεις να αλλάξεις από FileLogger σε DatabaseLogger, πρέπει να αλλάξεις τον ίδιο τον κώδικα της EmployeeManager.
- Δεν μπορείς εύκολα να το δοκιμάσεις (π.χ. να περάσεις ένα FakeLogger για testing).
Με Dependency Injection (✅ Καλό)
Τώρα, η EmployeeManager δεν ξέρει ποιο logger έχει — απλώς ξέρει ότι υπάρχει κάποιος που κάνει Log().
public interface ILogger
{
    void Log(string message);
}
public class FileLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[File] {message}");
    }
}
public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[Database] {message}");
    }
}
public class EmployeeManager
{
    private readonly ILogger _logger;
    // ✅ Η εξάρτηση εισάγεται από έξω (injection)
    public EmployeeManager(ILogger logger)
    {
        _logger = logger;
    }
    public void AddEmployee(string employeeName)
    {
        _logger.Log($"Employee '{employeeName}' added");
    }
}
Χρήση:
// Μπορείς να επιλέξεις όποιο logger θέλεις!
ILogger logger = new FileLogger();
// ή ILogger logger = new DatabaseLogger();
var manager = new EmployeeManager(logger);
manager.AddEmployee("Nikos");
📈 Τι κερδίζεις:
Ευελιξία: αλλάζεις το logger χωρίς να αλλάξεις την EmployeeManager
Testability: μπορείς να περάσεις ένα MockLogger στις δοκιμές
Καθαρό design: η EmployeeManager δεν ενδιαφέρεται πώς γίνεται το log — μόνο ότι γίνεται.
✅ Συμπέρασμα
Με τη χρήση interfaces:
Ο κώδικας γίνεται καθαρός και επεκτάσιμος.
Κάθε αρχή SOLID εφαρμόζεται με σαφήνεια.
Είναι εύκολο να προσθέτουμε νέες λειτουργίες χωρίς να σπάει ο υπάρχων κώδικας.
Δείτε στον παρακάτω σύνδεσμο αναλυτικά για το DI principle
Dependency Injection στη Clean Architecture: Πώς τα modules μένουν ανεξάρτητα
 

 
    
Top comments (0)