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:
W czasie działania programu użytkownik może utworzyć instancję klasy CellStyle:
Sama komórka mogłaby być reprezentowana przez klasę Cell:
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...
...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:
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.
Świetnie. Teraz tworzymy kopie naszego prototypu - obiektu warningCellStyle. Zmiana w jednej komórce nie wpłynie na zmiany w innych komórkach.
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:
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
Prześlij komentarz