Czy wzorzec strategii można wdrożyć bez znacznego rozgałęzienia?


14

Wzorzec strategii działa dobrze, aby uniknąć dużych, jeśli ... innych konstrukcji i ułatwić dodawanie lub zastępowanie funkcjonalności. Jednak moim zdaniem nadal ma to jedną wadę. Wygląda na to, że w każdej implementacji wciąż musi istnieć rozgałęziony konstrukt. Może to być plik fabryczny lub plik danych. Jako przykład weź system zamawiania.

Fabryka:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

Kod po tym nie musi się martwić, a teraz jest tylko jedno miejsce, aby dodać nowy typ zamówienia, ale ta sekcja kodu wciąż nie jest rozszerzalna. Wyciągnięcie go do pliku danych nieco poprawia jego czytelność (dyskusyjne, wiem):

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Ale to wciąż dodaje kod podstawowy do przetwarzania pliku danych - przyznany, łatwiejszy do testowania jednostkowego i stosunkowo stabilny kod, ale dodatkowa złożoność.

Ponadto tego rodzaju konstrukcja nie testuje dobrze integracji. Każda indywidualna strategia może być teraz łatwiejsza do przetestowania, ale każda nowa strategia, którą dodajesz, dodatkowo komplikuje testowanie. Jest mniej, niż byś zrobił, gdybyś nie użył wzoru, ale wciąż tam jest.

Czy istnieje sposób na wdrożenie wzorca strategii łagodzącego tę złożoność? A może jest to tak proste, jak to tylko możliwe, a próba posunięcia się do przodu dodałaby kolejną warstwę abstrakcji, która przyniosłaby niewiele korzyści?


Hmmmmm ... można uprościć takie rzeczy, jak eval... może nie działać w Javie, ale może w innych językach?
FrustratedWithFormsDesigner

1
@FrustratedWithFormsDesigner odbicie to magiczne słowo w java
maniak ratchet

2
Nadal będziesz gdzieś potrzebował tych warunków. Przesuwając je do fabryki, po prostu przestrzegasz zasad DRY, ponieważ w przeciwnym razie instrukcja if lub switch mogła pojawić się w wielu miejscach.
Daniel B

1
Wada, o której wspominasz, jest często nazywana naruszeniem zasady otwartego zamknięcia
k3b

zaakceptowana odpowiedź w powiązanym pytaniu sugeruje słownik / mapę jako alternatywę dla if-else i przełącznika
gnat

Odpowiedzi:


16

Oczywiście nie. Nawet jeśli użyjesz kontenera IoC, będziesz musiał gdzieś mieć warunki, decydując, którą konkretną implementację wprowadzić. Taka jest natura wzorca strategii.

Naprawdę nie rozumiem, dlaczego ludzie myślą, że to problem. W niektórych książkach, takich jak Refaktoryzacja Fowlera , znajdują się stwierdzenia, że ​​jeśli widzisz przełącznik / przypadek lub łańcuch if / els w środku innego kodu, powinieneś rozważyć ten zapach i spróbować przenieść go na własną metodę. Jeśli kod w każdym przypadku jest dłuższy niż wiersz, może dwa, powinieneś rozważyć uczynienie tej metody Metodą Fabryczną, zwracającą Strategie.

Niektóre osoby przyjęły to do wiadomości, że przełącznik / obudowa jest zła. Nie o to chodzi. Ale jeśli to możliwe, powinien znajdować się sam.


1
Nie uważam tego też za coś złego. Zawsze jednak szukam sposobów na zmniejszenie ilości kodu, który mógłby go dotykać, co skłoniło nas do pytania.
Michael K

Instrukcje switch (i długie bloki if / else-if) są złe i należy ich unikać, jeśli to w ogóle możliwe, aby utrzymać kod w zarządzaniu. Który powiedział: „jeśli w ogóle możliwe” przyznaje, że istnieją pewne przypadki, w których musi istnieć wyłącznik, oraz w tych przypadkach, starać się utrzymać go w dół do jednego miejsca, a jeden, który sprawia, że utrzymanie go mniej chore (to tak łatwo przypadkowo brakuje 1 z 5 miejsc w kodzie, które trzeba było zsynchronizować, jeśli nie zostaną poprawnie wyodrębnione).
Shadow Man

10

Czy wzorzec strategii można wdrożyć bez znacznego rozgałęzienia?

Tak , za pomocą HashMap / słownik gdzie co rejestry w realizacji Strategii. Metoda fabryczna stanie się podobna

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

Każda implementacja strategii musi rejestrować fabrykę z jej typem zamówienia i informacjami dotyczącymi tworzenia klasy.

factory.register(NEW_ORDER, NewOrder.class);

Możesz użyć konstruktora statycznego do rejestracji, jeśli Twój język to obsługuje.

Metoda register robi nic więcej niż dodawanie nowej wartości do mapy hash:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[aktualizacja 2012-05-04]
To rozwiązanie jest znacznie bardziej złożone niż oryginalne „rozwiązanie przełączające”, które wolałbym przez większość czasu.

Jednak w środowisku, w którym strategie często się zmieniają (tj. Kalkulacja ceny w zależności od klienta, czasu, ...), to rozwiązanie z mapą zbiorczą w połączeniu z kontenerem IoC może być dobrym rozwiązaniem.


3
Masz teraz całą Hashmapę strategii, aby uniknąć zmiany / przypadku? Jak dokładnie rzędy factory.register(NEW_ORDER, NewOrder.class);czystsze, a mniej naruszenie OCP, niż rzędy case NEW_ORDER: return new NewOrder();?
pdr

5
To wcale nie jest czystsze. Jest to sprzeczne z intuicją. Poświęca zasadę „trzymaj to, co głupie”, aby ulepszyć otwartą, zamkniętą rywalizację. To samo dotyczy odwrócenia kontroli i wstrzyknięcia zależności: są one trudniejsze do zrozumienia niż proste rozwiązanie. Pytanie brzmiało: „zaimplementuj ... bez znaczącego rozgałęzienia”, a nie „jak stworzyć bardziej intuicyjne rozwiązanie”
k3b

1
Nie widzę też, żebyś uniknął rozgałęzienia. Właśnie zmieniłeś składnię.
pdr

1
Czy nie ma problemu z tym rozwiązaniem w językach, które nie ładują klas, dopóki nie są potrzebne? Kod statyczny nie uruchomi się, dopóki klasa nie zostanie załadowana, a klasa nigdy nie zostanie załadowana, ponieważ żadna inna klasa się do niej nie odwołuje.
kevin cline

2
Osobiście podoba mi się ta metoda, ponieważ pozwala traktować twoje strategie jako zmienne dane , zamiast traktować je jako niezmienny kod.
Tacroy

2

„Strategia” polega na tym, że przynajmniej raz trzeba wybierać między alternatywnymi algorytmami . Gdzieś w twoim programie ktoś musi podjąć decyzję - może użytkownik lub twój program. Jeśli użyjesz IoC, refleksji, ewaluatora pliku danych lub konstrukcji przełącznika / skrzynki do implementacji, nie zmieni to sytuacji.


Nie zgodziłbym się z twoim oświadczeniem raz. Strategie można wymieniać w czasie wykonywania. Na przykład po niepowodzeniu przetwarzania żądania przy użyciu standardowej ProcessingStrategy można wybrać VerboseProcessingStrategy i ponownie uruchomić przetwarzanie.
dmux

@dmux: pewnie, odpowiednio zmodyfikowałem moją odpowiedź.
Doc Brown

1

Wzorzec strategii jest używany, gdy określasz możliwe zachowania, i najlepiej jest go używać podczas wyznaczania procedury obsługi podczas uruchamiania. Określanie, którego wystąpienia strategii można użyć za pośrednictwem Mediatora, standardowego kontenera IoC, niektórych fabryk takich jak opisujesz, lub po prostu używając właściwego opartego na kontekście (ponieważ często strategia jest dostarczana jako część szerszego zastosowania klasy, która ją zawiera).

W przypadku tego rodzaju zachowania, w którym należy wywoływać różne metody w oparciu o dane, sugerowałbym użycie konstrukcji polimorfizmu dostarczanych przez dany język; nie wzór strategii.


1

Rozmawiając z innymi programistami, w których pracuję, znalazłem inne interesujące rozwiązanie - specyficzne dla Javy, ale jestem pewien, że pomysł działa w innych językach. Skonstruuj wyliczenie (lub mapę, nie ma to większego znaczenia, które) za pomocą odwołań do klas i utwórz instancję za pomocą odbicia.

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

To drastycznie zmniejsza kod fabryczny:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Proszę zignorować słabą obsługę błędów - przykładowy kod :))

Nie jest to najbardziej elastyczne, ponieważ kompilacja jest nadal wymagana ... ale redukuje zmianę kodu do jednej linii. Podoba mi się, że dane są oddzielone od kodu fabrycznego. Możesz nawet wykonywać takie czynności, jak ustawianie parametrów albo za pomocą klas abstrakcyjnych / interfejsów w celu zapewnienia wspólnej metody lub tworzenia adnotacji w czasie kompilacji w celu wymuszenia podpisów określonych konstruktorów.


Nie jestem pewien, co spowodowało powrót do strony głównej, ale to dobra odpowiedź, aw Javie 8 możesz teraz tworzyć odniesienia do Konstruktora bez refleksji.
JimmyJames

1

Rozszerzalność można poprawić, każąc każdej klasie zdefiniować własny typ zamówienia. Następnie twoja fabryka wybiera tę, która pasuje.

Na przykład:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}

1

Myślę, że powinien istnieć limit liczby dopuszczalnych gałęzi.

Na przykład, jeśli w instrukcji switch znajduje się więcej niż osiem przypadków, to ponownie oceniam kod i szukam tego, co mogę ponownie uwzględnić. Dość często stwierdzam, że niektóre przypadki można zgrupować w osobną fabrykę. Zakładam tutaj, że istnieje fabryka, która buduje strategie.

Tak czy inaczej, nie możesz tego uniknąć i gdzieś będziesz musiał sprawdzić stan lub typ obiektu, z którym pracujesz. Następnie zbudujesz dla tego strategię. Dobry stary podział problemów.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.