Na przykład tutaj można dowiedzieć się o niezwykłych korzyściach stosowania wzorca Builder (na przykład do wypiekania pizzy:)). Używam zatem, ale ten budowniczy mi więcej przeszkadza niż pomaga. Można pokusić się o stwierdzenie, że to tylko dodatkowa i nad wyraz rozdmuchana warstwa pośrednia, która niczego użytecznego nie wnosi. Ot, tworzy kilka obiektów i już.
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 Set itemGroups = 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 Set orders = 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 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 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 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 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 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 System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );int itemsAmount = Integer.parseInt( scanner.nextLine() );for( int j = 0; 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 Iterator groupIterator; 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 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 System.out.print( "Dodaj produkt dla grupy '" + builder.getCurrentItemGroupName() + "' : " );builder.addItem( scanner.nextLine() );}}orders.add( builder.getOrder() );}