Backend

Prywatny konstruktor C++ i metoda kreacyjna

Prywatny konstruktor C++ jest dość ciekawym zagadnieniem w kontekście paradygmatu programowania obiektowego. Postanowiłem opisać temat przydatności prywatnych konstruktorów oraz podejścia do ich stosowania z wykorzystaniem metody kreacyjnej.

Łukasz Kosiński. Backend developer & analityk w Scythe Studio. Programista z zamiłowania. Analityk z zacięciem do rozwiązywania złożonych problemów i rozbijania je na części pierwsze. W pracy kieruje się zasadami tworzenia dobrego oprogramowania, stawiając jakość na pierwszym miejscu. Bardzo ceni zorganizowaną pracę i skuteczny kontakt. Autor bloga: binarnie.pl. Prywatnie amator słabego filmu, słabej książki i jazdy off-road.


Konstruktor prywatny w klasach o wszystkich składowych i metodach statycznych

Jeżeli z jakiegoś powodu nie zdefiniujemy własnego konstruktora w klasie to wówczas kompilator sam wygeneruje dla tej klasy konstruktor domyślny. Natomiast jeżeli stworzymy własny konstruktor niesparametryzowany to wówczas zastąpi on konstruktor domyślny. Załóżmy, że taki konstruktor będzie dodatkowo ograniczony zasięgiem private jako konstruktor prywatny. Wówczas nie będzie możliwości utworzenia instancji tejże klasy z zewnątrz.

Takie podejście może się przydać w momencie, gdy chcemy utworzyć klasę zawierającą jedynie metody statyczne i nie mamy zamiaru (a nawet chcemy zabronić) tworzyć kolejnych instancji tej klasy. Pomijając kwestię poprawności wzorca projektowego – singleton posłuży nam za przykład na wykorzystanie konstruktora prywatnego w C++.

class Calculator
{
  private:
    Calculator(){
        currentValue = 0;
    };
    
    static Calculator* instance; 
    
  public:
    static Calculator* createInstance() {
        if(instance == nullptr)
            instance = new Calculator();
        return instance;
    }
    static double currentValue;
 
    static double add(double value) {
        currentValue += value;
        return currentValue;
    }
    
};
 
Calculator* Calculator::instance = nullptr;
double Calculator::currentValue = 0;
 
int main()
{
    Calculator *calculator = Calculator::createInstance();
    std::cout << "Result of add 5.5 is: " << calculator->add(5.5);
    return 0;
}

W powyższym kodzie korzystając ze statycznej metody kreacyjnej createInstance() tworzymy instancję klasy zastrzegając, że możliwe będzie utworzenie jedynie jednej takiej instancji. Aby to osiągnąć korzystamy z konstruktora prywatnego, aby uniemożliwić taki zapis poza tą klasą:

Calculator *calculator = new Calculator(); // błąd

Prywatny konstruktor C++ a nazwane konstruktory

Kolejnym zastosowań jakie mogę podpisać pod prywatny konstruktor w C++ jest sytuacja, w której klasa posiada wiele, delikatnie się od siebie różniących konstruktorów. Właśnie z uwagi na drobne różnice w ich budowie są one potencjalnym źródłem błędów w chociażby strukturze klasy.

Chcąc rozwiązać ten problem najlepiej jest utworzyć jeden jednoznaczny konstruktor prywatny. Ze względu na jego hermetyzację będzie on możliwy do utworzenia jedynie wewnątrz innych metod danej klasy lub też w ramach przyjaźni bez możliwości jego wywołania na zewnątrz.

Po utworzeniu prywatnego konstruktora należy skorzystać z tzw. nazwanych konstruktorów, czyli statycznych metody danej klasy, które w swoim ciele skorzystają z prywatnego konstruktora a następnie zwrócą obiekt danej klasy.

Podaną sytuację najlepiej zrozumieć na przykładzie. Jakby to powiedział Linus Torvalds:

Talk is cheap. Show me the code.

class Image {
 
    public:
 
        Image (float h, float s, float v);
 
        Image (float h, float s, float l);
 
      
 
        private:
 
            float h,s,v;
 
};

W powyższej klasie reprezentującej obraz mamy dwa konstruktory o takiej samej ilości i takich samych typach parametrów. Jej celem jest możliwość utworzenia instancji obiektu tej klasy podając przy tym składowe obrazu w formacie HSV lub HSL. Nie ma więc mowy o przeciążeniu. Idealnym rozwiązaniem będzie właśnie skorzystanie z nazwanego konstruktora.

class Image {
    
    public:
        static Image createHsv(float h, float s, float v); 
        static Image createHsl(float h, float s, float l); 
    
    private:
        float h,s,v;
        Image (float h, float s, float v);
};
 
Image Image::createHsv(float h, float s, float v) {
    return Image (h, s, v);
}
 
Image Image::createHsl(float h, float s, float l) {
    someConversion(h, s, l);
    return Image (h, s, l);
}

Metoda createHsl zwróci obiekt klasy Obraz po wcześniejszym przekonwertowaniu parametrów w formacie hue, saturation, value na hue, saturation, lightness co zlikwidowało problem zdublowanych konstruktorów.

Użycie nazwanego konstruktora może wydawać się analogiczne do wzorca fabryki (niedługo na blogu). Różnica jest jednak dość znacząca, ponieważ w przypadku nazwanego konstruktora jesteśmy w 100% pewni klasy zwracanego obiektu. Natomiast wzorzec fabryki służy również do implementacji abstrakcji, czyli na przykład może zwrócić obiekt pewnej podklasy.

Prywatny konstruktor C++ kontra konstruktory wyniszczające

Na potrzeby tego wpisu wprowadźmy sobie pojęcie konstruktora wyniszczającego. Nie jest to w żadnej mierze oficjalny termin. Po prostu chciałem korzystać z wymownego określenia. Umówmy się, że tak będziemy nazywać długie i niebezpieczne konstruktory, które mogą w wyniku swojego działania pozostawić niestworzony obiekt w nieoczekiwanym/nielegalnym stanie.

Dla dobra obiektów najlepiej oczekiwać, że konstruktor utworzy nową instancję obiektu klasy w racjonalnym stanie. Przez stan racjonalny rozumiem moment, w którym mogę wywołać na obiekcie każdą metodę bez obaw.

Ofiarą wyniszczającego konstruktora może w równej mierze być programista odpowiedzialny w przyszłości za rozwój kodu napisanego przez Ciebie. Wyobraź sobie klasę z kilkoma wielolinijkowymi konstruktorami tworzącymi obiekt zgoła odmiennie. Zgroza. Lepiej jest skorzystać z prywatnego konstruktora tworzącego obiekt w stanie racjonalnym w zunifikowany sposób i skorzystać z odpowiednich konstruktorów nazwanych wraz z funkcjami inicjalizującymi.

Dodatkowo hipotetycznie załóżmy, że program grom jasny strzelił w trakcie wykonywania konstruktora, czyli w trakcie tworzenia obiektów. Pozostawiony wówczas obiekt może znaleźć się w nieoczekiwanym/nielegalnym stanie. Dodatkowo sami prosimy się o wycieki pamięci.

class Hotel {
 
  public:   
 
    Hotel(int rooms, int floors, string name) {
 
        _rooms = rooms;
        _floors = floors;
        _name = name;
 
        // dużo kodu nie związanego
        //z inicjalizacją składowych  
    }
  
    private:
        int _rooms;
        int _floors;
        string _name;
 
};

Lepszym rozwiązaniem od powyższego jest mały refactoring kodu i wykorzystanie metody kreacyjnej w połączeniu z konstruktorem prywatnym w taki sposób:

class Hotel {
 
   
 
    public:
 
    static Hotel* createInstance(int rooms, int floors, std::string name) {
        Hotel *hotel = new Hotel(rooms, floors, name);
 
        // dużo kodu nie związanego
        //z inicjalizacją składowych      
 
        return hotel;
    }
     
    private:
 
        int _rooms;
        int _floors;
        std::string _name;
 
        Hotel(int rooms, int floors, std::string name) :
            _rooms(rooms), _floors(floors), _name(name) {}
 
};

W synergii do prywatnych konstruktorów działa wzorzec fabryki mogący wziąć na siebie ciężar wyniszczających operacji wewnątrz konstruktora. Wykorzystuje on z resztą metody kreacyjne.

Podsumowanie

Mam nadzieję, że skutecznie udzieliłem odpowiedzi na pytanie w jakim celu stosować konstruktor prywatny w C++, a zamieszczone przykłady były w miarę życiowe. Przy czym zaznaczam, że omówione kwestie dotyczące wykorzystania konstruktora prywatnego w kodzie znajdą swoje odzwierciedlenie w innych obiektowych językach programowania.

Ostatnimi czasy spostrzegłem jak wiele daje znajomość popularnych wzorców projektowych, więc z pewnością wkrótce pojawią się na blogu wpisy właśnie na ten temat. Do tego czasu zapraszam do zapoznania się z materiałami umieszczonymi na tej stronie.

Zobacz też

najwięcej ofert html

Artykuł został pierwotnie opublikowany na binarnie.pl. Zdjęcie główne artykułu pochodzi z unsplash.com.

Wieloletni ekspert i entuzjasta frameworka Qt

Ma doświadczenie w pracy nad cross-platformowymi projektami Qt dla branży medycznej, motoryzacyjnej, a także z obszaru elektroniki konsumenckiej i obronności. Sam siebie opisuje jako specjalistę w Qt i QML z silną znajomością C++. Założyciel Scythe Studio - firmy świadczącej usługi w zakresie programowania Qt.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://justjoin.it/blog/prywatny-konstruktor-c-i-metoda-kreacyjna" order_type="social" width="100%" count_of_comments="8" ]