CodeGym /Kurslar /C# SELF /Polimorfizm və abstraksiyada problemlər

Polimorfizm və abstraksiyada problemlər

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

1. Polimorfizm həmişə sehr deyil

Əgər ilk nümunələrə baxsaq, C#-da polimorfizm tam məntiqlə işləyir kimi görünür: miras al, override et, hər şeyi baza tipindən çağır — və hər şey işləyir. Amma praktikada bəzi nüanslar ortaya çıxır. Gəlin onlara yaxından baxaq.

Metodların gizlədilməsi və new açar sözü

Təsəvvür elə ki, bizdə baza və törəmə sinif var, hər ikisində eyni adlı metod elan olunub, amma override açar sözü olmadan. Əgər törəmə sinifdə bu metod yenidən təyin olunubsa, amma override olmadan, o zaman bu metod baza metodunun reallaşdırmasını gizlədir, amma onu override etmir. Kompilyator, qayğıkeş valideyn kimi, dərhal xəbərdarlıq edəcək və açıq şəkildə new yazmağı təklif edəcək:

class Animal
{
    public void Speak()
    {
        Console.WriteLine("Heyvan səs çıxarır.");
    }
}

class Cat : Animal
{
    public new void Speak()
    {
        Console.WriteLine("Miyau!");
    }
}

// İstifadə
Animal animal = new Cat();
animal.Speak(); // Çap edəcək: "Heyvan səs çıxarır."

Vau! Hətta əgər biz Cat tipli obyekt yaradıb onu Animal tipli dəyişəndə saxlasaq belə, çağırışda baza sinifin orijinal metodu işləyəcək. Niyə? Çünki metod virtual kimi elan olunmayıb! Tələ nömrə bir: əgər polimorfizmdən istifadə etmək istəyirsənsə, virtual və override açar sözlərini unutma. new yalnız o zaman istifadə et ki, məqsədin metodu gizlətməkdir, override etmək yox (bu isə çox nadir hallarda və əsaslı səbəblə lazımdır).

Konstruktor çağırışları və polimorfizm

Daha bir gözlənilməz məqam: konstruktorlar virtual deyil. Əgər baza sinifdə konstruktor elan etsən, törəmə sinifdə də öz konstruktorunu yazsan, onlar polimorfik olmayacaq. Nümunə:

class Animal
{
    public Animal()
    {
        Console.WriteLine("Animal konstruktoru");
    }
}

class Cat : Animal
{
    public Cat()
    {
        Console.WriteLine("Cat konstruktoru");
    }
}

// İstifadə
Animal animal = new Cat();
// Çap edəcək:
// Animal konstruktoru
// Cat konstruktoru

Amma əgər baza sinifin konstruktorunda override oluna bilən metod çağırırsansa, nəticə gözlənilməz ola bilər — virtual metod törəmə obyektin tam inicializasiyasından əvvəl çağırılacaq! Buna görə konstruktorlarda virtual/abstrakt metodları çağırmaq olmaz.

override zamanı enkapsulyasiyanın "qırılması" problemi

Virtual metodlar yaxşıdır, amma əgər baza sinifdə hansısa metodun konkret davranışına güvənmisənsə, sonra isə bu metod törəmə sinifdə override olunub və logikanı pozursa — gözlənilməz bug-lar çıxacaq.

class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("Heyvan yeyir.");
    }

    public void Live()
    {
        Eat(); // İstənilən override olunmuş versiyanı çağıra bilər!
    }
}

class Cat : Animal
{
    public override void Eat()
    {
        Console.WriteLine("Pişik balıq yeyir.");
    }
}

Animal a = new Cat();
a.Live(); // Çap edəcək "Pişik balıq yeyir."

Əgər baza sinifdə Animal metod Eat() "heyvan yeyir" yazırdısa, sonra törəmə sinifdə override olunmuş Eat()-də təhlükəli nəsə əlavə olunsa, bu bütün sinifin işini poza bilər. Bu problemə Barbara Liskov-un əvəzləmə prinsipinin pozulması (Liskov Substitution Principle, LSP) deyilir. Dizayn edəndə həmişə düşün: törəmə siniflərin davranışı baza sinifə uyğun qalacaqmı?

Cast və tip çevirmə problemləri

Polimorfizm müxtəlif obyektləri bir "yığın"da saxlamağa imkan verir: məsələn, baza tipli siyahıda həm itlər, həm pişiklər (hər ikisi Animal-dan miras alır) ola bilər. Amma əgər spesifik nəsə çağırmaq istəsən:

List<Animal> pets = new List<Animal> { new Cat(), new Dog() };

foreach (var pet in pets)
{
    if (pet is Cat cat)
    {
        cat.Purr();
    }
}

Əgər tip yoxlamasını unudub ehtiyatsız cast etsən, InvalidCastException səhvi alacaqsan. Bəzən bu, həddindən artıq tip yoxlamalarına, kodun çətinləşməsinə və dizaynın təkmilləşdirilməsinə ehtiyac olduğuna işarədir.

2. Abstraksiya problemləri

Abstraksiya — istifadəçiyə obyektlə işləməyi asanlaşdıran və daxili vəziyyətə çıxışı məhdudlaşdıran əla alətdir. Amma burada da tələlər var!

Abstraksiya səviyyəsində ifratçılıq (Over-Abstraction)

Bəzi yeni başlayan (və təkcə onlar yox) proqramçılar "düzgün OOP" həvəsinə düşüb, baza siniflər, interfeyslər və abstrakt qatlar ilə qat-qat "tort" düzəldirlər. Nəticədə, necə işlədiyini müəllif özü belə başa düşmür.

interface IAnimal
{
    void Speak();
}

abstract class Feline : IAnimal
{
    public abstract void Speak();
}

class Cat : Feline
{
    public override void Speak()
    {
        Console.WriteLine("Miyau!");
    }
}

Görəsən, arada heç nə əlavə etməyən abstrakt sinif nəyə lazımdır? Abstraksiya naminə abstraksiya dəstəyin çətinləşməsinə və arxitekturanın dolaşıqlığına gətirib çıxarır.

Düşünülməmiş iyerarxiya

Fikirləş, əgər hansısa əməli iyerarxiyanın ən yuxarısına çıxarsan, amma əslində o, hamıya uyğun deyil:

abstract class Animal
{
    public abstract void Fly();
}
class Cat : Animal
{
    public override void Fly()
    {
        throw new NotImplementedException("Pişiklər uça bilmir!");
    }
}

Ya törəmə siniflərdə exception atan stub-lar yazmalısan, ya da interfeysin reallığı əks etdirməməsi ilə barışmalısan. Bu tipik anti-pattern — "səhv iyerarxiya"dır. Belə hallarda metodları ayrıca interfeyslərə çıxarmaq daha yaxşıdır (məsələn, IFlyable).

Məsləhət: bir abstraksiyanı "hər şey üçün" etməyə çalışma.

Abstrakt siniflər və API dəyişiklikləri problemləri

Abstrakt sinif prodakşna çıxan kimi və ondan miras alanlar yaranan kimi, istənilən dəyişiklik riskli olur. Yeni abstrakt metod əlavə etmək bütün törəmə siniflərdə onun reallaşdırılmasını tələb edir — yoxsa kod ümumiyyətlə kompilyasiya olunmayacaq. Bu, kitabxanaların və publik API-lərin dəstəyini xeyli çətinləşdirir.

Məhz belə hallar üçün default implementasiyalı interfeyslər (Default Interface Methods — bax mühazirə 116) yaradılıb: onlar interfeysləri genişləndirməyə imkan verir, mövcud kodu dərhal dəyişməyə ehtiyac olmadan.

Abstraksiyada enkapsulyasiyanın pozulması

Sinifi abstrakt edəndə, çox vaxt onun üzvlərini protected elan etməli olursan ki, törəmə siniflər onlara çata bilsin. Bu isə tez-tez daxili logikanın sızmasına gətirib çıxarır, halbuki onu gizlətmək daha yaxşı olardı. Nəticədə, törəmə siniflər baza sinifin daxili bütövlüyünü poza biləcək məlumat və əməliyyatlara çıxış əldə edir.

3. Praktik səhv ssenariləri

Elə düşünmə ki, bunlar yalnız ev tapşırıqlarında olur — real həyatda da, bəzən təcrübəli proqramçılar belə bu tələlərə düşür.

virtual kimi elan olunmayan metodlarla nümunə

Tutaq ki, biz loglama üçün tədris tətbiqimizi genişləndiririk (bax Gün 24). Baza logger-imiz var:

class BaseLogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

class FileLogger : BaseLogger
{
    public void Log(string message)
    {
        // Fayla yazırıq
        Console.WriteLine("Fayla: " + message);
    }
}

// İstifadə:
BaseLogger logger = new FileLogger();
logger.Log("Salam!"); // Gözlənilən: "Fayla: Salam!", reallıq: "Salam!"

FileLogger implementasiyası düşündü ki, metodu override edib, amma override əlavə etməyi və baza metodu virtual etməyi unutdu. Nəticədə çağırış baza versiyaya gedir.

Tövsiyə: override oluna bilən metodları həmişə baza sinifdə virtual, törəmə sinifdə isə override kimi işarələ.

Səhv abstraksiya nümunəsi: "Çevik" heyvanlar

Heyvanlar mövzusunu davam etdiririk! IFlyable interfeysini ayırırıq ki, bütün heyvanları Fly metodunu reallaşdırmağa məcbur etməyək:

interface IFlyable
{
    void Fly();
}

class Bird : IFlyable
{
    public void Fly() => Console.WriteLine("Quş uçur!");
}

class Cat
{
    // Pişik IFlyable reallaşdırmır
}

İndi "uçanlar"la işləyən funksiya yaza bilərik, pişiklərə toxunmadan:

void MakeItFly(object creature)
{
    if (creature is IFlyable flyingThing)
    {
        flyingThing.Fly();
    }
    else
    {
        Console.WriteLine("Bu heyvan uça bilmir.");
    }
}

Belə yanaşma arxitekturanı saxta abstrakt metodlarla pozmamağa imkan verir.

Genişlənmədə "sərt" abstrakt siniflərlə problemlər

Təsəvvür elə ki, belə abstrakt siniflə kitabxana buraxmısan:

public abstract class Creature
{
    public abstract void DoAction();
}

Kitabxananı istifadə edənlər bu sinifdən miras alan öz siniflərini yaradır. Bir ildən sonra API-ni genişləndirmək istəyirsən və əlavə edirsən:

public abstract class Creature
{
    public abstract void DoAction();
    public abstract void Sleep(); // Yeni metod!
}

İndi bütün istifadəçi sinifləri kompilyasiya olunmur, çünki yeni abstrakt metodu reallaşdırmaq məcburiyyətindədirlər. Bu, abstraksiyaların dizaynında çox ehtiyatlı olmağa və mümkün qədər default metodlu interfeyslərə üstünlük verməyə səbəbdir.

4. Tipik tələlərdən yayınmaq üçün məsləhətlər

Tətbiqləriniz gimnastlar kimi çevik olsun, amma ayaqlarınızı sındırmayın!

  • Mirasdan həddindən artıq istifadə etmə: əgər kompozisiya (bir obyektin digərinə daxil edilməsi) ilə keçinmək mümkündürsə, elə et.
  • Metodları yalnız həqiqətən override etmək lazım olduğunu biləndə virtual et.
  • Mənasız abstrakt siniflər elan etmə və "böyüyəndə lazım olar" deyə iyerarxiya qurma.
  • Metodları override edəndə logikanın düzgünlüyünü yoxla: baza sinifin invariantlarını (qaydalarını) pozma.
  • Publik baza sinif və interfeyslərə kitabxananı buraxdıqdan sonra yeni abstrakt metodlar əlavə etmə.
  • Proqram hissələri arasında zəif əlaqə üçün interfeyslərdən istifadə et.
  • API-ni genişləndirmək üçün Default Interface Methods istifadə et.
2
Tapşırıq
C# SELF, səviyyə, dərs
Bağlanıb
`new` açar sözündən istifadə edərək metodu gizlətmək
`new` açar sözündən istifadə edərək metodu gizlətmək
2
Tapşırıq
C# SELF, səviyyə, dərs
Bağlanıb
Abstrakt siniflərdən və ixtisaslaşma prinsipindən istifadə
Abstrakt siniflərdən və ixtisaslaşma prinsipindən istifadə
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION