Object-oriented programming rests on four ideas that show up in almost every C# codebase: encapsulation, abstraction, inheritance and polymorphism. Each one solves a different problem, hiding internal state, hiding implementation detail behind a contract, reusing structure across related types and letting the same call run different code depending on the actual object involved. This article builds one small vehicle example and adds a pillar at a time, so each idea builds directly on the last.

Ο αντικειμενοστραφής προγραμματισμός στηρίζεται σε τέσσερις ιδέες που εμφανίζονται σχεδόν σε κάθε codebase C#: encapsulation, abstraction, inheritance και polymorphism. Η καθεμία λύνει διαφορετικό πρόβλημα, απόκρυψη εσωτερικής κατάστασης, απόκρυψη λεπτομέρειας υλοποίησης πίσω από ένα συμβόλαιο, επαναχρησιμοποίηση δομής σε σχετικούς τύπους και δυνατότητα η ίδια κλήση να τρέχει διαφορετικό κώδικα ανάλογα με το πραγματικό αντικείμενο. Αυτό το άρθρο χτίζει ένα μικρό παράδειγμα με οχήματα και προσθέτει έναν πυλώνα τη φορά, ώστε κάθε ιδέα να χτίζεται κατευθείαν πάνω στην προηγούμενη.

1. Encapsulation: hide the state, expose the rules

1. Encapsulation: κρύψε την κατάσταση, εξέθεσε τους κανόνες

Encapsulation means bundling data together with the logic that keeps it valid and hiding the raw data so nothing outside the class can put it into an invalid state. A Vehicle keeps its fuel level private. The only way to change it is through methods that enforce the rules:

Το encapsulation σημαίνει να δένεις τα δεδομένα μαζί με τη λογική που τα κρατά έγκυρα και να κρύβεις τα ακατέργαστα δεδομένα ώστε τίποτα έξω από την κλάση να μην μπορεί να τα βάλει σε μη έγκυρη κατάσταση. Ένα Vehicle κρατά το επίπεδο καυσίμου του private. Ο μόνος τρόπος να το αλλάξεις είναι μέσω μεθόδων που επιβάλλουν τους κανόνες:

public class Vehicle
{
    private double _fuelLevel;

    public void Refuel(double liters)
    {
        if (liters <= 0)
        {
            throw new ArgumentException("Refuel amount must be positive.");
        }

        _fuelLevel += liters;
    }

    public void Drive(double distanceKm)
    {
        var fuelNeeded = distanceKm * 0.08;

        if (fuelNeeded > _fuelLevel)
        {
            throw new InvalidOperationException("Not enough fuel for this trip.");
        }

        _fuelLevel -= fuelNeeded;
    }
}

Nothing outside Vehicle can set _fuelLevel to a negative number or drive further than the fuel allows. Every change goes through a method that checks the rule first. That is the entire point of encapsulation, not just hiding a field, but making invalid states impossible to reach from outside.

Τίποτα έξω από το Vehicle δεν μπορεί να θέσει το _fuelLevel σε αρνητικό αριθμό ή να οδηγήσει παραπάνω από όσο επιτρέπει το καύσιμο. Κάθε αλλαγή περνάει από μια μέθοδο που ελέγχει πρώτα τον κανόνα. Αυτό είναι όλο το νόημα του encapsulation, όχι απλώς η απόκρυψη ενός field, αλλά το να καθιστάς αδύνατο να φτάσεις σε μη έγκυρες καταστάσεις από έξω.

2. Abstraction: expose a contract, hide the details

2. Abstraction: εξέθεσε ένα συμβόλαιο, κρύψε τις λεπτομέρειες

Abstraction means describing what a type can do without exposing how it does it. An IVehicle only promises Start and Stop. Code written against that interface never needs to know what actually happens inside:

Το abstraction σημαίνει να περιγράφεις τι μπορεί να κάνει ένας τύπος χωρίς να εκθέτεις το πώς το κάνει. Ένα IVehicle υπόσχεται μόνο Start και Stop. Κώδικας γραμμένος πάνω σε αυτό το interface δεν χρειάζεται ποτέ να ξέρει τι πραγματικά συμβαίνει από μέσα:

public interface IVehicle
{
    void Start();
    void Stop();
}

public class Vehicle : IVehicle
{
    public void Start() => Console.WriteLine("Vehicle starting.");
    public void Stop() => Console.WriteLine("Vehicle stopping.");
}
static void StartTrip(IVehicle vehicle)
{
    vehicle.Start();
}

StartTrip only depends on IVehicle. It works with a Vehicle today and with anything else that implements Start and Stop tomorrow, an ElectricScooter for example, without a single line inside StartTrip changing. This is the same idea covered in more depth in the Dependency Injection article: depend on the shape, not on one specific implementation.

Το StartTrip εξαρτάται μόνο από το IVehicle. Δουλεύει με ένα Vehicle σήμερα και με οτιδήποτε άλλο υλοποιεί Start και Stop αύριο, ένα ElectricScooter για παράδειγμα, χωρίς να αλλάξει ούτε μία γραμμή μέσα στο StartTrip. Αυτή είναι η ίδια ιδέα που καλύπτεται με περισσότερη λεπτομέρεια στο άρθρο για το Dependency Injection: εξάρτηση από το σχήμα, όχι από μία συγκεκριμένη υλοποίηση.

3. Inheritance: share structure, add what is different

3. Inheritance: μοιράσου δομή, πρόσθεσε ό,τι διαφέρει

Inheritance lets a class reuse another class's members and add its own. Vehicle gains a Brand and a Model. Car and Motorcycle each inherit Brand, Model, Refuel, Drive, Start and Stop for free and add only what makes them different:

Το inheritance επιτρέπει σε μια κλάση να επαναχρησιμοποιήσει τα members μιας άλλης κλάσης και να προσθέσει τα δικά της. Το Vehicle αποκτά ένα Brand και ένα Model. Τα Car και Motorcycle κληρονομούν το καθένα Brand, Model, Refuel, Drive, Start και Stop δωρεάν και προσθέτουν μόνο ό,τι τα κάνει διαφορετικά:

public class Vehicle : IVehicle
{
    public string Brand { get; }
    public string Model { get; }

    public Vehicle(string brand, string model)
    {
        Brand = brand;
        Model = model;
    }

    // Refuel, Drive, Start and Stop stay exactly as before
}

public class Car : Vehicle
{
    public int Doors { get; }

    public Car(string brand, string model, int doors) : base(brand, model)
    {
        Doors = doors;
    }
}

public class Motorcycle : Vehicle
{
    public Motorcycle(string brand, string model) : base(brand, model) { }
}

Motorcycle does not repeat a single line from Vehicle. Car adds exactly one new thing, Doors. Everything else, the fuel rules, Start, Stop, comes from the base class.

Το Motorcycle δεν επαναλαμβάνει ούτε μία γραμμή από το Vehicle. Το Car προσθέτει ακριβώς ένα νέο πράγμα, το Doors. Όλα τα υπόλοιπα, οι κανόνες καυσίμου, το Start, το Stop, έρχονται από τη βασική κλάση.

4. Polymorphism: same call, different behavior

4. Polymorphism: ίδια κλήση, διαφορετική συμπεριφορά

Polymorphism means calling the same method on a Vehicle-typed reference and getting behavior specific to the actual object underneath. Add a virtual Describe method to Vehicle and override it in each derived class:

Το polymorphism σημαίνει να καλείς την ίδια μέθοδο πάνω σε μια αναφορά τύπου Vehicle και να παίρνεις συμπεριφορά ειδική για το πραγματικό αντικείμενο από κάτω. Πρόσθεσε μια virtual μέθοδο Describe στο Vehicle και κάνε την override σε κάθε παράγωγη κλάση:

public class Vehicle : IVehicle
{
    // ...

    public virtual string Describe() => $"{Brand} {Model}, a vehicle.";
}

public class Car : Vehicle
{
    // ...

    public override string Describe() => $"{Brand} {Model}, a car with {Doors} doors.";
}

public class Motorcycle : Vehicle
{
    // ...

    public override string Describe() => $"{Brand} {Model}, a motorcycle.";
}
List<Vehicle> vehicles = new()
{
    new Car("Toyota", "Corolla", 4),
    new Motorcycle("Honda", "CB500")
};

foreach (var vehicle in vehicles)
{
    Console.WriteLine(vehicle.Describe());
}
// Toyota Corolla, a car with 4 doors.
// Honda CB500, a motorcycle.

The loop only ever sees Vehicle, yet each call to Describe runs the version that matches the actual object, the car's version for the car, the motorcycle's version for the motorcycle. That distinction, along with exactly why the same code with new instead of override behaves completely differently, gets a full article of its own: Polymorphism in C#.

Το loop βλέπει μόνο Vehicle, όμως κάθε κλήση στο Describe τρέχει την έκδοση που ταιριάζει στο πραγματικό αντικείμενο, την έκδοση του αυτοκινήτου για το αυτοκίνητο, την έκδοση της μηχανής για τη μηχανή. Αυτή η διάκριση, μαζί με το γιατί ακριβώς ο ίδιος κώδικας με new αντί για override συμπεριφέρεται εντελώς διαφορετικά, καλύπτεται σε ολόκληρο ξεχωριστό άρθρο: Polymorphism σε C#.

All four pillars together, in one small runnable program:

Και οι τέσσερις πυλώνες μαζί, σε ένα μικρό εκτελέσιμο πρόγραμμα:

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

Takeaways

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

  • Encapsulation hides data behind methods that enforce the rules, so invalid states become impossible from outside the class.
  • Abstraction exposes a small contract, an interface or a set of public members, that hides the implementation behind it, so callers depend on the shape rather than on a specific class.
  • Inheritance lets a derived class reuse a base class's members and add only what is different, instead of repeating shared structure everywhere.
  • Polymorphism lets the same method call run different code depending on the actual object behind a base-typed reference, covered in full in the dedicated Polymorphism article.
  • The four pillars work together in practice. The Vehicle example above uses all four in well under fifty lines of code.
  • Το encapsulation κρύβει τα δεδομένα πίσω από μεθόδους που επιβάλλουν τους κανόνες, ώστε οι μη έγκυρες καταστάσεις να γίνονται αδύνατες από έξω από την κλάση.
  • Το abstraction εκθέτει ένα μικρό συμβόλαιο, ένα interface ή ένα σύνολο δημόσιων members, που κρύβει την υλοποίηση από πίσω, ώστε οι καλούντες να εξαρτώνται από το σχήμα και όχι από μια συγκεκριμένη κλάση.
  • Το inheritance επιτρέπει σε μια παράγωγη κλάση να επαναχρησιμοποιήσει τα members μιας βασικής κλάσης και να προσθέσει μόνο ό,τι διαφέρει, αντί να επαναλαμβάνει κοινή δομή παντού.
  • Το polymorphism επιτρέπει στην ίδια κλήση μεθόδου να τρέχει διαφορετικό κώδικα ανάλογα με το πραγματικό αντικείμενο πίσω από μια αναφορά τύπου βάσης, κάτι που καλύπτεται πλήρως στο αφιερωμένο άρθρο για το Polymorphism.
  • Οι τέσσερις πυλώνες δουλεύουν μαζί στην πράξη. Το παράδειγμα Vehicle από πάνω χρησιμοποιεί και τους τέσσερις σε πολύ λιγότερες από πενήντα γραμμές κώδικα.