Ära kutsu virtuaalseid meetode klassi konstruktoris

11.07.2011  |  Gunnar

Keeruline juttÜks halbu nähtusi, millele ma aeg-ajalt peale satun, on see, et C# klasside konstruktoris tehakse pöördumisi virtuaalsetele omadustele ja meetoditele. Peamiselt jookseb selle probleemiga kokku O/R-mappereid kasutades. Mis on selle probleemi olemus ja mis pahad asjad võivad juhtuda – sellest siinkohal üks kiire ülevaade.

virtual – oluline erinevus C# ja Java vahel

Akadeemilist tausta omav Java ja praktikute maailmast välja kasvanud C# on küll süntaksilt sarnased, kuid nende sisemises toimimises on päris üksjagu erinevat. Võtmesõna virtual, mis tähendab seda, et päriv klass võib antud omaduse või meetodi enda versiooniga asendada, on üks ehe näide sellest.

Java korral loetakse kõik väljapoole nähtavad klassi osad virtuaalseteks. C# toimib vastupidi – mida soovid, et teised klassid saaks oma versiooniga asendada, tuleb ise virtuaalseks märkida.

Millest selline erinevus?

C# keele autor Anders Hejlsbergi kinnitusel on üheks põhjuseks jõudlus. Näiteks virtuaalsete meetodite kutsumisel on vaja rakendust jooksutaval mootoril teha rohkem tööd kui seda on vaja “tavaliste” meetodite korral.

Täiendav töö seisneb selles, et objekti tüübi põhjal otsitakse virtuaalsete meetodite tabelist üles see õige override, mis välja tuleb kutsuda.

Peamine probleem taandub inimfaktorile

Kuid see pole ainus probleem. Kui jõuame inimesteni, siis olukord muutub oluliselt ja võimalikud tagajärjed omandavad sootuks uued mõõtmed.

Väljavõte intervjuust Anders Hejlsbergiga (soovitan soojalt selle intervjuu läbi lugeda, see ei ole pikk):

When we make something virtual in a platform, we're making an awful lot of promises about how it evolves in the future. For a non-virtual method, we promise that when you call this method, x and y will happen. When we publish a virtual method in an API, we not only promise that when you call this method, x and y will happen. We also promise that when you override this method, we will call it in this particular sequence with regard to these other ones and the state will be in this and that invariant.

Every time you say virtual in an API, you are creating a call back hook. As an OS or API framework designer, you've got to be real careful about that. You don't want users overriding and hooking at any arbitrary point in an API, because you cannot necessarily make those promises. And people may not fully understand the promises they are making when they make something virtual.

Lühidalt – kui teed kõik meetodid ja omadused virtuaialseks, siis puudub igasugune võimalus kontrollida objektide sisemist olekut ja garanteerida objekti kasutavale koodile, et kõik toimib alati õiges järjekorras ja õigel kujul.

O/R-mapperid ei saa ilma virtualita

O/R-mapperid rikuvad mõnes mõttes kõik selle ilusa ära, sest ilma klassi avalikku osa virtualiseerimata ei saa nad luua tegelike klasside ümber omi wrappereid, mis tegelevad selliste ülesannetega nagu lazy loading ja muutuste jälgimine. See funktsionaalsus lahendatakse ära sellise vahendi abil nagu dynamic proxy. Need on spetsiaalselt selleks mõeldud jubinad ja kes tahab lähemalt mõnega tutvust teha, siis üks tuntud dünaamilistest proxydest on näiteks Caste DynamicProxy.

Dünaamiline proxy tahab avalikku osa virtuaalsena sel põhjusel, et see loob uue klassi, mis laiendab etteantud klassi. Laiendavale klassile lisatakse meetodid ja omadused, mis teevad ära mapperi jaoks vajaliku töö ja mis kutsuvad ühtlasi baasklassist välja meetode ja omadusi, mille override-d nad on. Dünaamiline proxy peab loodud klassid mahutama ära antud tüübi hierarhiasse, sest vastasel korral on tegemist täiesti uute klassidega, mille tüübid tuleb meil oma koodis mängu tuua.

Seega – tänu virtuaalsele avalikule osale saab O/R-mapper lisada meie äriklassidele oma funktsionaalsuse juurde selliselt, et meie kood katki ei lähe.

Probleem – parametriseeritud konstruktor

Järgmiseks jõuame sinnamaale, kus inimestel on soov teha klassid, mis on kohe algusest peale õigesti initsialiseeritud. Paketi skoobis parameetriteta konstruktor kuulub mapperile ja avalik konstruktor tahab kaasa saada kõiki olulisemaid omadusi. Tulemusena tekkiv kood on üksjagu ohtlik tänu sellele kuidas .NET klasse loob. Vaatame järgmist näidet:

class Program
{
    static void Main()
    {
        var x = new Child();
    }
}

class Parent
{
    public Parent()
    {
        DoSomething();
    }   
    protected virtual void DoSomething() {}
}
   
class Child : Parent
{
    private string foo;   
    public Child()
    {
        foo = "HELLO";
    }   
    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower());
    }
}

Kui me selle jupi koodi käivitame, siis saame sellise tulemuse:

NullReferenceException virtuaalse meetodi kutsumisel konstruktorist

Miks? Põhjus on küllaltki lihtne. Objekti loomine koosneb kahest sammust: initsialiseerimine ja konstrueerimine. Initsialiseerijad käivitatakse hierarhia põhjast tipu suunas, seega siis baasklassi suunas. Konstrueerimine, mille käigus käivitatakse konstruktorid, toimib aga vastupidi – baasklassist eemale hierarhia sügavuste poole.

Hetkel, mil me kutsume välja klassi Parent konstruktoris meetodi DoSomething() pole klassi Child konstruktorit veel käivitatud ning muutuja nimega foo ei oma seega väärtus (tema väärtus on null).

Kuidas hoiduda parametriseeritud konstruktorist?

Siin tulevad meile appi disainimustrid ja mida me siinkohal peaksime kasutama on selline muster nagu Factory. Factory idee on lihtne – me ei loo ise objekte väljapool oma äriloogika kihti, vaid kutsume välja meetodi, mis objekti meile tagastab. Me hoiame ennast konstrueerimisest lihtsalt eemale ja seega on meil üks mure rakenduse muudes kihtides vähem, millega tegeleda.

Selle lähenemise juures on meil asjad lahendatud selliselt:

  • on olemas kas äriloogika kihist väljapoole näha Factory klass või pakub mõni äriloogika klassidest ise vastavad meetodid välja,
  • klasside konstruktorid on näha ainult äriloogika kihi skoobis (ehk siis internal),
  • Factory meetodid hoolitsevad ise selle eest, et õiget tüüpi objekt saaks korrektselt loodud,
  • Factory meetodi käest küsitakse kõik uued instantsid.

See on üks võimalus kuidas probleem lahendada.

Kokkuvõtteks

Me saame alati vältida klassi virtuaalsete liikmete kasutamist konstruktoris, kui me teeme disainilised otsused, mis on kooskõlas heade disainitavadega. Sel teel hoidume me mitmetest väga halbadest probleemidest, mida hiljem on raske tuvastada ja parandada. Lisaks sellele on meil võimalik vältida pikka parameetrite rivi klassi konstruktoris, sest Factory annab meile hea varjulise nurgataguse, kus kõik initsialiseerimine tasakesi ära teha.

Kommenteeri

sulge
Saada link e-postiga

© DT 2013 | Creative Commons Attribution-Noncommercial 3.0 License | WordPress