Jak wspomniałem plusy dodatnie i ujemne budowniczego były już nie raz szeroko omawiane. Mnie interesuje odpowiedź na inne pytanie: W jaki sposób zaprojektować interfejs budowniczego?. To znaczy:
- Jak zdecydować jakie metody powinien mieć budowniczy?
- Jak go sensownie używać (pragmatycznie, nie sztuka dla sztuki), aby pomagał, a nie przeszkadzał?
Ponieważ debugowałem blog Sławka, żeby podłączyć syntaxhighlightera, to pożyczyłem sobie Order i OrderItem do przykładu:).
Koncept jest następujący: mamy aplikację do składania zamówień na produkty. Zamówienie składa się z grup produktowych, a te składają się z produktów, jak na rysunku.
Dla uproszczenia załóżmy, że jest to aplikacja konsolowa. Właściciel oczekuje, że będzie pracował z aplikacją następująco:
Podaj identyfikator zamówienia: Paczka-E112
Podaj priorytet zamówienia: 1
Podaj nazwy grup produktowych: spożywcze chemia zabawki
Podaj ilość porduktów dla grupy 'spożywcze': 2
Dodaj produkt dla grupy 'spożywcze': sałata 2,45zł 2
Dodaj produkt dla grupy 'spożywcze': morele 6,79zł 1
Podaj ilość porduktów dla grupy 'chemia': 3
...
Startuję z następującym zestawem klas:
public class Order { private String id; private int priority; private SetitemGroups = new HashSet (); } public class ItemGroup { private String name; private Set - items = new HashSet
- (); } public class Item { private String name; private Money price; private int quantity; }
Za wykonanie zadania Dodawanie zamówienia, odpowiedzialną uczyńmy klasę:
public class UserInterface { private Setorders = new HashSet (); public void addOrder() { // TOIMPL } public static void main(String[] args) { new UserInterface().addOrder(); } }
W pierwszym podejściu kod, który działa, mógłby wyglądać na przykład tak:
public void addOrder() { Scanner scanner = new Scanner( System.in ); System.out.print( "Podaj identyfikator zamówienia: " ); String orderId = scanner.nextLine(); System.out.print( "Podaj priorytet zamówienia: " ); int orderPriority = Integer.parseInt( scanner.nextLine() ); Order order = new Order( orderId, orderPriority ); System.out.print( "Podaj nazwy grup produktowych: " ); String[] itemGroupsNames = scanner.nextLine().split( " " ); for ( int i = 0; i < itemGroupsNames.length; ++i ) { ItemGroup itemGroup = new ItemGroup( itemGroupsNames[ i ] ); System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " ); int itemsAmount = Integer.parseInt( scanner.nextLine() ); for( int j = 0; j < itemsAmount; ++j ) { System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " ); String[] itemParams = scanner.nextLine().split( " " ); String itemName = itemParams[ 0 ]; Money itemPrice = Money.parseMoney( itemParams[ 1 ] ); int itemQuantity = Integer.parseInt( itemParams[ 2 ] ); Item item = new Item( itemName, itemPrice, itemQuantity ); itemGroup.addItem( item ); } order.addItemGrup( itemGroup ); } orders.add( order ); }
public class OrderBuilder { private Order order; public void newOrder( String orderId, int orderPriority ) { order = new Order( orderId, orderPriority ); } public Order getOrder() { return order; } public void addItemGroup( String name ) { order.addItemGrup( new ItemGroup( name ) ); } public void addItem( String groupName, String name, Money price, int qty ) { ItemGroup group = order.findItemGroup( name ); group.addItem( new Item( name, price, qty ) ); } }
Kluczowe pytanie: Czy ten budowniczy jest pomocny?
Już zerkając na kod budowniczego, można mieć wątpliwości. Właściwie nie robi nic ponad, enkaspulację wywołań konstruktorów oraz metod dodających. Ok, można go sobie mokować i dodawać nowe implementacje, ale zobaczmy jak wygląda kod, który użytkuje tego budowniczego:public void addOrder() { Scanner scanner = new Scanner( System.in ); System.out.print( "Podaj identyfikator zamówienia: " ); String orderId = scanner.nextLine(); System.out.print( "Podaj priorytet zamówienia: " ); int orderPriority = Integer.parseInt( scanner.nextLine() ); OrderBuilder builder = new OrderBuilder(); builder.newOrder( orderId, orderPriority ); System.out.print( "Podaj nazwy grup produktowych: " ); String[] itemGroupsNames = scanner.nextLine().split( " " ); for ( int i = 0; i < itemGroupsNames.length; ++i ) { builder.addItemGroup( itemGroupsNames[ i ] ); System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " ); int itemsAmount = Integer.parseInt( scanner.nextLine() ); for( int j = 0; j < itemsAmount; ++j ) { System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " ); String[] itemParams = scanner.nextLine().split( " " ); String itemName = itemParams[ 0 ]; Money itemPrice = Money.parseMoney( itemParams[ 1 ] ); int itemQuantity = Integer.parseInt( itemParams[ 2 ] ); builder.addItem( itemGroupsNames[ i ], itemName, itemPrice, itemQuantity ); } } orders.add( builder.getOrder() ); }
Krok 1: Dlaczego ItemGroup i Item trzeba dodawać pojedynczo?
Wcale nie trzeba...Przecież budowniczy może mieć metody, które przyjmą input, który przyszedł od użytkownika i samodzielnie go zinterpretuje. Pytanie: Czy czasem znów nie dojdzie do pomieszania warstw? Nie nie dojdzie. Przeanalizujmy odpowiedzialności poszczególnych klas:- Order, ItemGroup, Item - to model dziedziny, reprezentuje rzeczywistość
- UserInterface - komunikuje się z użytkownikiem; odbiera od niego żądania (wpisy z konsoli) i przekazuje je do odpowiednich elementów niżej; prezentuje użytkownikowi wynik działania systemu
- OrderBuilder - służy do złożenia zamówienia z mniejszych elementów
public class OrderBuilder { private Order order; public void newOrder( String orderId, int orderPriority ) { order = new Order( orderId, orderPriority ); } public void addItemGroups( String itemsGroupsInput ) { String[] itemGroupsNames = itemsGroupsInput.split( " " ); for ( int i = 0; i < itemGroupsNames.length; ++i ) { order.addItemGrup( new ItemGroup( itemGroupsNames[ i ] ) ); } } public void addItem( String itemParamsInput ) { String[] itemParams = itemParamsInput.split( " " ); String groupName = itemParams[ 0 ]; ItemGroup group = order.findItemGroup( groupName ); Money price = Money.parseMoney( itemParams[ 1 ] ); int qty = Integer.parseInt( itemParams[ 2 ] ); group.addItem( new Item( groupName, price, qty ) ); } public Order getOrder() { return order; } }
public void addOrder() { Scanner scanner = new Scanner( System.in ); System.out.print( "Podaj identyfikator zamówienia: " ); String orderId = scanner.nextLine(); System.out.print( "Podaj priorytet zamówienia: " ); int orderPriority = Integer.parseInt( scanner.nextLine() ); OrderBuilder builder = new OrderBuilder(); builder.newOrder( orderId, orderPriority ); System.out.print( "Podaj nazwy grup produktowych: " ); String itemsGroupsInput = scanner.nextLine(); builder.addItemGroups( itemsGroupsInput ); String[] itemGroupsNames = itemsGroupsInput.split( " " ); for ( int i = 0; i < itemGroupsNames.length; ++i ) { System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " ); int itemsAmount = Integer.parseInt( scanner.nextLine() ); for( int j = 0; j < itemsAmount; ++j ) { System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " ); String itemParamsInput = scanner.nextLine(); builder.addItem( itemParamsInput ); } } orders.add( builder.getOrder() ); }
Krok 2: Chodzenie po strukturze zamówienia
To, co nam bruździ to fakt, trze trzeba dodać Items do każdej z ItemGroup. Więc najpierw za pomocą OrderBuilder.addItemGroups tworzone są wszystkie grupy, ale jeszcze dodatkowo UserInterface przetrzymuje sobie jeszcze tablicę nazw grup produktowych, że by po nich przeiterować i dodać pozycje zamówienia. Wydaje się to trochę nienaturalne, ponieważ ta sama informacja jest przechowywana w dwóch różnych formach (String[] i Setpublic class OrderBuilder { private Order order; private IteratorgroupIterator; private ItemGroup currentGroup; public void newOrder( String orderId, int orderPriority ) { order = new Order( orderId, orderPriority ); } public void addItemGroups( String itemsGroupsInput ) { String[] itemGroupsNames = itemsGroupsInput.split( " " ); for ( int i = 0; i < itemGroupsNames.length; ++i ) { order.addItemGrup( new ItemGroup( itemGroupsNames[ i ] ) ); } groupIterator = order.getItemGroups(); } public void addItem( String itemParamsInput ) { String[] itemParams = itemParamsInput.split( " " ); String groupName = itemParams[ 0 ]; Money price = Money.parseMoney( itemParams[ 1 ] ); int qty = Integer.parseInt( itemParams[ 2 ] ); currentGroup.addItem( new Item( groupName, price, qty ) ); } public Order getOrder() { return order; } public boolean hasNextItemGroup() { if ( groupIterator == null ) { return false; } return groupIterator.hasNext(); } public void moveNextItemGroup() { currentGroup = groupIterator.next(); } public String getCurrentItemGroupName() { return currentGroup.getName(); } }
public void addOrder() { Scanner scanner = new Scanner( System.in ); System.out.print( "Podaj identyfikator zamówienia: " ); String orderId = scanner.nextLine(); System.out.print( "Podaj priorytet zamówienia: " ); int orderPriority = Integer.parseInt( scanner.nextLine() ); OrderBuilder builder = new OrderBuilder(); builder.newOrder( orderId, orderPriority ); System.out.print( "Podaj nazwy grup produktowych: " ); builder.addItemGroups( scanner.nextLine() ); while ( builder.hasNextItemGroup() ) { builder.moveNextItemGroup(); System.out.print( "Podaj ilość produktów dla grupy '" + builder.getCurrentItemGroupName() + "' : "); int itemsAmount = Integer.parseInt( scanner.nextLine() ); for( int j = 0; j < itemsAmount; ++j ) { System.out.print( "Dodaj produkt dla grupy '" + builder.getCurrentItemGroupName() + "' : " ); builder.addItem( scanner.nextLine() ); } } orders.add( builder.getOrder() ); }