Dlaczego warto używać modyfikatora sealed

O modyfikatorze sealed już kiedyś pisałem – dzięki niemu możemy zabronić dziedziczenia po danej klasie:

sealed class SealedClass
{
 
}
class ChildClass:SealedClass // BLAD
{
}

Moim zdaniem jest on niedoceniany i zbyt rzadko używany.  Może dlatego, że nie wnosi on nic nowego do funkcjonalności czy łatwości w pisaniu kodu a “jedynie” stanowi ważny element w projekcie oraz w tym, jak inni użytkownicy z takiej biblioteki będą korzystać.

Pierwszą zaletą SEALED jest wydajność. Oczywiście w większości przypadków taka optymalizacja nie ma jakiegokolwiek znaczenia ale dobrze wiedzieć, że oprócz dobrego design osiągniemy lepszą wydajność. W programowaniu obiektowym, wywoływanie metod wirtualnych jest trochę wolniejsze ponieważ najpierw trzeba sprawdzić, która dokładnie metoda ma zostać wykonana (polimorfizm). Do tego  służy tzw. Virtual method table – po szczegóły odsyłam do Wikipedii.  Przykład:

internal class Program
{
    class BaseClass
    {
        internal virtual void HelloWorld()
        {
        }
    }
    sealed class SampleClass : BaseClass
    {
    }
    private static void Main(string[] args)
    {
        SampleClass sampleClass = new SampleClass();
        sampleClass.HelloWorld();
    }
}

Pomimo, że zostanie wyemitowana instrukcja IL callvirt, CLR wywoła metodę w sposób niewirtualny a zatem pozbawiony przeszukiwania VMT. Gdyby nie modyfikator sealed, SampleClass mógłby wskazywać na klasy pochodne, które mają już inną implementacje metody HelloWorld – stąd wywołanie byłoby trochę wolniejsze.

Powyższa optymalizacja należy jednak do ciekawostek. W praktyce dobrze to wiedzieć i pisać kod wydajniejszy ale zdecydowanie nie przynosi to zauważalnego efektu. Ważniejszą kwestią jest stabilność klasy. Jeśli nie zaprojektowało się klasy z myślą o możliwości jej rozszerzenia wtedy powinno się użyć sealed.  W końcu klasa nienapisana z myślą o metodach wirtualnych itp. może przynieść efekty nieoczekiwane. Warto zabezpieczyć się przed tym zagrożeniem i “zapieczętować ją”. Pochodne klasy mogą w sposób nieświadomy modyfikować stan klasy bazowej czy nie wywoływać bazowych metod kiedy jest to potrzebne. Nie każdy pisany kod musi być zawsze maksymalnie rozszerzalny i skalowalny. Czasami potrzebna jest pojedyncza klasa i nie warto tracić czasu na przygotowanie jej pod przyszłe, ewentualne rozszerzenia. Pisząc rozszerzalną klasę należy każde pole, metodę przemyśleć tak, aby klasa pochodna nie mogła popsuć bazowej implementacji. Zasada jest taka, że inny programista, nie znając bazowej implementacji nie powinien przypadkowo zakłócić stabilności klas – poprawny projekt zawsze wymusza to.

Jeśli w pewnym momencie okaże się, że jednak potrzebne są pewne rozszerzenia wtedy nie ma problemu z usunięciem sealed. W drugą stronę jest to oczywiście niemożliwe. Klasa raz niezapieczętowana nie może później już być zapieczętowana ponieważ mogłoby to spowodować błędy w kompatybilności. Jeśli klasa najpierw była unsealed, wtedy  w międzyczasie ktoś mógł już napisać klasę pochodną. W takim przypadku dodanie sealed po prostu spowodowałoby, że jakiś kod nie skompiluje się.

Dobrym zwyczajem jest rozpoczęcie implementacji od maksymalnych ograniczeń w dostępności do danych. Metody zatem powinno pisać się jako niewirtualne, prywatne a klasy jako sealed oraz internal (jeśli nie są one zagnieżdżone).

Warto wspomnieć, że modyfikator sealed można również dołączać do metod przeciążanych:

class BaseClass
{
   internal virtual void HelloWorld()
   {

   }
}
class SampleClass : BaseClass
{
   sealed internal override void HelloWorld()
   {
       base.HelloWorld();
   }
}

W takim przypadku SampleClass:HelloWorld jest ostatnią przeciążoną implementacją. Dalsze klasy dziedziczące po SampleClass nie będą mogły już dostarczać własnej implementacji HelloWorld. Podobnie sealed może zostać połączony z właściwościami – nie powinno to zaskakiwać bo są one tak naprawdę zwykłymi metodami.

13 thoughts on “Dlaczego warto używać modyfikatora sealed”

  1. Moim zdaniem sealed powoduje więcej problemów niż korzyści. Jestem zdania, że sealed powinno być używane jak najrzadziej (najlepiej nigdy), a wszystkie metody powinny być z defaultu wirtualne.

    Niewirtualne metody i zapieczętowane klasy (oraz klasy internal) to największa bolączka kodu wychodzącego ze stajni MS.

  2. Konkretny przykład?
    ” a wszystkie metody powinny być z defaultu wirtualne.”
    Oj nieeee zgadzam sie. To tak jak powiedzenie, ze wszystkie pola powinny byc domyslnie publiczne.

  3. Ja zgadzam się tutaj z Maciejem. Jak dla mnie jest to jedna z większych zalet Javy (wszystkie metody publiczne są domyślnie wirtualne). Jest to niesamowicie przydatne na przykład przy kodowaniu na platformę Android.

  4. Ale przykład czego?:) Za każdym razem gdy otwieram reflectora i kopiuję z niego kod zżymam się, że nie mogę po prostu czegoś odziedziczyć. Wiadomo że override robię na własną odpowiedzialność.
    Albo testy jednostkowe, których niejednokrotnie nie da się w sensowny sposób napisać właśnie dlatego że coś jest sealed/nonvirtual (vide cały sharepoint żeby daleko nie szukać).

    Wirtualne metody a publiczne pola to dwie różne sprawy. Metodę można zrobić prywatną (tzn wtedy kompilator powinien zezwalać na połączenie “private virtual”).

  5. “Jeśli nie zaprojektowało się klasy z myślą o możliwości jej rozszerzenia wtedy powinno się użyć sealed.”

    W tym cały klops, czy jesteśmy w stanie bezbłędnie to przepowiedzieć… Nasza myśl może się z czasem okazać błędna 🙂

    “Jeśli w pewnym momencie okaże się, że jednak potrzebne są pewne rozszerzenia wtedy nie ma problemu z usunięciem sealed.”

    Myślę że może to być pewien praktyczny problem, np. kiedy nie mamy dostępu czy kontroli nad kodem źródłowym (jak sam .NET).

    Z drugiej strony jest jeszcze inny argument za używaniem sealed – ten modyfikator daje nam dodatkowe zabezpieczenie ze strony kompilatora.

    Jeśli np. rzutujemy niezapieczętowaną klasę do interfejsu, którego ona nie wspiera, to projekt się nam zbuduje, bo nikt nie może z góry wykluczyć, że jakaś klasa dziedzicząca z tej klasy będzie już ten interfejs wspierać. Może się nam wtedy wywalić na “invalid cast” dopiero w praniu.

    Natomiast jeśli klasa JEST oznaczona jako sealed, to zostaniemy z miejsca przystopowani. Nie wspiera, to nie wspiera – sprawa jasna.

  6. “W programowaniu obiektowym, wywoływanie metod wirtualnych jest trochę wolniejsze ponieważ najpierw trzeba sprawdzić, która dokładnie metoda ma zostać wykonana (polimorfizm).”

    No to OK, powiedzmy że w naszej bibliotece nie oznaczyliśmy klasy jako sealed. Ktoś chce z niej skorzystać – kto mu broni odziedziczyć w jego projekcie z naszej klasy, nic w samej implementacji nie zmieniając, a tylko oznaczając tą swoją pochodną klasę jako sealed?

    Powiedzmy nawet że przesłoni jej metody, wywołując we wszystkich przeciążeniach ich wersję base… Czy to nie wystarczy, żeby doświadczył tej lepszej wydajności?

  7. Również zgadzam się z Maciejem. Jak Javy nie lubię, to tę kwestię ma rozwiązaną dużo lepiej. Zamknięte na modyfikację, otwarte na rozszerzanie – takie powinno być domyślne podejście w języku – jak by nie patrzeć – obiektowym. Konieczność dorzucania virtuali jest męcząca. Jeżeli jakiś modyfikator by się przydał, to ten z drugiej strony – coś a’la nonvirtual.
    Sama możliwość pieczętowania czasami jest użyteczna, ale moim zdaniem należy się dobrze zastanowić, zanim coś potraktujemy jako sealed.

  8. @Maciej,Marek ŁB:
    Niebezpieczne…
    Tym tropem mogę powiedzieć, że najlepiej wszystkie pola ustawić na public bo wtedy unit testy będzie łatwiej robić i zawsze będzie można coś zmienić, dostosować.
    Pytanie dlaczego powstały modyfikatory private, protected?Dlaczego tak odradzane jest korzystanie z publicznych pól?
    Jedną z przyczyn jest właśnie ryzyko, że użytkownik, źle skorzysta z naszej klasy i zrobi bałagan (pozostawi obiekt w niespójnym stanie).
    I do tego właśnie służy sealed. Zgodnie ze znaną regułą, zaczynamy od najmniejszych przywilejów – a jeśli faktycznie są koniecznie wtedy je dodajemy. Nigdy w drugą stronę.
    To nie ważne, że użytkownik popsuje “tylko” swój kod. Dobry kod powinien zapobiegać takim sytuacjom i zawsze zachowywać się w sposób przewidywalny. Użytkownik bez szczegółów implementacyjnych nie powinien mieć problemów z korzystaniem z klasy.

    I to nie jest tylko moja praktyka – nawet team C# rozważał ustawienie sealed\internal jako domyślnego modyfikatora (teraz jest tylko internal jako default).

    Z kolei to, że w .NET Framework jakieś klasy mogły być rozszerzalne a nie są (zostały zapieczętowane) to już inna historia…
    Nie każda klasa nadaje się na dziedziczenie i we frameworkach gdzie implementacja klas jest dokonywana przez kompletnie innych ludzi ma to ogromne znaczenie,

  9. “I to nie jest tylko moja praktyka – nawet team C# rozważał ustawienie sealed\internal jako domyślnego modyfikatora (teraz jest tylko internal jako default).”

    No czyli sealed wypadł (pozostał teorią a nie praktyką) więc jak widać po rozważeniu sprawy koniec końców podjęli decyzję w odwrotnym kierunku 🙂

  10. Ja lubię “sealed”, i często je stosuję razem z odpowiednikiem “final” w javie. Praktycznie każda metoda którą dodaję w javie ma dodane “final” z wyjątkiem tych, które mają być wirtualne, a są deklarowane w klasach abstrakcyjnych. Natomiast nie wiedziałem, że “sealed” można dodać do metody. Może tego nigdy nie potrzebowałem bo lubię implementować interfejsy jako IInterfejs.Metoda, która z definicji jest niewidzialna nawet dla “this”.

    Dziedziczenie i wirtualność to potężne narzędzia, które wymagają wiele pracy już na etapie projektowania. Jeśli tylko można to należy ich unikać. Przecież to powoduje, że każda klasa, która dziedziczy po innej “tyje” o cały balast API wprowadzony przez przodków.

    Znacznie lepsze jest tworzenie nowych prostych interfejsów, a klasy które je implementują nie dziedziczą lecz zawierają istniejące implementacje innych interfejsów jako prywatne pola lub właściwości, z których korzystają ile mogą.

  11. @Pawel:
    No dokladnie, takie podejsce preferuje. Dac jak najmniej uzytkownikowi koncowemu do zepsucia:)

  12. Panie Piotrze Zielinski,
    Pracuje dla firmy obslugujacej koncerny tytoniowe, samochodowe, petrochemiczne i takie, ktore wymagaja FDA.
    Podstawa pracy jest zawsze dobra organizacja, takze w oprogramowaniu. Niechec programistow do “sealed” ma kilka zrodel. Przede wszystkim balaganiarstwo, lenistwo i krotkowzrocznosc. Male i ambitne firmy przychodza i odchodza, wlasnie przez zlych programistow. Dobrzy inzynierowie przechodza czesto do nas, argumentujac to checia tworzenia oprogramowania o lepszej jakosci.
    Pozdrawiam.

Leave a Reply

Your email address will not be published.