fbpx

Interfejs budowniczego

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 < 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 );
}

Po pobieżny przyjrzeniu się temu rozwiązaniu, można zauważyć, że nastąpiło tu pewne pogmatwanie. Komunikowanie się z użytkownikiem (wyświetlanie komunikatów i odbieranie wpisanych danych) zostało zmiksowane z kodem biznesowym (budowanie zamówienia). W pierwszym zachwycie nad wzorce Builder mam ogromną ochotę go użyć! UWAGA jeśli przychodzi ci go głowy napisać klasę OrderBuilder z jedną metodą build mającą kilkanaście parametrów, to nie tędy droga. Wyszedł by z tego nie budowniczy ale jakieś fabrykoniewiadomoco. Wracając do tematu: wprowadźmy zatem ad hoc budowniczego (bo ponoć to dobra praktyka). Budowniczy ten mógłby wyglądać następująco:

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() );
}

Jest tak samo brzydki jak był! Budowniczy nie wniósł znaczącego wkładu do tego kodu. Moim zdaniem interfejs budowniczego należy projektować z perspektywy klienta. Pisząc klient mam na myśli usługę, która akurat budowniczego będzie wykorzystywać – w tym przypadku jest to klasa UserIterface. Interfejs budowniczego powinien być wygodny dla klienta.

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

Zatem jeśli budowniczy może spokojnie przyjąć stringi podane przez użytkownika, a potem je zinterpretować. Dopóki budowniczy nie zacznie pisać na konsolę, to odpowiedzialność klas zostanie zachowana. Zobaczmy jak to mogłoby wyglądać:

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;
}
}

Jak widać budowniczy zmądrzał nieco, potrafi zrobić coś więcej niż proste dodawanie. A jak wygląda korzystanie z niego?

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() );
}

Choć nie idealnie to jednak jest nieco lepiej – metoda mieści się na jednym ekranie:)

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 Set) w dwóch różnych miejscach (UserInterface i OrderBuilder). Skoro chcemy tylko chodzić po wewnętrznej strukturze zamówienia (przesuwać się do kolejnych grup i dodawać produkty), to dlaczego nie wyposażyć budowniczego w tę możliwość. Niech budowniczy udostępni zestaw metod do chodzenia po grupach produktowych. Zobaczmy:

public 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 < 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();
}
}

Natomiast korzystanie z budowniczego wygląda następująco:

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() );
}

Teraz już klasa UserInterface zajmuje się wyłącznie komunikacją z użytkownikiem, natomiast budowniczy zajmuje się składaniem zamówienia do kupy. Każda z klas zachowała swoją odpowiedzialność, a z używanie budowniczego rzeczywiście przynosi korzyści klientowi większe niż tylko enkapsulacja konstruktorów i dodawania do list. Dzięki wykorzystaniu budowniczego kod kliencki staje się prostszy i bardziej przejrzysty. Dla porządku trzeba dodać, że odpowiedzialność budowniczego została zwiększona. Potrafi on teraz również chodzić po strukturze zamówienia. Jest więc jednocześnie Iteratorem. Dalej warto metody iterator wydzielić do osobnego interfejsu, ale to już nieco inna historia.