fbpx

Na co dzień pracuję z językiem C# dlatego też klasa abstrakcyjna i interfejs nie są mi obce. Postanowiłem zapoznać się bardziej szczegółowo z tematem i podzielić się swoją wiedzą oraz spostrzeżeniami.

Poniżej odpowiem na następujące pytania:

  • Jak rozumieć abstrakcję?
  • Czym jest klasa abstrakcyjna a czym interfejs?
  • W jaki sposób używać abstrakcji w kodzie?
  • Jakie właściwości mają te obiekty?

Temat dotyczy głównie programowania obiektowego i w zależności od języka możemy je w inny sposób implementować. Poniżej postaram się temat maksymalnie omówić.

Wprowadzenie

Klasa abstrakcyjna i interfejs to dwa mechanizmy do tworzenia pewnego rodzaju abstrakcji w programowaniu obiektowym. Zarówno jeden jak i drugi typ omówię na podstawie języka C#, ponieważ z nim mam najwięcej wspólnego. Postaram się też o prostą implementację. 

Czym jest abstrakcja?

Na początku warto odpowiedzieć sobie na powyższe pytanie. Abstrakcja to reprezentacja pewnego bytu, który został pozbawiony niektórych szczegółów. Szczegóły te są nieistotne w danym kontekście co pozwala na przykład grupować metody według ich cech ogólnych takich jak typ czy parametry wejściowe.

Po co w kodzie stosować abstrakcje?

Odpowiedzi może być wiele ale jedną z nich jest obniżenie złożoności aplikacji. Jak wiadomo, systemy się rozrastają, powstaje coraz więcej kodu a przez to staje się on bardziej złożony i mało czytelny. Dzięki zastosowaniu obiektów abstrakcyjnych rozkładamy tą złożoność na mniejsze czynniki. Aby to dobrze zobrazować posłużę się przykładem poniżej.

public void LogFatalErrorToDatabase(string error)
{
    Connection(); 
    exepurl = context.Current.Request.Url.ToString(); 

    SqlCommand com = new SqlCommand("LogToDatabase", con); 
    com.CommandType = CommandType.StoredProcedure; 
    com.Parameters.AddWithValue("@ExceptionMsg", error); 
    com.Parameters.AddWithValue("@ExceptionType", "FatalError"); 
    com.Parameters.AddWithValue("@ExceptionURL", exepurl); 

    com.ExecuteNonQuery(); 
}

Mogę za każdym razem logować błąd przy pomocy tych kilku linii wywołujących zapytanie. Byłoby to z czasem uciążliwe i ilość kodu rosłaby dużo szybciej. Złożony kod został obudowany w prostą abstrakcję o nazwie LogFatalErrorToDatabase, która może być wywoływana dalej za pomocą jednej linii kodu. Mogę śmiało dodać, że dobre obudowanie w abstrakcje jest jedną z podstaw prawidłowego programowania obiektowego. Odpowiedni poziom abstrakcji zmniejszy koszt oraz długość procesu wdrażania programistów do projektu.

Napiszę na zakończenie, że powyższy przykład dotyczył abstrakcji jako ogólnej cechy programowania obiektowego. Można ją traktować jako każdą wydzieloną metodę. Do budowania abstrakcji jeszcze o poziom niżej idealnie nadają się tytułowe klasy abstrakcyjne i interfejsy.

Klasa abstrakcyjna

Po pierwsze klasa abstrakcyjna, jest pewnego rodzaju uogólnieniem klas pochodnych. Najprościej rzecz ujmując klasa taka ma sens w przypadku, gdy kilka klas pochodnych wymaga użycia tej samej funkcji ale z innymi jej implementacjami. Można przedstawić to na wiele sposobów. Częste przykłady klasy abstrakcyjnej to Figura lub Zwierzę. Poniżej zaimplementuję przykład dotyczący sprzedaży

Implementacja

Poniższy kod bardzo uogólniłem i jest trywialny ale oddaje w pełni ideę klasy abstrakcyjnej. Na pierwszy rzut oka widać słowo kluczowe abstract w tworzeniu obiektu klasy. Wewnątrz dodane zostały cztery metody abstrakcyjne do pobierania nazwy, ceny, procentu rabatu oraz kategorii. Ostatnia metoda w klasie służy do pobrania nazwy magazynu. Z racji założeń poczynionych przez biznes, okazało się, że jest tylko jeden magazyn i więcej nie będzie w planach. Zostało więc zaproponowane, że metoda z pobraniem nazwy magazynu będzie zaimplementowana w klasie abstrakcyjnej ProductToSell

Następnie dwie publiczne klasy Book oraz Cup dziedziczą z klasy abstrakcyjnej ProductToSell. Powoduje to sytuację, gdzie wszystkie metody abstrakcyjne muszą zostać nadpisane i zaimplementowane. Służy do tego słowo kluczowe override. W klasie Program następuje deklaracja dwóch wyżej przedstawionych klas i wypisanie wyników na ekran konsoli. Warto skopiować kod do IDE i spróbować na przykład zadeklarować w funkcji Main obiekt klasy ProductToSell

using System;

namespace Klasa_abstrakcyjna
{
    public abstract class ProductToSell
    {
        public abstract string getName();
        public abstract double getPrice();
        public abstract int getPercentDiscount();
        public abstract string getCategory();

        public string getWarehouse()
        {
            return "Warehouse number 1";
        }
    }

    public class Book : ProductToSell
    {
        public override string getName()
        {
            return "Prawdziwa historia McDonald's";
        }

        public override double getPrice()
        {
            return 24.99;
        }

        public override int getPercentDiscount()
        {
            return 5;
        }
        public override string getCategory()
        {
            return "Książka";
        }
    }

    public class Cup : ProductToSell
    {
        public override string getName()
        {
            return "Kubek programisty";
        }

        public override double getPrice()
        {
            return 21.99;
        }

        public override int getPercentDiscount()
        {
            return 0;
        }
        public override string getCategory()
        {
            return "Kubek";
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Book book = new Book();
            Console.WriteLine("Kategoria: " + book.getName());
            Console.WriteLine("Nazwa: " + book.getCategory());
            Console.WriteLine("Cena: " + book.getPrice().ToString());
            Console.WriteLine("Rabat [%]: " + book.getPercentDiscount().ToString());
            Console.WriteLine("Magazyn: " + book.getWarehouse());
            Console.WriteLine(Environment.NewLine);

            Cup cup = new Cup();
            Console.WriteLine("Kategoria: " + cup.getName());
            Console.WriteLine("Nazwa: " + cup.getCategory());
            Console.WriteLine("Cena: " + cup.getPrice().ToString());
            Console.WriteLine("Rabat [%]: " + cup.getPercentDiscount().ToString());
            Console.WriteLine("Magazyn: " + cup.getWarehouse());

            Console.ReadLine();
        }
    }
}

Właściwości klas abstrakcyjnych

  • Zawierają słowo kluczowe abstract
  • Nie można utworzyć instancji klasy abstrakcyjnej
  • W przypadku, gdy klasa posiada przynajmniej jedną metodę abstrakcyjną (wirtualną) wtedy musi być klasą abstrakcyjną
  • Może posiadać konstruktor i metody z zaimplementowanym ciałem
  • Gdy klasa zawiera same metody abstrakcyjne(wirtualne) wtedy powinna zostać zamieniona na interfejs
  • W klasie pochodnej możliwe jest dziedziczenie tylko po jednej klasie abstrakcyjnej
  • Metody klas mogą być publiczne lub chronione

Podsumowując

Klasa abstrakcyjna to dość kontrowersyjny twór w programowaniu. Daje programiście spore pole do popisu. Jednak możliwości wymieszania abstrakcji deklarowanych metod z implementacją metod niewirtualnych potrafią wprowadzić w zakłopotanie. Wielu programistów unika przez to abstrakcji jak ognia a wystarczy sobie przypomnieć, że pomagają chociażby w zachowaniu trzeciej zasady SOLID, czyli podstawienia Liskov oraz piątej, czyli odwrócenia zależności.

Podzielenie kodu na mniej złożone elementy pomaga, jednak warto pamiętać by nie przesadzać. Zbyt mocne „poszatkowanie” kodu poskutkuje zbyt dużą elementarnością i problemami podczas testów lub szukania błędów.

Interfejs

Po drugie interfejs. Tak samo jak klasa abstrakcyjna służy do tego, by kod uczynić bardziej przyjaznym i łatwym do utrzymywania. Jedną z różnic między jednym a drugim abstrakcyjnym typem jest to, że interfejs zawiera w ciele tylko definicje metod a nie ich implementację. Poniżej pokaże na kolejnym przykładzie w jaki sposób zastosować interfejs.

Implementacja

Często zdarza się, że biznes przychodzi z nowymi ustaleniami i pomysłami. Tym razem padło na to, że otworzony został drugi magazyn i książki będą w magazynie numer 1 a kubki w magazynie numer 2. Powoduje to sytuacje, że metoda getWarehouse będzie posiadać inną implementację w klasie Book a inną w klasie Cup. Oczywiście można pozostawić klasę abstrakcyjną i nadpisywać wszystkie metody w niej znajdujące się lecz zgodnie z jedną z wyżej przedstawionych właściwości powinna zostać zamieniona na interfejs. Tak też stało się w tym przykładzie. 

Interfejs IProductToSell posiada pięć definicji metod, które to definicje implementujemy w klasach Book oraz Cup. Każda z tych klas dziedziczyć będzie po omawianym interfejsie. Metoda Main jest identyczna jak w przykładzie powyżej. 

using System;

namespace Wpis_11___Interfejs
{
    public interface IProductToSell
    {
        string getName();
        double getPrice();
        int getPercentDiscount();
        string getCategory();
        string getWarehouse();
    }

    public class Book : IProductToSell
    {
        public string getName()
        {
            return "Prawdziwa historia McDonald's";
        }

        public double getPrice()
        {
            return 24.99;
        }

        public int getPercentDiscount()
        {
            return 5;
        }

        public string getCategory()
        {
            return "Książka";
        }

        public string getWarehouse()
        {
            return "Magazyn numer 1";
        }
    }

    public class Cup : IProductToSell
    {
        public string getName()
        {
            return "Kubek programisty";
        }

        public double getPrice()
        {
            return 21.99;
        }

        public int getPercentDiscount()
        {
            return 0;
        }
        public string getCategory()
        {
            return "Kubek";
        }

        public string getWarehouse()
        {
            return "Magazyn numer 2";
        }
    }

    public class Interfejs
    {
        static void Main(string[] args)
        {
            Book book = new Book();
            Console.WriteLine("Kategoria: " + book.getName());
            Console.WriteLine("Nazwa: " + book.getCategory());
            Console.WriteLine("Cena: " + book.getPrice().ToString());
            Console.WriteLine("Rabat [%]: " + book.getPercentDiscount().ToString());
            Console.WriteLine("Magazyn: " + book.getWarehouse());
            Console.WriteLine(Environment.NewLine);

            Cup cup = new Cup();
            Console.WriteLine("Kategoria: " + cup.getName());
            Console.WriteLine("Nazwa: " + cup.getCategory());
            Console.WriteLine("Cena: " + cup.getPrice().ToString());
            Console.WriteLine("Rabat [%]: " + cup.getPercentDiscount().ToString());
            Console.WriteLine("Magazyn: " + cup.getWarehouse());

            Console.ReadLine();
        }
    }
}

Właściwości interfejsu

  • Zawiera słowo kluczowe interface
  • Przyjęło się, że interfejs rozpoczyna się dużą literą I
  • Nie można utworzyć instancji interfejsu
  • Wiele interfejsów może być użyte do dziedziczenia w tej samej klasie
  • Interfejs nie może zawierać pól
  • Metody w interfejsie muszą być publiczne

Podsumowując

Jak widać interfejs jest dość przyjaznym obiektem. Nie posiada aż takiego wymieszania jak klasa abstrakcyjna i ma ściśle określone zasady. Programista ma szanse budować klasy z dziedziczeniem po interfejsach. Pomoże to postępować zgodnie z wymaganiami, bez pomijania koniecznych metod. Do tego interfejs jest transparentny i nie zawiera w sobie Spaghetti code.

Również tak samo jak klasa abstrakcyjna, tak i interfejs ma swój udział w zasadach SOLID. Używane są w zasadach segregacji interfejsów oraz odwrócenia zależności.

Podsumowanie

Podczas omawiania interfejsów i klas abstrakcyjnych skupiłem się na przykładach i właściwościach głównie na przykładzie platformy .NET i języka C#. W przypadku języków takich jak PHP, Java czy C++ mogą być nieznaczne różnice. Proszę o wypisanie ich w komentarzach.

Warto też mieć na uwadze to, że zarówno klasa abstrakcyjna jak i interfejs nie są naszymi wrogami. Oczywiście czasami ich użycie może wydawać się bez sensu jednak im większy system tym więcej będziemy poświęcali czasu na utrzymanie i naprawę błędów. Wtedy swoją dobrą twarz pokażą właśnie te dwa typy. Już sam fakt, że są podstawą trzech z pięciu zasad SOLID dużo nam o nich mówi.

Możesz postawić pytanie dlaczego omawiam na przykładzie jednego języka programowania. Ogólnie jestem zdania, że inżynier oprogramowania powinien poradzić sobie z każdym językiem. Przykłady zaprezentowane powyżej stworzone zostały po to by pokazać idee jakie stoją za abstrakcyjnymi obiektami w programowaniu.

A już w następnym wpisie poruszę temat algorytmu Boyera-Moore’a.

PS. Będę wdzięczny za komentarze pod tym postem!

PS2. Czeka też na Ciebie prezent. Wystarczy, że zapiszesz się na Newsletter poniżej!

PS3. Dołącz do naszej grupy na Facebook!


Subskrybuj

Zapisz się na Newsletter, odbierz NAGRODĘ w postaci 10 omówionych algorytmów pojawiających się w pytaniach podczas REKRUTACJI.

Dodatkowo otrzymuj co niedzielę informacje na temat nowych wpisów, wiadomości ze świata IT i matematyki oraz ciekawych wydarzeniach. Nie przegap i dołącz już dziś do 405 osób!.

Źródła

http://zasoby.open.agh.edu.pl/~09sdczerner/strona/page/Klasy_abstrakcyjne_i_interfejsy.html
https://dl.acm.org/citation.cfm?id=861332 
https://books.google.pl/books?hl=pl&lr=&id=Ns4e0ko-S7QC&oi=fnd&pg=PT5&dq=c%23&ots=dtsTL934f4&sig=lsm5El7ao8WamAAAEuLrh-wMOu4&redir_esc=y#v=onepage&q=c%23&f=false 
http://edu.pjwstk.edu.pl/wyklady/pri/scb/index45.html 
http://mst.mimuw.edu.pl/lecture.php?lecture=poc&part=Ch7  http://pawel.rogalinski.staff.iiar.pwr.wroc.pl/dydaktyka/INE2018L_JP3_Java/Interfejsy.pdf
https://enauczanie.pg.edu.pl/moodle/pluginfile.php/321927/mod_resource/content/1/Wyklady_PDF/c_sharp_jwr_1_1.pdf  
Autor

👨‍💻 .NET and Python programming passionate 🏦 Digtial Banking Solutions 🎓 Student 📊 Psychology 📚 Bookworm 🏠 Warsaw

5 komentarzy

  1. Bardzo ciekawy wpis. Z interfejsami i klasami abstrakcyjnymi miałem do czynienia podczas kursu Javy na Udemy. W sumie to temat jest dość intuicyjny i łatwy w zrozumieniu. Porzuciłem jednak Jave na rzecz Pythona gdyż Java wydała mi się zbyt trudna do nauczenia.

    W ogóle to podziwiam ludzi którzy potrafią pisać złożone aplikacje składające się z wielu zależności i klas w Javie, C# czy innym języki strichte obiektowym. Klasy abstrakcyjne i interfejsy, tak jak pisze Mateusz, na pewno bardzo ułatwiają pracę nad kodem takich aplikacji.

    Muszę sobie poczytać o SOLID, ponieważ pierwszy raz się spotkałem z tym terminem. Chętnie bym poczytał tez na tym blogu o TDD i DDD czy o APIs. Często w ofertach pracy widzę te terminy i bardzo niewiele o nich wiem jeszcze :-).

    Dziękuję Mateusz za dzielenie się wiedzą i doświadczeniem.

  2. Czyli w skrócie: klasa abstrakcyjna definiuje czym obiekt jest, natomiast interfejs wskazuje co obiekt może robić.
    Dla nowicjuszy polecam trochę „zaniedbać” te kwestie, a już na pewno nie kuć definicji na pamięć – w moim przypadku potrzeba użycia i pewne zrozumienie przyszły przy pierwszym „większym” obiektowym projekcie który posiadał więcej niż 7 klas 🙂

Napisz komentarz

This site uses Akismet to reduce spam. Learn how your comment data is processed.