An interface and an abstract class both let other code depend on a contract instead of a concrete type, but they solve different problems and follow different rules. Mixing them up, or reaching for the wrong one out of habit, is a common source of awkward designs. This article builds one small shape example that uses both at once, shows exactly where they differ and connects the choice back to how the built-in DI container wires everything together.

Ένα interface και μια abstract class επιτρέπουν και τα δύο σε άλλον κώδικα να εξαρτάται από ένα συμβόλαιο αντί για συγκεκριμένο τύπο, αλλά λύνουν διαφορετικά προβλήματα και ακολουθούν διαφορετικούς κανόνες. Το να τα μπερδεύεις, ή να διαλέγεις το λάθος από συνήθεια, είναι συχνή πηγή αδέξιων σχεδιασμών. Αυτό το άρθρο χτίζει ένα μικρό παράδειγμα με σχήματα που χρησιμοποιεί και τα δύο μαζί, δείχνει ακριβώς πού διαφέρουν και συνδέει την επιλογή με το πώς το ενσωματωμένο DI container συνδέει τα πάντα.

What each one actually is

Τι είναι πραγματικά το καθένα

An interface is a pure contract: a list of members with no implementation and no state. A class or struct can implement as many interfaces as it needs. An abstract class can mix abstract members, which force a derived class to provide an implementation, with concrete members that already have real, shared code and it can hold fields and state. A class can inherit from only one class, abstract or not.

Ένα interface είναι ένα καθαρό συμβόλαιο: μια λίστα από members χωρίς υλοποίηση και χωρίς κατάσταση. Μια class ή struct μπορεί να υλοποιήσει όσα interfaces χρειάζεται. Μια abstract class μπορεί να συνδυάσει abstract members, που αναγκάζουν μια παράγωγη κλάση να δώσει υλοποίηση, με concrete members που ήδη έχουν πραγματικό, κοινό κώδικα και μπορεί να κρατά fields και κατάσταση. Μια κλάση μπορεί να κάνει inherit από μόνο μία κλάση, abstract ή όχι.

public interface IShape
{
    double CalculateArea();
}

public abstract class Shape : IShape
{
    public string Name { get; }

    protected Shape(string name)
    {
        Name = name;
    }

    public abstract double CalculateArea();

    public void PrintInfo() // real, shared code, not possible in a plain interface
    {
        Console.WriteLine($"{Name}: area = {CalculateArea():F2}");
    }
}

IShape promises nothing but CalculateArea. Shape promises the same thing, but also hands every derived class a working Name property and a ready-made PrintInfo method, real code that an interface alone could never provide.

Το IShape υπόσχεται μόνο το CalculateArea. Το Shape υπόσχεται το ίδιο πράγμα, αλλά επίσης δίνει σε κάθε παράγωγη κλάση μια έτοιμη ιδιότητα Name και μια έτοιμη μέθοδο PrintInfo, πραγματικό κώδικα που ένα interface από μόνο του δεν θα μπορούσε ποτέ να δώσει.

The same hierarchy, both at once

Η ίδια ιεραρχία και τα δύο μαζί

Circle and Rectangle both extend Shape and inherit PrintInfo for free:

Τα Circle και Rectangle κάνουν και τα δύο extend το Shape και κληρονομούν το PrintInfo δωρεάν:

public class Circle : Shape
{
    private readonly double _radius;

    public Circle(double radius) : base("Circle")
    {
        _radius = radius;
    }

    public override double CalculateArea() => Math.PI * _radius * _radius;
}

public class Rectangle : Shape
{
    private readonly double _width;
    private readonly double _height;

    public Rectangle(double width, double height) : base("Rectangle")
    {
        _width = width;
        _height = height;
    }

    public override double CalculateArea() => _width * _height;
}

Circle already spent its one inheritance slot on Shape. It can still pick up extra behavior by implementing another interface, something no second abstract class could ever offer:

Το Circle έχει ήδη ξοδέψει τη μία του θέση κληρονομικότητας στο Shape. Μπορεί ακόμα να αποκτήσει επιπλέον συμπεριφορά υλοποιώντας ένα ακόμα interface, κάτι που καμία δεύτερη abstract class δεν θα μπορούσε ποτέ να προσφέρει:

public interface IComparableShape
{
    int CompareTo(IShape other);
}

public class Circle : Shape, IComparableShape
{
    // ...

    public int CompareTo(IShape other) => CalculateArea().CompareTo(other.CalculateArea());
}

That single line, Shape, IComparableShape, is the whole point. One base class, any number of interfaces. This is exactly why interfaces, not abstract classes, are the usual tool for describing a capability a type happens to have on the side.

Αυτή η μία γραμμή, Shape, IComparableShape, είναι όλο το νόημα. Μία βασική κλάση, όσα interfaces χρειάζεται. Γι' αυτό ακριβώς τα interfaces, όχι οι abstract classes, είναι το συνηθισμένο εργαλείο για να περιγράψεις μια ικανότητα που τυχαίνει να έχει ένας τύπος στο πλάι.

How this connects to DI

Πώς συνδέεται αυτό με το DI

A class that needs a shape does not need Shape, the base class, or Circle, the concrete type. It only needs the contract:

Μια κλάση που χρειάζεται ένα σχήμα δεν χρειάζεται το Shape, τη βασική κλάση, ή το Circle, τον συγκεκριμένο τύπο. Χρειάζεται μόνο το συμβόλαιο:

public class ShapeReportService
{
    private readonly IShape _shape;

    public ShapeReportService(IShape shape)
    {
        _shape = shape;
    }

    public void Report() => Console.WriteLine($"Report -> area: {_shape.CalculateArea():F2}");
}

This is precisely the pattern covered in the Dependency Injection article: ShapeReportService depends on an abstraction, a container hands it whichever implementation is registered, builder.Services.AddScoped<IShape, Circle>();. Swapping Circle for Rectangle never touches ShapeReportService. Nothing here is special to interfaces, an abstract class registered the same way, AddScoped<Shape, Circle>(), would work too. The reason interfaces win almost every time as the injected type is the same reason Circle could still pick up IComparableShape above: a class only gets one base class and a dependency should never be the thing that spends it. Reserve an abstract class for genuinely shared implementation between closely related subclasses, not for the type other classes ask for in their constructor.

Αυτό είναι ακριβώς το pattern που καλύπτεται στο άρθρο για το Dependency Injection: το ShapeReportService εξαρτάται από μια αφαίρεση, ένα container του δίνει όποια υλοποίηση είναι καταχωρημένη, builder.Services.AddScoped<IShape, Circle>();. Η αντικατάσταση του Circle με Rectangle δεν αγγίζει ποτέ το ShapeReportService. Τίποτα εδώ δεν είναι ειδικό για τα interfaces, μια abstract class καταχωρημένη με τον ίδιο τρόπο, AddScoped<Shape, Circle>(), θα δούλευε επίσης. Ο λόγος που τα interfaces κερδίζουν σχεδόν πάντα ως ο τύπος που γίνεται inject είναι ο ίδιος λόγος που το Circle μπόρεσε να αποκτήσει το IComparableShape από πάνω: μια κλάση παίρνει μόνο μία βασική κλάση και μια εξάρτηση δεν πρέπει ποτέ να είναι αυτό που την ξοδεύει. Κράτησε μια abstract class για πραγματικά κοινή υλοποίηση ανάμεσα σε στενά συγγενικές υποκλάσεις, όχι για τον τύπο που ζητούν άλλες κλάσεις στον constructor τους.

Everything above together, runnable and editable:

Όλα τα παραπάνω μαζί, εκτελέσιμα και επεξεργάσιμα:

Live example, editable, runs on .NET Fiddle
Ζωντανό παράδειγμα, επεξεργάσιμο, τρέχει στο .NET Fiddle

Takeaways

Συμπεράσματα

  • An interface is a pure contract, no fields, no shared implementation. An abstract class can mix abstract members with real, shared code and can hold state.
  • A class can implement any number of interfaces but can extend only one class, abstract or not. That single slot is precious.
  • Use an interface for the contract other code depends on. Reserve an abstract class for shared implementation between closely related subclasses.
  • When registering a service with the DI container, prefer an interface as the injected type. It keeps the one inheritance slot free for whatever the implementation actually needs.
  • This is the same idea from the Dependency Injection article, applied one level deeper: depend on the shape, not the implementation and let the shape itself usually be an interface rather than a base class.
  • Ένα interface είναι καθαρό συμβόλαιο, χωρίς fields, χωρίς κοινή υλοποίηση. Μια abstract class μπορεί να συνδυάσει abstract members με πραγματικό, κοινό κώδικα και να κρατά κατάσταση.
  • Μια κλάση μπορεί να υλοποιήσει όσα interfaces θέλει αλλά να κάνει extend μόνο μία κλάση, abstract ή όχι. Αυτή η μία θέση είναι πολύτιμη.
  • Χρησιμοποίησε interface για το συμβόλαιο από το οποίο εξαρτάται άλλος κώδικας. Κράτησε την abstract class για κοινή υλοποίηση ανάμεσα σε στενά συγγενικές υποκλάσεις.
  • Όταν καταχωρείς μια υπηρεσία στο DI container, προτίμησε ένα interface ως τον τύπο που γίνεται inject. Κρατάει ελεύθερη τη μία θέση κληρονομικότητας για ό,τι χρειάζεται πραγματικά η υλοποίηση.
  • Αυτή είναι η ίδια ιδέα από το άρθρο για το Dependency Injection, εφαρμοσμένη ένα επίπεδο πιο βαθιά: εξάρτηση από το σχήμα, όχι από την υλοποίηση και άσε το ίδιο το σχήμα να είναι συνήθως interface αντί για βασική κλάση.