<

5. Wzorce projektowe (1)


Prawidłowe programowanie wymaga właściwego zastosowania wzorców projektowych. Zaczynamy od wprowadzenia pojęcia wzorców projektowych, przedstawienia historii ich rozwoju i klasyfikacji  oraz omówienia trzech - dość prostych, ale ważnych wzorców - Factory, Flyweight, Singleton. W dalszym toku wykładów przedstawione zostaną inne - wybrane - wzorce projektowe.


1. Co to są wzorce projektowe (design patterns) ?


Wzorce projektowe - to wielokrotnie powtórzone (powtarzające się) i pozytywnie zweryfikowane schematy rozwiązań często spotykanych problemów projektowych. 

Wzorce projektowe dotyczą architektury całej aplikacji, a nie pojedynczej klasy,


Wzorce projektowe nie są wynajdywane, ale odkrywane. Jakiś subtelny, świetny sposób rozwiązania jakiegoś problemu nie jest wzorcem projektowym.
Wzorzec musi być zastosowany wielokrotnie, niejako pojawić się w codziennej praktyce projektowej, potwierdzić swoje znaczenie. W tej praktyce jest dostrzegany i właśnie odkrywany.

Wzorców nie należy mylić z tzw. applications framework (przykładowo JCF nie jest wzorcem projektowym). Te ostatnie dotyczą raczej bardziej technicznych szczegółów, w tym implementacyjnych i znajdują się na niższym poziomie abstrakcji niż wzorce.


2. Czy i po co stosować wzorce projektowe?


Stosowanie wzorców projektowych pozwala na pisanie lepszych, bardziej efektywnych, skalowalnych, łatwiej modyfikowalnych, mniej narażonych na błędy programów


Przede wszystkim dlatego, że w codziennej praktyce, doświadczeniu całych rzesz projektantów i programistów te właśnie problemy są w centrum uwagi i te właśnie problemy są  rozwiązywane tak czy inaczej. Dobre rozwiązania sprawdzają sie, powtarzają, w końcu trafiają do "spisu" wzorców projektowych. Wzorce są dobrze opisane, wyjaśnione na konkretnych przykładach - dają więc szansę na poznanie dobrych metod programowania i zastosowanie ich po to, by ułatwić proces tworzenia i wdrażania aplikacji czy systemów.

ALE! - Nie należy na siłę stosować wzorców projektowych!!!



3. Historia rozwoju wzorców projektowych


Termin "wzorce projektowe"  pochodzi z prac architekta Christophera Alexandra.
Pierwszym wzorcem projektowym była architektura MVC w Smalltalku.

Przełomowe znaczenie miała wydana w połowie lat 90-tych książka “Design Patterns: Abstraction and Reuse of Object Oriented-Design”,  autorstwa tzw. Gang Of Four - GoF (E. Gamma, R. Helm, R. Johnson, J. Vlissides), w której sklasyfikowano i dobrze opisane 23 wzorce projektowe.


Rozwój wzorców:

Ostatnio pojawiało się i rozwija się  zainteresowanie wzorcem "Inverse of Control" (w sensie Dependency Injection). Ten temat poruszymy w wykładzie 14.


4. Wzorce GoF - klasyfikacja

W książce “Design Patterns: Abstraction and Reuse of Object Oriented-Design” opisano trzy klasy wzorców projektowych:

konstrukcyjne:
tworzenie obiektów, w tym: delegowanie procesu tworzenia do innych klas (ważne ze względu na zmniejszanie współzalezności w kodzie), kontrola nad sposobem tworzenia obiektów

strukturalne:
zarządzanie strukturą obiektów i strukturami złożonymi z obiektów

behawioralne:
zachowanie obiektów i komunikacja między nimi

Są to:

W poniższej tabeli pokazano zastosowania poszczególnych wzorców.

Nazwa wzorca Kategoria Zastosowanie Często używane z Pokrewne do
Abstract Factory Konstrukcyjne Uzyskiwanie instancji Factory Method
Prototype
Singleton + Facade
Factory Method
Prototype
Singleton
Adapter Strukturalne Dostosowanie interfejsu - Bridge
Decorator
Proxy
Command Behawioralne Separowanie akcji
Composite Composite
Memento
Prototype
Composite Strukturalne Strukturalna kompozycja/dekompozycja obiektów-systemów - Decorator
Iterator
Visitor
Decorator Strukturalne łatwe uzupełnianie o dodatkowe właściwości/ funkcjonalność
- Object Adapter
Composite
Strategy
Facade Strukturalne Ułatwienia dostępu Singleton Abstract Factory Abstract Factory
Mediator
Flyweight Strukturalne Współdzielenie zasobów = ograniczanie wymagań pamięciowych
- Singleton
State
Strategy
Shareable
Iterator Behawioralne Nawigacja

- Composite
Factory Method
Memento
Observer Behawioralne
Komunikacja, uniezależnienie fragmentów kodu

- Mediator
Singleton
Proxy Strukturalne Kontrolowanie dostępu
- Adapter
Decorator
Singleton Konstrukcyjne Kontrolowanie dostępu - Abstract Factory
Builder
Prototype
State Behawioralne Zmiany stanów obiektu
Flyweight Flyweight
Singleton
Strategy Behawioralne Implementacja algorytmu - Flyweight
State
Template Method
Template Method Behawioralne Implementacja algorytmu
- Strategy
Bridge Strukturalne Implementacja - Abstract Factory
Class Adaptor
Builder Konstrukcyjne Tworzenie struktur - Abstract Factory
Composite
Chain of Responsibility Behawioralne Organizacja przepływu zadań - Composite
Factory Method Konstrukcyjne Tworzenie obiektów Template Method Abstract Factory
Template Method
Prototype
Mediator Behawioralne Interakcja między obiektami
- Facade
Observer
Prototype Konstrukcyjne Tworzenie obiektów - Prototype
Composite
Decorator
Visitor Behawioralne "Double-dispatching" (zob. następne wykłady)
Composite
Composite
Visitor
Interpreter Behawioralne Organizacja zadań
- Composite
Flyweight
Iterator
Visitor
Memento Behawioralne Zarządzanie obiektami - Command
Iterator
Żródło: na podstawie David Corner wg GoF i Craig Larman,  ObjectSpace


5. Fabryki (factory method, factory, abstract factory)

Metoda fabryczna definiuje standardowy sposób tworzenia obiektów w sposób niezależny od ich rodzaju


Diagram GoF dla wzorca Factory (Factory Method) wygląda następująco

 

Jaki jest sens tego wzorca? Wyobraźmy sobie, że mamy dwie implementacje jakiejś czynności często wykonywanej w różnych miejscach aplikacji. Np. wyświetlanie komunikatów.

interface MsgDisplay {
   void show(String msg);
}

// Mamy dwie rózne implementacje

class ConsoleDisplay implements MsgDisplay {
   public void show(String s) {
     System.out.println(s);
   }
}

class DialogDisplay implements MsgDisplay {
  public void show(String s) {
    JOptionPane.showMessageDialog(null, s );
  }
}

W kodzie innych klas musimy tworzyć obiekty konkretnej klasy, np:

    MsgDisplay msg = new ConsoleDisplay(); // w wielu miejscach w kodzie!
    msg.show("Bad");                        // wiele zmian, jeśli zmianiamy impl. 

Teraz chcemy zmienić implementację. Przy takim sposobie programowania oznacza to, że musimy zmienić i rekompilować kody wielu klas składających się na aplikację.

Wzorzec Factory (Factory Method) pozwala nam tego uniknąć.
Możemy scentralizować zmiany kodu w jednym miejscu:

class MsgDisplayFactory {

  public static MsgDisplay getInstance() {  // zmiana implementacji TYLKO TU
    return new DialogDisplay();
  }

}

a kawałki "klienckie" (te wiele fragmentów, które potrzebują wypisania komunikatu) pisać całkiem abstrakcyjnie, unikając w ten sposób zmian i potrzeby rekompilacji wielu klas:
    MsgDisplay msg = MsgDisplayFactory.getInstance(); // tu nie ma zmian
    msg.show("Good");

Zauważmy, że działanie metod fabrycznych może zależeć od parametrów (albo inaczej ustalanych kontekstów), podawanych w fazie wykonania programu. Klasy klienckie nie troszczą się o konkretny typ wyniku (wiedzą, że zawsze dostaną właściwy), a metody fabryczne zwracają obiekty różnych klas - zależnie od kontekstu.

Przykład - klasa Calendar:

    public static Calendar getInstance()
    {
        Calendar cal = createCalendar(TimeZone.getDefaultRef(), Locale.getDefault());
	cal.sharedZone = true;
	return cal;
    }

    private static Calendar createCalendar(TimeZone zone,
					   Locale aLocale)
    {
	// If the specified locale is a Thai locale, returns a BuddhistCalendar
	// instance.
	if ("th".equals(aLocale.getLanguage())
	    && ("TH".equals(aLocale.getCountry()))) {
	    return new sun.util.BuddhistCalendar(zone, aLocale);
	}

	// else create the default calendar
        return new GregorianCalendar(zone, aLocale);	
    }
Źródło: Sun



Rozwinięciem wzorca Factory jest AbstractFactory, który to wzorzec:

 



6. Singleton

Wzorzec Singleton zapewnia, że klasa będzie miała tylko jedną instancję (można utworzyć jeden obiekt tej klasy), a jednocześnie udostępnia globalny, jednolity sposób uzyskiwania i odwoływania się do tego obiektu z różnych fragmentów kodu (innych klas)


 

 
Klasyczna implementacja wzorca Singleton wygląda tak:

public class ClassicSingleton {
   private static ClassicSingleton instance = null;

   private ClassicSingleton() {  // prywatny konstruktor
   }

   public static ClassicSingleton getInstance() {
      if(instance == null) {
         instance = new ClassicSingleton();
      }
      return instance;
   }
}

Co zapewnia:
Ale:

Problemy ze współbieżnością

Generalnie, powinniśmy metodę statyczną getInstance() w klasie singletonu (zwracającą jedyny obiekt klasy) uczynić synchronizowaną, bowiem jeśli dwa wątki będą równolegle wykonywać tę metodę, to może się zdarzyć, że otrzymamy dwa obiekty klasy (co przeczy definicji singletonu). Faktycznie, w poniższym kodzie:
class KlasaSingletonu {
  private static KlasaSingletonu obj;
  private KlasaSingletonu() {
     // ...
  }   

  public static KlasaSingletonu getInstance() {
    if (obj == null) obj = new KlasaSingletonu();
    return obj;
  }
}  
jeden z wątków może zostać wywłaszczony zaraz po sprawdzania warunku (obj == null), przychodzący na jego miejsce drugi wątek może stworzyć obiekt, a przywrócony pierwszy - "pamiętając", że obiektu nie było - też go stworzy.

Powinniśmy więc napisać tak:
  public static synchronized KlasaSingletonu getInstance() {
    if (obj == null) obj = new KlasaSingletonu();
    return obj;
  }
Warto zauważyć jednak, że taka synchronizacja jest potrzebna tylko przy pierwszym wywołaniu metody getInstance(). Wszystkie inne wywołania będą miały charakter odczytu, nie muszą zatem być synchronizowane, a ponieważ synchronizacja jest kosztowna - to chcielibyśmy jej uniknąć.
Niestety - z tych samych powodów co poprzednio - poniższe rozwiązanie, wydawałoby się logicznie synchronizowane tylko na pierwszym odwolaniu i unikające synchronizacji przy następnych - będzie wadliwe:

  public static KlasaSingletonu getInstance() {
    if (obj == null) { // w tym if (...) mamy ten sam problem co poprzednio
       synchronized(KlasaSingletonu.class) {
         obj = new KlasaSingletonu();
       }
    } 
    return obj;
  }
Aby uniknąć synchronizowanych odwołań powinniśmy zatem stworzyć obiekt przy pierwszym odwołaniu do klasy, co można uzyskać np. tak:

class KlasaSingletonu {

  private static final KlasaSingletonu obj = new KlasaSingletonu();

  private KlasaSingletonu() { // prywatny konstruktor
     // ...
  }   

  public static KlasaSingletonu getInstance() {
    return obj;
  }
}

Różne class-loadery

W niektórych sytuacjach (np. w kontenerach serwletów) mogą być używane różne class-loadery do ładowania klasy singletonu przy odwołaniu do niej z różnych części aplikacji (np. aplikacji WEB). W tej sytuacji powstanie wiele różnych instancji singletonu.

Rozwiązania:


Serializowalny singleton:

public class Singleton implements java.io.Serializable {
   public static Singleton INSTANCE = new Singleton();

   private Singleton() {
      // Exists only to thwart instantiation.
   }

   private Object readResolve() {
     return INSTANCE;
   }
}
Źródło: David Geary. op.cit.






7. Flyweight


Flyweight (dosłownie - waga musza, chodzi tu o lekkość konstrukcji)-  jest stosowany wtedy, gdy klasy - logicznie - odwołują się do bardzo dużej liczby obiektów, z których wiele jest do siebie podobnych (nawet takich samych). Duża liczba obiektów powoduje straty pamięci. Zajętość pamięci można zdecydowanie zmniejszyć poprzez współdzielenie takich samych obiektów (klasy, choć logicznie odwołują się do dużej liczby różnych obiektów,  tak naprawdę posługują się niewieloma dzielonymi między sobą obiektami).


Zauważmy, że obiekty współdzielone powinny być  (zazwyczaj) niezmienne, bowiem zwykle zostały już wykorzystane przez różne klasy do jakichś celów konstrukcyjnych (np. ramki wokół komponentów wizualnych są w Swingu obiektami konstruowanymi wg wzorca Flyweight, jeśli zatem ustaliliśmy ramkę dla jakiegoś komponentu, to spodziewamy się, że ta sama ramka  ustalana dla innego komponentu przez inną metodę lub w innej klasie będzie wyglądać tak samo).

Diagram GOF wygląda następująco:

 r


Rozważmy uproszczony przykład. Program ma tworzyć pudełka o różnych rozmiarach (ale o wysokości równej podwójnej szerokości).
Wiele rozmiarów będzie się powtarzać. Wiemy mniej więcej jakie są najczęściej występujące rozmiary i postanowiliśmy je współdzielić pomiędzy różnymi pudełkami (przy powtórzeniu tych rozmiarów). Może to dać duże oszczędności pamięci, np. jeśli mamy 7 najczęściej występujących rozmiarów i powtórzą się one milion razy, oszczędzimy ok. 28 MB pamięci.
Aby współdzielić rozmiary musimy uczynić klasę Dimension niezmienną.
Zgodnie z przedstawioną wcześniej  receptą:

final class Dimension {

  private int width;
  private int height;

  public Dimension(int w, int h) {
    width = w;
    height = h;
  }

  public int getWidth()  { return width; }
  public int getHeight() { return height; }

  public String toString() { return width + "x" + height; }

}
Rozmiary pudełek będzie nam dostarczać fabryka rozmiarów pudełek (klasa BoxDimensionFactory). Będzie ona zawierała tablicę najczęściej używanych rozmiarów.
Rozmiary będzie zwracać metoda fabryczna makeDimension(...), która na podstawie przekazanej szerokości pudełka sprawdzi, czy taki rozmiar już był użyty i jeśli tak zwróci ten rozmiar z tablicy, a jeśli nie stworzy nowy rozmiar i - gdy jest to jeden z często używanych  przed zwróceniem wstawi go do tablicy. W ten sposób:

Naszą fabrykę uczynimy singletonem. To też jest sposób na współdzielenia obiektów i oszczędności pamięci. Gdybyśmy bowiem chcieli wielokrotnie pobierać takie obiekty-fabryki (np. w różnych metodach czy klasach za pomocą odwołania BoxDimensionFactory.getInstance()) - to i tak zawsze dostaniemy ten jedyny "singleton".

Oznacza to, po pierwsze, zapewnienie spójności danych, a po drugie - właśnie oszczędność pamięci,

Oto kod fabryki rozmiarów.
class BoxDimensionFactory {

  // często występujące szerokości pudełek
  private int[] widths = { 10, 20, 30, 40, 50, 60, 70 };

  // Tablica rzomiarów pudełek - do ponownego użycia (współdzielenia)
  private Dimension[] d = new Dimension[widths.length];

  private int reused;  // ile razy ponownie użyto gotowego rozmiaru

  // Singleton
  // --- odniesienie do jedynego obiektu fabryki
  private static BoxDimensionFactory bdf;

  // --- prywatny konstruktor
  private BoxDimensionFactory() {}

  // --- metoda zwracająca fabrykę
  public static BoxDimensionFactory getInstance() {
    if (bdf == null) bdf = new BoxDimensionFactory(); // jeżeli obiekt nie istnieje -stwórz
    return bdf;                                       // zwróć jedyny obiekt klasy
  }

  // Metoda fabryczna
  // zwraca referencję do obiekty klasy Dimension
  public Dimension makeDimension(int w) {
    for (int i=0; i < widths.length; i++)
      if (w == widths[i]) { // jeżeli często występujący rozmiar
         // jeżeli używany pierwszy raz - utwórz go i zapisz do tablicy
         if (d[i] == null) d[i] = new Dimension(w, 2*w);
         else reused++;  // jeżeli już był utworzony - zwiększ licznik ponownego użycia
         return d[i];    // zwróć rozmiar - z tablicy "ponownego użycia"
      }
    return new Dimension(w, 2*w);  // jeżeli jakiś inny rozmiar - utwórz go i zwróć
  }

  // Zwraca liczbę ponownego użycia rozmiarów
  public int reusedCount() {
    return reused;
  }

}

Klasy testujące przedstawia wydruk.

class Box {
  private Dimension dim;
  private String cont;

  public Box(Dimension d, String c) {
    dim = d;
    cont = c;
  }

  public String toString() {
     return "Pudełko: " + dim + " Zawartość: " + cont;
  }
}

class BoxTest {

  public static void main(String[] args) {

    // Pobranie fabryki rozmiarów
    BoxDimensionFactory boxDimFac =  BoxDimensionFactory.getInstance();

    // na jakie pudełka jest teraz zapotrzebowanie
    int[] potrzebne = { 10, 10, 10, 20, 30, 45, 20, 20, 20, 20, 10,
                        20, 50, 65, 50, 50, 60, 100, 50, 50, 50,
                      };

    // Kolejne pudełka tworzymy na podstawie rozmiarów
    // uzyskanych z fabryki rozmiarów
    // nie wiemy i nie interesuje nas czy rozmiary to nowe obiekty
    // czy też już używane przez inne pudełka
    Box box = null;
    for (int i = 0; i < potrzebne.length; i++)
      box = new Box(boxDimFac.makeDimension(potrzebne[i]), "Kwiaty");

    System.out.println("Ostatnie pudełko");
    System.out.println(box);
    System.out.println("Na " + potrzebne.length + " rozmiarów pudełek\n" +
          "Utworzono nowych " + (potrzebne.length - boxDimFac.reusedCount()) +
          "\nPonownie użyto (przy wspóldzieleniu) " + boxDimFac.reusedCount()
          );
  }

}

Program wyprowadzi na konsoli:
Ostatnie pudełko
Pudełko: 50x100 Zawartość: Kwiaty
Na 21 rozmiarów pudełek
Utworzono nowych 8
Ponownie użyto (przy wspóldzieleniu) 13

Zobacz też nieco bardziej rozwinięte demo programu.








8. Podsumowanie

Zapoznaliśmy się z:


9. Zadania

Zad. 1

Napisać aplikację, która symuluje zakupy w kwiaciarni "samoobsługowej".
W kwiaciarni są kwiaty, kwiaty mają swoje nazwy oraz kolory. Ceny kwiatów znajdują się w cenniku.
Do kwiaciarni przychodzą klienci. Klienci mają imiona oraz dysponują jakimś zasobem pieniędzy. Wybierają kwiaty i umieszczają je na wózku sklepowym. Następnie płacą za zawartość wózka i przepakowują ją do pudełka.
Aplikacja wymaga zdefiniowania kilku klas i umiejętnego ich użycia, w taki sposób by następujący program działał poprawnie.

// Klasa "kwiaciarnia"
class Florist {

  public Florist() {
    // Ustalenie cennika
    PriceList pl = PriceList.getInstance();
    pl.set("róża", 10);
    pl.set("bez", 12);
    pl.set("piwonia", 8);
  }

}

// Klasa testująca
class FloristsTest {

  // tu definicja metody valueOf(Box pudelko, String kolor) zwracającej
  // sumaryczną wartość kwiatów o podanym kolorzez, znajdujących się w pudełku

  public static void main(String[] args) {

    // Kwiaciarnia samoobsługowa
    Florist kwiaciarnia = new Florist();

    // Przychodzi klient janek. Ma 200 zł
    Customer janek = new Customer("Janek", 200);

    // Bierze różne kwiaty: 5 róż, 5 piwonii, 3 frezje, 3 bzy
    janek.get(new Rose(5));
    janek.get(new Peony(5));
    janek.get(new Freesia(3));
    janek.get(new Lilac(3));

    // Pewnie je umieścił na wózku sklepowyem
    // Zobaczmy co tam ma
    ShoppingCart wozekJanka = janek.getShoppingCart();
    System.out.println("Przed płaceniem\n" + wozekJanka);

    // Teraz za to zapłaci...
    janek.pay();

    // Czy przypadkiem przy płaceniu nie okazało się,
    // że w wozku są kwiaty na które nie ustalono jescze cceny?
    // W takim razie zostałyby usunięte z wózka i Janek nie płaciłby za nie

    System.out.println("Po zapłaceniu\n" + janek.getShoppingCart());

    // Ile Jankowi zostało pieniędzy?
    System.out.println("Jankowi zostało : " + janek.getCash() + " zł");

    // Teraz jakos zapakuje kwiaty (może do pudełka)
    Box pudelkoJanka = new Box(janek);
    janek.pack(pudelkoJanka);

    // Co jest teraz w wózku Janka...
    // (nie powinno już nic być)
    System.out.println("Po zapakowaniu do pudełka\n" + janek.getShoppingCart());

    // a co w pudełku:
    System.out.println(pudelkoJanka);

    // Zobaczmy jaka jest wartość czerwonych kwiatów w pudełku Janka
    System.out.println("Czerwone kwiaty w pudełku Janka kosztowały: " +
                        valueOf(pudelkoJanka, "czerwony") );

    // Teraz przychodzi Stefan
    // ma tylko 60 zł
    Customer stefan = new Customer("Stefan", 60);

    // ąle nabrał kwiatów nieco za dużo jak na tę sumę
    stefan.get(new Lilac(3));
    stefan.get(new Rose(5));

    // co ma w wózku
    System.out.println(stefan.getShoppingCart());

    // płaci i pakuje do pudełka
    stefan.pay();
    Box pudelkoStefana = new Box(stefan);
    stefan.pack(pudelkoStefana);

    // co ostatecznie udało mu się kupić
    System.out.println(pudelkoStefana);
    // ... i ile zostało mu pieniędzy
    System.out.println("Stefanowi zostało : " + stefan.getCash() + " zł");
  }
}

Program powinien wyprowadzić następujące wyniki:

Przed płaceniem
Wózek własciciel Janek
róża, kolor: czerwony, ilość 5, cena 10.0
piwonia, kolor: czerwony, ilość 5, cena 8.0
frezja, kolor: żółty, ilość 3, cena -1.0
bez, kolor: biały, ilość 3, cena 12.0
Po zapłaceniu
Wózek własciciel Janek
róża, kolor: czerwony, ilość 5, cena 10.0
piwonia, kolor: czerwony, ilość 5, cena 8.0
bez, kolor: biały, ilość 3, cena 12.0
Jankowi zostało : 74.0 zł
Po zapakowaniu do pudełka
Wózek własciciel Janek -- pusto
Pudełko własciciel Janek
bez, kolor: biały, ilość 3, cena 12.0
piwonia, kolor: czerwony, ilość 5, cena 8.0
róża, kolor: czerwony, ilość 5, cena 10.0
Czerwone kwiaty w pudełku Janka kosztowały: 90.0
Wózek własciciel Stefan
bez, kolor: biały, ilość 3, cena 12.0
róża, kolor: czerwony, ilość 5, cena 10.0
Pudełko własciciel Stefan
róża, kolor: czerwony, ilość 5, cena 10.0
Stefanowi zostało : 10.0 zł

Uwaga: w pokazanym tekście programu  występują odwołania do klas: PriceList, Customer, ShoppingCart, Box, Rose, Lilac, Freesia, Peony . Trzeba je odpowiednio zdefiniować, ale oprócz tego nalezy zdefiniować jeszcze co najmniej kilka ważnych klasy (których w programie nie widać) potrzebne do spełnienia wymagań postawionych przed programem.

Trzeba też zdefiniować w klasie FloristsTest metodę valueOf(Box pudelko, String kolor) zwracającą wartość kwiatów o podanym kolorze, znajdujących się w pudełku.

Wymaganie podstawowe:
dodanie do powyższego programu zakupów innych kwiatów (np. orchidei) ma byc bardzo łatwe.

Potrzeba tylko :

Przy dodaniu nowego rodzaju kwiatów nie wolno modyfikować żadnych innych klas programu.

Wymagania dodatkowe:
Dodatkowe uwagi.