Przejdź do głównej zawartości

[OOP] Na czym polega wzorzec projektowy Prototyp?

Prototyp to wzorzec projektowy, który pozwala na tworzenie obiektów danej klasy w oparciu o już istniejący obiekt.

Rozważmy następujący przykład: mamy aplikację służącą do tworzenia arkuszy kalkulacyjnych - coś jak Excel lub Arkusze Google. Arkusz składa się z rzędów i kolumn komórek. Pierwotnie wszystkie komórki są białe i piszemy w nich podstawową czcionką. Chcemy jednak, by użytkownik miał możliwość zdefiniowania własnego typu komórki - przykładowo może stworzyć styl komórki "z ostrzeżeniem", która będzie miała czerwone tło i pogrubioną czcionkę, lub styl "wartość poprawna", który będzie miał zielone tło, zielony kolor czcionki i brak pogrubienia. Raz utworzony styl może zastosować do dowolnie wybranych komórek.

Jako że użytkownik sam tworzy nowe style - dynamicznie, w czasie działania programu - nie możemy zastosować dziedziczenia (np. utworzenie klasy KomórkaOstrzegawcza dziedziczącej z klasy Komórka).

Musimy wziąć pod uwagę ważną rzecz: to że użytkownik zastosuje swój styl do całej kolumny komórek nie zmienia faktu, że w dowolnym momencie ma mieć możliwość zmiany stylu konkretnej komórki. Może wybrać dowolną z nich i przykładowo ustawić jej czcionkę na niebieską. Nie może to zmienić koloru czcionki w innych komórkach.

Jak więc ugryźć ten problem? Najlepiej utworzyć klasę reprezentującą styl komórki - nazwijmy ją CellStyle - której to instancja będzie tworzona gdy użytkownik stworzy nowy styl komórki. Klasa ta mogłaby wyglądać tak:

    class CellStyle
    {
        public Color BackroungColor { get; set; }
        public Color ForegroundColor { get; set; }
        public Font Font { get; set; }
    }

    class Color
    {
        private int _r, _g, _b;

        public Color(int r, int g, int b)
        {
            _r = r;
            _g = g;
            _b = b;
        }
    }

    class Font
    {

        public Font(int fontSize, bool isBold, bool isItalic, string fontName)
        {
            FontSize = fontSize;
            IsBold = isBold;
            IsItalic = isItalic;
            FontName = fontName;
        }

        public int FontSize { get; set; }
        public bool IsBold { get; set; }
        public bool IsItalic { get; set; }
        public string FontName { get; set; }
    }

W czasie działania programu użytkownik może utworzyć instancję klasy CellStyle:

            var warningCellStyle = new CellStyle
            {
                BackroungColor = new Color(255, 0, 0),
                ForegroundColor = new Color(0, 0, 0),
                Font = new Font(10, true, false, "Arial")
            };

Sama komórka mogłaby być reprezentowana przez klasę Cell:


    class Cell
    {
        public string Content { get; set; }
        public CellStyle Style { get; set; }
    }

Teraz wyobraźmy sobie, że użytkownik zaznacza kilkanaście komórek i zastosowuje do nich styl, który utworzył. Nie możemy zrobić czegoś takiego...

            foreach(var cell in selectedCells)
            {
                cell.Style = warningCellStyle;
            }

...gdyż do wszystkich komórek przypiszemy tę samą instancję klasy CellStyle. Gdy dla jednej zmienimy na przykład kolor tekstu, zmieni się on dla wszystkich. Zobaczmy:

            var selectedCells = new List<Cell>
            {
                new Cell {Content = "aaa"},
                new Cell {Content = "bbb"},
                new Cell {Content = "ccc"},
            };

            foreach(var cell in selectedCells)
            {
                cell.Style = warningCellStyle;
            }

            selectedCells[1].Style.ForegroundColor = new Color(0, 255, 0); //kolor zmienił się na zielony we wszytskich komórkach!


Musimy skopiować (lub jak kto woli - sklonować) obiekt warningCellStyle. Zależy nam na głębokiej, a nie płytkiej kopii. Więcej o klonowaniu obiektów w C# można przeczytać tutaj: [under construction]. W tym przykładzie użyjemy własnego interfejsu IDeepClonable. Dodajmy go i zmieńmy nasze klasy.


    interface IDeepClonable<T>
    {
        T DeepClone();
    }

    class CellStyle : IDeepClonable<CellStyle>
    {
        public Color BackroungColor { get; set; }
        public Color ForegroundColor { get; set; }
        public Font Font { get; set; }

        public CellStyle DeepClone()
        {
            return new CellStyle
            {
                BackroungColor = BackroungColor.DeepClone(),
                ForegroundColor = ForegroundColor.DeepClone(),
                Font = Font.DeepClone()
            };
        }
    }

    class Color : IDeepClonable<Color>
    {
        private int _r, _g, _b;

        public Color(int r, int g, int b)
        {
            _r = r;
            _g = g;
            _b = b;
        }

        public Color DeepClone()
        {
            return new Color ( _r, _g, _b );
        }
    }

    class Font : IDeepClonable<Font>
    {

        public Font(int fontSize, bool isBold, bool isItalic, string fontName)
        {
            FontSize = fontSize;
            IsBold = isBold;
            IsItalic = isItalic;
            FontName = fontName;
        }

        public int FontSize { get; set; }
        public bool IsBold { get; set; }
        public bool IsItalic { get; set; }
        public string FontName { get; set; }

        public Font DeepClone()
        {
            return new Font(FontSize, IsBold, IsItalic, FontName);
        }
    }

Świetnie. Teraz tworzymy kopie naszego prototypu - obiektu warningCellStyle. Zmiana w jednej komórce nie wpłynie na zmiany w innych komórkach.


            var warningCellStyle = new CellStyle
            {
                BackroungColor = new Color(255, 0, 0),
                ForegroundColor = new Color(0, 0, 0),
                Font = new Font(10, true, false, "Arial")
            };

            var selectedCells = new List<Cell>
            {
                new Cell {Content = "aaa"},
                new Cell {Content = "bbb"},
                new Cell {Content = "ccc"},
            };

            foreach(var cell in selectedCells)
            {
                cell.Style = warningCellStyle.DeepClone();
            }

            selectedCells[1].Style.ForegroundColor = new Color(0, 255, 0); //kolor zmienił się tylko w wybranej komórce

Podsumujmy: daliśmy użytkownikowi możliwość utworzenia prototypu danego obiektu. Używamy ten obiekt jak sztancę do tworzenia nowych obiektów. Każdy z nich, już po utworzeniu, jest jednak niezależnym bytem.

Przykład który przedstawiłam jest oczywiście bardzo prosty. Wyobraźmy sobie jednak sytuację, gdy tworzenie pojedynczego obiektu jest znacznie trudniejsze - na przykład wymaga od nas skomunikowania się z wieloma innymi serwisami w celu pobrania jakichś danych. Wtedy zastosowanie prototypu i tworzenie nowych obiektów na podstawie już istniejącego wzorca jest niezwykle przydatne - pozwala nam zaoszczędzić złożonych operacji przy tworzeniu kolejnych obiektów.

W poście tym korzystałam z:

  • https://www.udemy.com/design-patterns-csharp-dotnet
  • https://softwareengineering.stackexchange.com/questions/373873/what-is-a-real-world-usage-for-prototype-pattern-in-java
  • https://stackoverflow.com/questions/13887704/whats-the-point-of-the-prototype-design-pattern

Komentarze