Jak refaktoryzować aplikację z wieloma skrzynkami przełączników?

10

Mam aplikację, która przyjmuje liczbę całkowitą jako dane wejściowe i na podstawie wejściowych wywołań metod statycznych różnych klas. Za każdym razem, gdy dodawany jest nowy numer, musimy dodać kolejny przypadek i wywołać inną metodę statyczną innej klasy. W przełączniku jest teraz 50 skrzynek i za każdym razem, gdy muszę dodać kolejną skrzynkę, wzdrygam się. Czy jest na to lepszy sposób?

Zastanowiłem się i wpadłem na ten pomysł. Używam wzorca strategii. Zamiast mieć skrzynkę przełączników, mam mapę obiektów strategii z kluczem będącym liczbą całkowitą wejściową. Po wywołaniu metody będzie ona wyszukiwać obiekt i wywoływać ogólną metodę dla obiektu. W ten sposób mogę uniknąć użycia konstrukcji skrzynki przełączników.

Co myślisz?

Kaushik Chakraborty
źródło
2
Jaki jest faktyczny problem z bieżącym kodem?
Philip Kendall,
Co się stanie, gdy będziesz musiał dokonać jednej z tych zmian? Czy musisz dodać switchprzypadek i wywołać wcześniej istniejącą metodę w złożonym systemie, czy też musisz wymyślić zarówno metodę, jak i jej wywołanie?
Kilian Foth,
@KilianFoth Odziedziczyłem ten projekt jako programista konserwacji i nie musiałem jeszcze dokonywać żadnych zmian. Jednak wkrótce wprowadzę zmiany, więc chcę teraz dokonać refaktoryzacji. Ale aby odpowiedzieć na twoje pytanie, tak na to drugie.
Kaushik Chakraborty
2
Myślę, że musisz pokazać skondensowany przykład tego, co się dzieje.
whatsisname
1
@KaushikChakraborty: następnie wymyśl przykład z pamięci. Są sytuacje, w których odpowiedni jest przełącznik Uber-Switch o wielkości 250+, i są przypadki, w których przełącznik jest zły, bez względu na to, jak mało ma. Diabeł tkwi w szczegółach i nie mamy żadnych szczegółów.
whatsisname

Odpowiedzi:

13

W przełączniku jest teraz 50 skrzynek i za każdym razem, gdy muszę dodać kolejną skrzynkę, wzdrygam się.

Uwielbiam polimorfizm. Uwielbiam SOLID. Uwielbiam programowanie czysto obiektowe. Nienawidzę patrzeć na tych złych przedstawicieli, ponieważ są stosowane dogmatycznie.

Nie zrobiłeś dobrego argumentu za refaktoryzacją strategii. Nawiasem mówiąc, refaktoryzacja ma swoją nazwę. Nazywa się to Zamień warunkowy na polimorfizm .

Znalazłem dla ciebie odpowiednie porady z c2.com :

To naprawdę ma sens tylko wtedy, gdy często powtarzane są te same lub bardzo podobne testy warunkowe. W przypadku prostych, rzadko powtarzanych testów, zastąpienie prostego warunku przez gadatliwość wielu definicji klas i prawdopodobnie przeniesienie tego wszystkiego daleko od kodu, który faktycznie wymaga warunkowo wymaganej aktywności, dałoby podręcznikowy przykład zaciemnienia kodu. Wolę klarowność niż dogmatyczną czystość. - DanMuller

Masz przełącznik z 50 skrzyniami, a twoją alternatywą jest wyprodukowanie 50 przedmiotów. Aha i 50 wierszy kodu konstrukcji obiektu. To nie jest postęp. Dlaczego nie? Ponieważ to refaktoryzacja nie wpływa na zmniejszenie liczby z 50. Korzystasz z tego refaktoryzacji, gdy okazuje się, że musisz utworzyć kolejną instrukcję switch na tym samym wejściu w innym miejscu. Właśnie wtedy refaktoryzacja pomaga, ponieważ zamienia 100 z powrotem na 50.

Tak długo, jak mówisz o „przełączniku”, jakby to był jedyny, nie polecam tego. Jedyną korzyścią wynikającą z refaktoryzacji jest to, że zmniejsza ona szanse, że niektóre goofball skopiują i wkleją przełącznik 50 skrzynek.

Polecam uważnie przyjrzeć się tym 50 przypadkom pod kątem podobieństw, które można uwzględnić. Mam na myśli 50? Naprawdę? Jesteś pewien, że potrzebujesz tylu skrzynek? Być może próbujesz tutaj wiele zrobić.

candied_orange
źródło
Zgadzam się z tym, co mówisz. Kod zawiera wiele redundancji, być może wiele przypadków nie jest nawet potrzebnych, ale na pierwszy rzut oka nie wydaje się, aby tak było. Każdy przypadek wywołuje metodę, która wywołuje wiele systemów i agreguje wyniki i wraca do kodu wywołującego. Każda klasa jest samodzielna, wykonuje jedną pracę i obawiam się, że naruszę zasadę wysokiej spójności, gdybym zmniejszył liczbę przypadków.
Kaushik Chakraborty
2
Mogę zdobyć 50 bez naruszenia wysokiej spójności i utrzymać wszystko w spokoju. Po prostu nie mogę tego zrobić z jednym numerem. Potrzebowałbym 2, 5 i jeszcze 5. Dlatego nazywa się to faktoringiem. Poważnie, spójrz na całą swoją architekturę i sprawdź, czy nie możesz znaleźć wyjścia z tego 50 piekła, w którym się znajdujesz. Refaktoryzacja polega na cofnięciu złych decyzji. Nie utrwalanie ich w nowych formach.
candied_orange 23.04.17
Teraz, jeśli widzisz sposób na zmniejszenie 50 za pomocą tego refaktoryzacji, idź do niego. Aby wykorzystać pomysł Doc Browna: Mapa map może zająć dwa klucze. Coś do przemyślenia.
candied_orange 23.04.17
1
Zgadzam się z komentarzem Candied. Problemem nie jest 50 przypadków w instrukcji switch, problemem jest projekt architektoniczny wyższego poziomu, który powoduje, że wywołujesz funkcję, która musi zdecydować między 50 opcjami. Zaprojektowałem kilka bardzo dużych i złożonych systemów i nigdy nie byłem zmuszony do takiej sytuacji.
Dunk
@Candied „Korzystasz z tego refaktoryzacji, gdy okazuje się, że musisz utworzyć kolejną instrukcję switch na tym samym wejściu w innym miejscu.” Czy możesz to rozwinąć? Ponieważ mam podobny przypadek, w którym mam skrzynki przełączników, ale na różnych warstwach, jak w naszym pierwsza autoryzacja projektu, walidacja, procedury CRUD, a następnie dao. Tak więc w każdej warstwie znajdują się skrzynki przełączników na tym samym wejściu, tj. Liczba całkowita, ale spełniające różne funkcje, takie jak auth, są prawidłowe. więc czy powinniśmy stworzyć jedną klasę jodłową dla każdego typu, która ma inne metody? Czy nasza sprawa pasuje do tego, co próbujesz powiedzieć, „powtarzając ten sam przełącznik na tym samym wejściu”?
Siddharth Trikha
9

Mapa samych obiektów strategii, która jest inicjowana w jakiejś funkcji kodu, w której wygląda kilka linii kodu

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

wymaga od ciebie i twoich współpracowników zaimplementowania funkcji / strategii, które mają być wywoływane w oddzielnych klasach, w bardziej jednolity sposób (ponieważ wszystkie obiekty strategii będą musiały implementować ten sam interfejs). Taki kod jest często nieco bardziej kompleksowy niż

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Jednak nadal nie zwolni cię to z obowiązku edytowania tego pliku kodu, ilekroć trzeba dodać nowy numer. Rzeczywiste zalety tego podejścia są inne:

  • inicjalizacja mapy jest teraz oddzielona od kodu wysyłki, który faktycznie wywołuje funkcję powiązaną z określoną liczbą, a ta ostatnia nie zawiera już tych 50 powtórzeń, po prostu będzie wyglądać myMap[number].DoIt(someParameters). Dlatego ten kod wysyłki nie musi być dotykany za każdym razem, gdy nadejdzie nowy numer, i można go wdrożyć zgodnie z zasadą Otwarte-Zamknięte. Co więcej, gdy otrzymasz wymagania, w których musisz rozszerzyć sam kod wysyłki, nie będziesz już musiał zmieniać 50 miejsc, ale tylko jedno.

  • zawartość mapy jest określana w czasie wykonywania (podczas gdy zawartość konstrukcji przełącznika jest określana przed czasem kompilacji), więc daje to możliwość uczynienia logiki inicjalizacji bardziej elastyczną lub rozszerzalną.

Tak, są pewne korzyści, a to z pewnością krok w kierunku bardziej SOLIDNEGO kodu. Jeśli jednak opłaca się refaktoryzować, jest to coś, co Ty lub Twój zespół będzie musiał sam podjąć. Jeśli nie spodziewasz się, że kod wysyłki zostanie zmieniony, logika inicjalizacji zostanie zmieniona, a czytelność switchnie jest prawdziwym problemem, wówczas refaktoryzacja może nie być teraz tak ważna.

Doktor Brown
źródło
Chociaż nie chcę ślepo zastępować każdego przełącznika polimorfizmem, powiem, że przy użyciu mapy w sposób, który sugeruje tutaj doktor Brown, w przeszłości działał dla mnie bardzo dobrze. Podczas realizacji tego samego interfejsu należy wymienić Doit1, Doit2itp z jednej Doitmetody, która ma wiele różnych implementacji.
candied_orange
A jeśli masz kontrolę nad typem symbolu wejściowego używanego jako klucz, możesz pójść o krok dalej, tworząc doTheThing()metodę symbolu wejściowego. Więc nie musisz utrzymywać mapy.
Kevin Krumwiede
1
@KevinKrumwiede: to, co sugerujesz, oznacza po prostu przekazywanie samych obiektów strategii w programie, jako zamiennik liczb całkowitych. Jednak gdy program przyjmuje liczbę całkowitą jako dane wejściowe z jakiegoś zewnętrznego źródła danych, musi istnieć mapowanie z liczby całkowitej na powiązaną strategię co najmniej w jednym miejscu systemu.
Doc Brown
Rozwijając sugestię Doc Browna: możesz również stworzyć fabrykę, która zawiera logikę tworzenia obiektów strategii, jeśli zdecydujesz się pójść tą drogą. To powiedziawszy, odpowiedź udzielona przez CandiedOrange ma dla mnie sens.
Vladimir Stokic
@DocBrown Do tego zmierzałem „jeśli masz kontrolę nad typem symbolu wejściowego”.
Kevin Krumwiede
0

Jestem zdecydowanie za strategią przedstawioną w odpowiedzi @DocBrown .

Mam zamiar zasugerować poprawę odpowiedzi.

Połączenia

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

może być dystrybuowany. Nie musisz wracać do tego samego pliku, aby dodać kolejną strategię, która jeszcze lepiej przestrzega zasady Open-Closed.

Załóżmy, że implementujesz Strategy1w pliku Strategy1.cpp. Możesz mieć następujący blok kodu.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Możesz powtórzyć ten sam kod w każdym pliku StategyN.cpp. Jak widać, będzie to dużo powtarzającego się kodu. Aby zmniejszyć duplikację kodu, możesz użyć szablonu, który można umieścić w pliku dostępnym dla wszystkich Strategyklas.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Następnie jedyną rzeczą, której musisz użyć w Strategy1.cpp, jest:

static StrategyHelper::Initializer<1, Strategy1> initializer;

Odpowiednia linia w StrategyN.cpp to:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Możesz przenieść szablony na inny poziom, używając szablonu klas dla konkretnych klas strategii.

class Strategy { ... };

template <int N> class ConcreteStrategy;

I zamiast tego Strategy1użyj ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Zmień klasę pomocnika, aby zarejestrować Strategys:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Zmień kod w Strateg1.cpp na:

static StrategyHelper::Initializer<1> initializer;

Zmień kod w StrategN.cpp na:

static StrategyHelper::Initializer<N> initializer;
R Sahu
źródło