CodeGym /Kurslar /C# SELF /İnterfeyslərlə abstrakt siniflərin fərqi

İnterfeyslərlə abstrakt siniflərin fərqi

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Bir medalın iki üzü

Artıq biz səninlə abstraksiya anlayışına kifayət qədər dərindən baxmışıq və C#-da onun iki ən güclü alətini – abstrakt siniflərinterfeysləri öyrənmişik. Yəqin ki, artıq hiss edirsən ki, bunlar oxşardır, çünki hər ikisi bizə davranışın "skeletini" müəyyən etməyə imkan verir, hansı ki, sonra konkret siniflər tərəfindən reallaşdırılmalıdır. Amma inan, bu, çəkic və tornavida müqayisə etmək kimidir: hər ikisi alətdir, amma fərqli işlər üçündür.

Əsasdan başlayaq: niyə bir iş üçün iki alət var? Proqramlaşdırmada, həyatda olduğu kimi, heç nə "sadəcə belə" olmur. Əgər iki oxşar alət varsa, deməli, onların özünəməxsus güclü tərəfləri və tətbiq sahələri var.

Gəlin onları qısa müqayisəli cədvəl şəklində təsəvvür edək ki, ümumi mənzərə aydın olsun. Bu bir növ "şparqalka"dır, ən vacib məqamları tez tutmağa kömək edəcək.

Xüsusiyyət Abstrakt sinif İnterfeys
Obyektin yaradılması Birbaşa yaratmaq olmaz (
new AbstractClass()
).
Birbaşa yaratmaq olmaz (
new IMyInterface()
).
Üzvlərin reallaşdırılması Aşağıdakılar ola bilər:
– Tam reallaşdırılmış metodlar/propertilər.
– Abstrakt metodlar/propertilər (reallaşdırmasız).
– Sahələr (fields), konstruktorlar, statik metodlar.
Default reallaşdırması olan metodlar, statik abstrakt və statik qeyri-abstrakt üzvlər ola bilər.
Obyekt sahələri və konstruktorlar ola bilməz.
Giriş modifikatorları İstənilən ola bilər (public, protected, internal, private). Bütün interfeys üzvləri default olaraq public olur. Onlar üçün giriş modifikatoru yazılmır (istisna: default reallaşdırmalı metodlar və statik üzvlər).
İrsi əlaqə Sinif yalnız bir abstrakt sinifdən irsi ala bilər. Sinif istənilən sayda interfeysi reallaşdıra bilər.
Əlaqə tipi "Aiddir" (is-a). Baza tipi və ümumi iyerarxiyanı müəyyən edir. "Bacara bilər" (has-a və ya can-do). Davranış/kompetensiya müqaviləsini müəyyən edir.
Vəziyyət (state) Vəziyyəti saxlaya bilər (obyekt sahələri). Statik sahələr ola bilər, amma obyekt sahələri ola bilməz.
Genişlənmə Yeni reallaşdırılmış metodlar əlavə etmək olar, irsi sinifləri dəyişmədən. Default reallaşdırmalı metodlar əlavə etmək olar, irsi sinifləri pozmadan.

Necədir, təsir edici deyil? Gəlin bu məqamları bir az daha detallı araşdıraq.

2. Çoxlu irsi əlaqə

Bu, bəlkə də ən əsas və yadda qalan fərqdir. C#-da, bir çox başqa dillərdə olduğu kimi (məsələn, Java), sinif yalnız bir valideyn sinifdən irsi ala bilər. O, adi sinif və ya abstrakt olsun – fərq etmir, yalnız bir! Bu, məşhur "romb problemi"nin (Diamond Problem) qarşısını almaq üçündür, yəni bir neçə sinifdən irsi alanda, eyni adlı metodlar varsa, hansı istifadə olunacaq – qeyri-müəyyənlik yaranır.

Amma interfeyslərdən isə sinif istədiyi qədər reallaşdıra bilər! Təsəvvür elə ki, sənin sinifin bir insan kimidir. O, "tələbə" ola bilər (Student sinifindən irsi alır), amma eyni zamanda "bişirə bilir" (ICookable), "sürə bilir" (IDriveable) və "oxuya bilir" (ISingable). Bu, çox çevikdir!


abstract class Animal
{
    public string Name;
    public abstract void MakeSound();
    public void Eat() => Console.WriteLine($"{Name} yeyir.");
}

interface IFlyable { void Fly(); double MaxFlyingAltitude { get; } }
interface ISwimable { void Swim(); }

class Duck : Animal, IFlyable, ISwimable
{
    public double MaxFlyingAltitude => 1000;
    public override void MakeSound() => Console.WriteLine($"{Name} vak-vak edir!");
    public void Fly() => Console.WriteLine($"{Name} uçur!");
    public void Swim() => Console.WriteLine($"{Name} üzür!");
}

class Program
{
    static void Main()
    {
        var duck = new Duck { Name = "Donald" };
        duck.MakeSound();
        duck.Eat();
        duck.Fly();
        duck.Swim();

        // Obyekti müxtəlif tiplərlə işlətmək:
        Animal a = duck;  a.Eat();
        IFlyable f = duck; f.Fly();
        ISwimable s = duck; s.Swim();
    }
}

Əgər sənin sinifin bir iyerarxiyanın hissəsi olmalıdırsa (məsələn, Dog aiddir Animal), onda sinifdən (abstrakt və ya adi) irsi al. Əgər sinifə hansısa bacarıq lazımdırsa (məsələn, Dog qaça bilir, Cat qaça bilir), amma bu bacarıqlar bir iyerarxiyaya aid deyil, interfeyslərdən istifadə et. İnterfeyslərin əsas üstünlüyü də budur – onlar bir-biri ilə əlaqəsi olmayan siniflər üçün davranış müqaviləsi yaradır.

3. Harada data saxlanılır, harada isə sadəcə vəd verilir?

Abstrakt siniflər istənilən şeyi saxlaya bilər:

  • Adi (abstrakt olmayan) metodlar və propertilər tam reallaşdırma ilə.
  • Abstrakt metodlar və propertilər reallaşdırmasız (onları override edirik).
  • Obyekt sahələri (fields), hansı ki, obyektin vəziyyətini saxlayır.
  • Konstruktorlar, hansı ki, bu vəziyyəti ilkinləşdirir.
  • Hətta statik metodlar və propertilər.
  • Və əlbəttə, istənilən giriş modifikatorları ola bilər: public, protected, private və s.

Bu, abstrakt sinifi qismən reallaşdırılmış davranışümumi vəziyyət üçün güclü alət edir.


abstract class Employee
{
    public string FirstName, LastName;
    public decimal Salary { get; protected set; }
    public Employee(string first, string last) { FirstName = first; LastName = last; }
    public void GetPaid(decimal sum)
    {
        Salary += sum;
        Console.WriteLine($"{FirstName} {LastName} {sum:C} aldı. Maaş: {Salary:C}");
    }
    public abstract void PerformWork();
    public abstract void TakeBreak();
}

class Developer : Employee
{
    public Developer(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} kod yazır.");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} kofe içir.");
}

class Tester : Employee
{
    public Tester(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} bug axtarır.");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} futbol oynayır.");
}

class Program
{
    static void Main()
    {
        Employee[] team = {
            new Developer("İvan", "Petrov"),
            new Tester("Mariya", "Sidorova")
        };

        foreach (var emp in team)
        {
            Console.WriteLine($"\n--- {emp.FirstName} {emp.LastName} üçün iş günü ---");
            emp.PerformWork();
            emp.GetPaid(2000);
            emp.TakeBreak();
        }
    }
}

İnterfeyslər isə tam başqa hekayədir. C# 8-ə qədər onlar yalnız elanlar (metod, property, indexer, event) saxlayırdı. Heç bir sahə, konstruktor, metod reallaşdırması yox idi! Sadəcə metodun "imzası", bədən olmadan. Və bütün üzvlər default olaraq public idi (sən public yazmasan da). Bu, interfeysin "təmiz müqavilə" olmasına zəmanət verirdi, heç bir reallaşdırma və ya gizli vəziyyət olmadan.

C# 8-dən başlayaraq interfeyslər bir az "qalınlaşdı" və default reallaşdırmalı metodlar (Default Interface Methods) və statik üzvlər əldə etdi. Bu, artıq mövcud interfeyslərə yeni metodlar əlavə etməyə imkan verir, milyonlarla kod sətrini "sındırmadan". Amma yenə də, interfeyslər obyekt sahələri və konstruktorlar saxlaya bilmir. Bu əsas məhdudiyyət saxlanılır ki, interfeyslər "davranış müqaviləsi" olaraq qalsın, "state" saxlayan olmasın.


interface ISaveable
{
    void Save(string file); 
    bool IsDirty { get; }
}

interface ILoadable
{
    void Load(string file);
}

class GameProgress : ISaveable, ILoadable
{
    public int Level { get; set; }
    public string PlayerName { get; set; }
    bool _isDirty = true;
    public bool IsDirty => _isDirty;

    public GameProgress(string name, int level)
    {
        PlayerName = name; Level = level;
    }
    public void Save(string file)
    {
        Console.WriteLine($"Yadda saxla: {PlayerName}, səviyyə {Level} -> {file}");
        _isDirty = false;
    }
    public void Load(string file)
    {
        Console.WriteLine($"Yüklə {file}-dən");
        PlayerName = "Yeni oyunçu"; Level = 5; _isDirty = true;
    }
    public void UpdateProgress(int newLevel)
    {
        Level = newLevel; _isDirty = true;
        Console.WriteLine($"Yeniləndi {Level}-ə.");
    }
}

class Program
{
    static void Main()
    {
        var game = new GameProgress("Qəhrəman", 1);
        game.UpdateProgress(3);

        ISaveable saver = game;
        if (saver.IsDirty) saver.Save("save.dat");

        ILoadable loader = game;
        loader.Load("save.dat");
        Console.WriteLine($"Yükləmədən sonra: {game.PlayerName}, {game.Level}");
    }
}

Nəticə: Əgər sənə baza sinif lazımdırsa, hansı ki, təkcə müqavilə yox, həm də artıq reallaşdırılmış kod (ümumi məntiq) və ya ümumi vəziyyət saxlayır, abstrakt sinif seç. Əgər sadəcə "check-list" və ya "davranış müqaviləsi" lazımdırsa, heç bir reallaşdırma və ya state olmadan, interfeys seç.

4. İnterfeysləri nə vaxt istifadə etməli?

İnterfeyslər əladır, əgər sən bacarıq və ya davranış müəyyən etmək istəyirsənsə, hansı ki, tamamilə fərqli, əlaqəsiz obyektlərdə ola bilər. Məsələn:

  • IDisposable: istifadə olunduqdan sonra düzgün "azad edilməli" obyektlər (fayl, şəbəkə bağlantısı, verilənlər bazası).
  • IEnumerable<T>: foreach ilə dövr etmək mümkün olan obyektlər.
  • IComparable<T>: eyni tipli başqa obyektlə müqayisə oluna bilən obyektlər.

Vacibdir ki, FileStreamSqlConnection IDisposable reallaşdıra bilər, List<T>Dictionary<TKey, TValue> isə IEnumerable<T> reallaşdıra bilər. Bu siniflər tamamilə fərqli iyerarxiyalara aiddir, amma onların ümumi bacarığı var, interfeys tərəfindən müəyyən olunur.

Nümunə: Təsəvvür elə bir sistemin var, MaşınTəyyarə var. Hər ikisi NəqliyyatVasitəsi ola bilər (bəlkə, abstrakt sinif). Amma həm Maşın, həm Təyyarə, hətta Qayıq (əgər əlavə etsən) – hərəkət edə bilər. Bu "hərəkət etmək" bacarığı üçün IMovable interfeysi əla seçimdir.


interface IMovable
{
    void Move(int distance);
}

class Car : IMovable
{
    public string Brand;
    public Car(string brand) => Brand = brand;
    public void Move(int d) => Console.WriteLine($"{Brand} {d} km gedir.");
}

class Airplane : IMovable
{
    public string Model;
    public Airplane(string model) => Model = model;
    public void Move(int d) => Console.WriteLine($"{Model} {d} km uçur.");
}

class Human : IMovable
{
    public string Name;
    public Human(string name) => Name = name;
    public void Move(int d) => Console.WriteLine($"{Name} {d} m gedir.");
}

class Program
{
    static void Main()
    {
        IMovable[] movers = { new Car("Toyota"), new Airplane("Boeing 747"), new Human("Artur") };
        foreach (var item in movers)
            item.Move(100);
    }
}

Gördün? Biz IMovable siyahısı yarada və Move() metodunu hər bir element üçün çağıra bilərik, bu Car, Airplane və ya Human olduğunu bilmədən. Bu, interfeyslər vasitəsilə polimorfizmin gücüdür.

5. Abstrakt sinifləri nə vaxt istifadə etməli?

Abstrakt siniflər idealdır, əgər sən ümumi baza funksionallığı müəyyən etmək istəyirsənsə, hansı ki, bir-birinə sıx bağlı siniflər qrupu üçün lazımdır və onlar aiddir bir ümumi şeyə. Onlar hazır kod verir, hansı ki, bütün irsi siniflər üçün eynidir, və eyni zamanda irsi sinifləri özlərinə xas hissələri reallaşdırmağa məcbur edir.

Təsəvvür elə, bir neçə bank hesabı növün var: SavingAccount (əmanət), CheckingAccount (hesablaşma), CreditAccount (kredit). Onların hamısı aiddir BankAccount-a. Hamısında balans var (Balance), hamısı hesaba pul qoyur (Deposit). Amma pul çıxarma qaydası (Withdraw) hərəsində fərqlidir. Burada abstrakt sinif BankAccount köməyə gəlir!


abstract class BankAccount
{
    public string AccountNumber { get; }
    public decimal Balance { get; protected set; }
    public BankAccount(string acc) { AccountNumber = acc; }
    public void Deposit(decimal sum)
    {
        if (sum > 0)
        {
            Balance += sum;
            Console.WriteLine($"{AccountNumber}: +{sum:C}, Balans: {Balance:C}");
        }
    }
    public abstract bool Withdraw(decimal sum);
}

class CheckingAccount : BankAccount
{
    public CheckingAccount(string acc) : base(acc) { }
    public override bool Withdraw(decimal sum)
    {
        if (Balance >= sum)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, Balans: {Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: Kifayət qədər vəsait yoxdur");
        return false;
    }
}

class CreditAccount : BankAccount
{
    public decimal CreditLimit { get; }
    public CreditAccount(string acc, decimal limit) : base(acc) => CreditLimit = limit;
    public override bool Withdraw(decimal sum)
    {
        if (Balance - sum >= -CreditLimit)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, Balans: {Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: Limit aşılıb");
        return false;
    }
}

class Program
{
    static void Main()
    {
        var checking = new CheckingAccount("12345");
        checking.Deposit(1000);
        checking.Withdraw(300);
        checking.Withdraw(800);

        Console.WriteLine("\n--- Kredit hesabı ---");
        var credit = new CreditAccount("67890", 500);
        credit.Deposit(200);
        credit.Withdraw(400);
        credit.Withdraw(400);
    }
}

Burada BankAccount bütün hesablar üçün ümumi məntiqi (Deposit) və hesab nömrəsi, balansı idarə edir. Amma pul çıxarma məntiqi (Withdraw) fərqlidir, ona görə də o, abstraktdır.

6. Onlar birlikdə işləyəndə: ideal cütlük

Ən zövqlü və güclü dizayn tez-tez abstrakt siniflə interfeysin birgə istifadəsini ehtiva edir. Abstrakt sinif özü bir və ya bir neçə interfeysi reallaşdıra bilər!

Təsəvvür elə: səndə abstract Animal var, hansı ki, baza şeyləri müəyyən edir. Amma bəzi Animal həm də IMovable, ICarnivore, IPredator və s. ola bilər. Sənin Animal hətta IMovable üçün baza reallaşdırma verə bilər (məsələn, Move(int speed) metodu), amma sonra konkret siniflər, məsələn, Lion və ya Fish, bu reallaşdırmanı özlərinə uyğun dəyişəcək.


public interface ISaveable
{
    void SaveState(string path);
    bool HasChanges { get; }
}

public class Vector3
{
    public float X, Y, Z;
    public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
    public override string ToString() => $"({X}, {Y}, {Z})";
}

public abstract class GameObject
{
    public string Id { get; }
    public Vector3 Position { get; }
    protected GameObject(string id, Vector3 pos) { Id = id; Position = pos; }
    public abstract void Update();
    public void Destroy() => Console.WriteLine($"{Id} məhv edildi");
}

public class Player : GameObject, ISaveable
{
    public int Health { get; private set; }
    private bool _hasChanges = true;
    public bool HasChanges => _hasChanges;

    public Player(string id, Vector3 pos, int health) : base(id, pos) => Health = health;

    public override void Update() =>
        Console.WriteLine($"{Id} yenilənir, HP: {Health}, Pos: {Position}");

    public void TakeDamage(int dmg)
    {
        Health -= dmg;
        _hasChanges = true;
        Console.WriteLine($"{Id} {dmg} zərər aldı. HP: {Health}");
    }

    public void SaveState(string path)
    {
        Console.WriteLine($"Yadda saxla {Id} (HP:{Health}) {path}-ə");
        _hasChanges = false;
    }
}

class GameEngine
{
    static void Main()
    {
        var player = new Player("P1", new Vector3(0, 0, 0), 100);
        player.Update();
        player.TakeDamage(20);

        ISaveable saver = player;
        if (saver.HasChanges) saver.SaveState("save.json");

        GameObject obj = player;
        obj.Update();
        player.Destroy();
    }
}

Bu nümunədə Player aiddir GameObject-ə (çünki ondan irsi alır, Id, PositionDestroy metodunu istifadə edir). Eyni zamanda Player yadda saxlanıla bilər (çünki ISaveable interfeysini reallaşdırır). Bu, çox çevik və güclü yanaşmadır!

7. Nüanslar və tipik səhvlər

Abstrakt sinifdən obyekt yaratmağa cəhd:

Animal myAnimal = new Animal();
– bu, yeni başlayanların tez-tez etdiyi səhvdir. Unutma: abstrakt sinif – şablondur, hazır obyekt deyil. Kompilyator dərhal xəbərdarlıq edəcək.

Abstrakt metodları reallaşdırmağı unutmaq: Əgər sən abstrakt sinifdən irsi alırsansa və sənin sinifin abstrakt deyil, mütləq baza sinifin bütün abstrakt metodlarını override etməlisən. Əks halda kompilyator yenə də xəbərdarlıq edəcək.

İnterfeysə sahə əlavə etməyə cəhd: Bu, interfeysləri fərqləndirən "olmaz"lardan biridir. C# 8+ ilə statik sahələr gəldi, amma obyekt sahələri (yəni konkret obyektə aid olanlar) hələ də qadağandır. İnterfeys – davranış müqaviləsidir, data saxlayan deyil.

virtualabstract qarışdırılması:

  • virtual metod/property default reallaşdırmaya malikdir və ola bilər irsi sinifdə override olunsun.
  • abstract metod/property reallaşdırmasızdırmütləq ilk qeyri-abstrakt irsi sinifdə override olunmalıdır.
  • virtual istifadə etmək, məntiq həmişə fərqlidirsə, boş reallaşdırmalar yazmağa və sonra onları override etməyə gətirib çıxarır. Bu, abstract-dan daha qeyri-dəqiq müqavilədir.

Bu fərqləri başa düşmək və abstrakt siniflə interfeys arasında düzgün seçim etmək hər bir C# proqramçısı üçün əsas bacarıqdır. Bu, sadəcə sintaksis məsələsi deyil, arxitektura düşüncəsidir. Sistem dizayn edəndə özünə sual ver: "Bu siniflər ümumi 'aiddir' iyerarxiyasına malikdir?" və "Bu siniflərin əlaqəsiz tiplər üçün lazım olan ümumi 'bacarığı' var?". Cavablar sənə düzgün seçim etməyə kömək edəcək.

Növbəti mühazirələrdə interfeyslərin daha inkişaf etmiş imkanlarını öyrənəcəyik, hansı ki, C#-ın son versiyalarında peyda olub. Bizimlə qal!

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION