Coś, co pojawia się dość często w mojej obecnej pracy, polega na tym, że istnieje uogólniony proces, który musi się wydarzyć, ale potem dziwna część tego procesu musi się nieco różnić w zależności od wartości określonej zmiennej, a ja nie jestem całkiem pewien, jaki jest najbardziej elegancki sposób na poradzenie sobie z tym.
Posłużę się przykładem, który zwykle mamy, który robi rzeczy nieco inaczej w zależności od kraju, z którym mamy do czynienia.
Mam więc klasę, nazwijmy to Processor
:
public class Processor
{
public string Process(string country, string text)
{
text.Capitalise();
text.RemovePunctuation();
text.Replace("é", "e");
var split = text.Split(",");
string.Join("|", split);
}
}
Tyle że tylko niektóre z tych działań muszą się wydarzyć w niektórych krajach. Na przykład tylko 6 krajów wymaga etapu kapitalizacji. Postać do podziału może się zmieniać w zależności od kraju. Wymiana akcentu 'e'
może być wymagana tylko w zależności od kraju.
Oczywiście możesz to rozwiązać, robiąc coś takiego:
public string Process(string country, string text)
{
if (country == "USA" || country == "GBR")
{
text.Capitalise();
}
if (country == "DEU")
{
text.RemovePunctuation();
}
if (country != "FRA")
{
text.Replace("é", "e");
}
var separator = DetermineSeparator(country);
var split = text.Split(separator);
string.Join("|", split);
}
Ale kiedy masz do czynienia ze wszystkimi możliwymi krajami na świecie, staje się to bardzo uciążliwe. I niezależnie od tego, if
stwierdzenia utrudniają czytanie logiki (przynajmniej, jeśli wyobrażasz sobie bardziej złożoną metodę niż w przykładzie), a złożoność cykliczna zaczyna się dość szybko pełzać.
Więc w tej chwili robię coś takiego:
public class Processor
{
CountrySpecificHandlerFactory handlerFactory;
public Processor(CountrySpecificHandlerFactory handlerFactory)
{
this.handlerFactory = handlerFactory;
}
public string Process(string country, string text)
{
var handlers = this.handlerFactory.CreateHandlers(country);
handlers.Capitalier.Capitalise(text);
handlers.PunctuationHandler.RemovePunctuation(text);
handlers.SpecialCharacterHandler.ReplaceSpecialCharacters(text);
var separator = handlers.SeparatorHandler.DetermineSeparator();
var split = text.Split(separator);
string.Join("|", split);
}
}
Handlery:
public class CountrySpecificHandlerFactory
{
private static IDictionary<string, ICapitaliser> capitaliserDictionary
= new Dictionary<string, ICapitaliser>
{
{ "USA", new Capitaliser() },
{ "GBR", new Capitaliser() },
{ "FRA", new ThingThatDoesNotCapitaliseButImplementsICapitaliser() },
{ "DEU", new ThingThatDoesNotCapitaliseButImplementsICapitaliser() },
};
// Imagine the other dictionaries like this...
public CreateHandlers(string country)
{
return new CountrySpecificHandlers
{
Capitaliser = capitaliserDictionary[country],
PunctuationHanlder = punctuationDictionary[country],
// etc...
};
}
}
public class CountrySpecificHandlers
{
public ICapitaliser Capitaliser { get; private set; }
public IPunctuationHanlder PunctuationHanlder { get; private set; }
public ISpecialCharacterHandler SpecialCharacterHandler { get; private set; }
public ISeparatorHandler SeparatorHandler { get; private set; }
}
Nie jestem też pewien, czy mi się podoba. Logika jest nadal nieco zaciemniona przez cały proces tworzenia fabryki i nie można po prostu spojrzeć na oryginalną metodę i zobaczyć, co się dzieje, na przykład, gdy wykonywany jest proces „GBR”. Ty też skończyć tworzenie wielu klas (w bardziej skomplikowanych przykładów niż to) w stylu GbrPunctuationHandler
, UsaPunctuationHandler
itp ... co oznacza, że trzeba spojrzeć na kilka różnych klas, aby dowiedzieć się wszystkich możliwych działań, które mogą się zdarzyć podczas interpunkcji obsługa. Oczywiście nie chcę jednej wielkiej klasy z miliardem if
oświadczeń, ale równie 20 klas o nieco odmiennej logice również czuje się niezręcznie.
Zasadniczo myślę, że wpadłem w jakiś węzeł OOP i nie wiem, jak go rozwiązać. Zastanawiałem się, czy istnieje jakiś wzorzec, który pomógłby w tego rodzaju procesie?
PreProcess
funkcjonalność, która może być zaimplementowana inaczej w zależności od niektórych krajów,DetermineSeparator
może być dostępna dla wszystkich z nich iPostProcess
. Wszystkie z nich mogą miećprotected virtual void
domyślną implementację, a następnie możesz mieć określone dlaProcessors
poszczególnych krajówif (country == "DEU")
ciebie sprawdźif (config.ShouldRemovePunctuation)
.country
Odpowiedzi:
Sugerowałbym zawarcie wszystkich opcji w jednej klasie:
i przekaż to do
Process
metody:źródło
CountrySpecificHandlerFactory
... o_0public class ProcessOptions
naprawdę powinno być po prostu[Flags] enum class ProcessOptions : int { ... }
...ProcessOptions
. Bardzo wygodneKiedy środowisko .NET postanowiło radzić sobie z tego rodzaju problemami, nie modelowało wszystkiego jako
string
. Masz na przykładCultureInfo
klasę :Teraz ta klasa może nie zawierać określonych funkcji, których potrzebujesz, ale możesz oczywiście stworzyć coś analogicznego. A potem zmieniasz
Process
metodę:Twoja
CountryInfo
klasa może następnie miećbool RequiresCapitalization
właściwość itp., Która pomoże twojejProcess
metodzie odpowiednio ukierunkować jej przetwarzanie.źródło
Może mógłbyś mieć jeden
Processor
na kraj?I jedna klasa bazowa do obsługi typowych części przetwarzania:
Powinieneś także przerobić typy zwracanych danych, ponieważ nie będą się kompilować tak, jak je napisałeś - czasami
string
metoda nic nie zwraca.źródło
Możesz utworzyć wspólny interfejs za pomocą
Process
metody ...Następnie wdrażasz go dla każdego kraju ...
Następnie możesz stworzyć wspólną metodę tworzenia instancji i wykonywania każdej klasy związanej z krajem ...
Następnie musisz po prostu utworzyć i używać procesorów tak ...
Oto działający przykład skrzypiec dotnet ...
Umieszczasz wszystkie przetwarzanie specyficzne dla kraju w każdej klasie kraju. Utwórz wspólną klasę (w klasie Przetwarzanie) dla wszystkich rzeczywistych indywidualnych metod, aby każdy procesor kraju stał się listą innych wspólnych wywołań, zamiast kopiować kod w każdej klasie kraju.
Uwaga: musisz dodać ...
aby metoda statyczna utworzyła instancję klasy kraju.
źródło
Process
i zamiast tego użyć go raz, aby uzyskać prawidłowy procesor IP? Zazwyczaj przetwarzasz dużo tekstu zgodnie z przepisami tego samego kraju.Process("GBR", "text");
wykonuje metodę statyczną, która tworzy instancję procesora GBR i wykonuje na tym metodę Process. Wykonuje to tylko w jednym przypadku dla tego konkretnego typu kraju.Kilka wersji temu C # swtich otrzymało pełne wsparcie dla dopasowania wzorca . Łatwo jest więc wykonać przypadek „dopasowania wielu krajów”. Chociaż nadal nie ma zdolności do upadku, jedno wejście może dopasować wiele przypadków z dopasowaniem wzorca. Może to sprawić, że ten spam będzie nieco jaśniejszy.
Npw przełącznik można zwykle zastąpić kolekcją. Musisz używać Delegatów i Słownika. Proces można zastąpić.
Następnie możesz stworzyć Słownik:
Użyłem functionNames do przekazania Delegata. Ale możesz użyć składni Lambda, aby podać tam cały kod. W ten sposób możesz po prostu ukryć całą kolekcję, tak jak każdą inną dużą kolekcję. Kod staje się prostym wyszukiwaniem:
Są to właściwie dwie opcje. Możesz rozważyć użycie Wyliczeń zamiast ciągów dla dopasowania, ale jest to drobny szczegół.
źródło
Być może wybrałbym (w zależności od szczegółów twojego przypadku użycia)
Country
bycie „prawdziwym” obiektem zamiast łańcucha. Słowem kluczowym jest „polimorfizm”.Zasadniczo wyglądałoby to tak:
Następnie możesz utworzyć wyspecjalizowane kraje dla tych, których potrzebujesz. Uwaga: nie musisz tworzyć
Country
obiektów dla wszystkich krajów, możesz je miećLatinlikeCountry
, a nawetGenericCountry
. Tam możesz zebrać, co należy zrobić, a nawet ponownie użyć innych, takich jak:Lub podobne.
Country
może tak naprawdęLanguage
nie jestem pewien co do przypadku użycia, ale rozumiem o co chodzi.Ponadto, metoda nie powinna oczywiście
Process()
być rzeczą, którą naprawdę musisz zrobić. CośWords()
lub coś.źródło
Chcesz przekazać (skinąć głową na łańcuch odpowiedzialności) coś, co wie o swojej kulturze. Więc użyj lub stwórz konstrukcję typu Country lub CultureInfo, jak wspomniano powyżej w innych odpowiedziach.
Ale ogólnie i zasadniczo twój problem polega na tym, że bierzesz konstrukcje proceduralne, takie jak „procesor” i stosujesz je do OO. OO polega na reprezentowaniu rzeczywistych koncepcji z dziedziny biznesu lub problematyki w oprogramowaniu. Procesor nie przekłada się na nic w świecie rzeczywistym oprócz samego oprogramowania. Ilekroć masz zajęcia takie jak Procesor, Menedżer lub Gubernator, powinny zadzwonić dzwonki alarmowe.
źródło
Łańcuch odpowiedzialności jest tym, czego możesz szukać, ale w OOP jest nieco kłopotliwy ...
Co z bardziej funkcjonalnym podejściem do C #?
UWAGA: Oczywiście nie musi to być wszystko statyczne. Jeśli klasa procesu potrzebuje stanu, możesz użyć klasy instancji lub częściowo zastosowanej funkcji;).
Możesz zbudować Proces dla każdego kraju podczas uruchamiania, przechowywać każdy z nich w zindeksowanej kolekcji i odzyskać je w razie potrzeby z kosztem O (1).
źródło
Chciałbym po prostu wdrożyć procedury
Capitalise
,RemovePunctuation
itp jako podprocesów, które mogą być z messagedtext
icountry
parametrów, a wróci przetworzony tekst.Skorzystaj ze słowników, aby pogrupować kraje, które pasują do określonego atrybutu (jeśli wolisz listy, to działałoby również przy niewielkim koszcie wydajności). Na przykład:
CapitalisationApplicableCountries
iPunctuationRemovalApplicableCountries
.źródło
Uważam, że informacje o krajach powinny być przechowywane w danych, a nie w kodzie. Dlatego zamiast klasy CountryInfo lub słownika CapitalizationApplicableCountries możesz mieć bazę danych z zapisem dla każdego kraju i polem dla każdego etapu przetwarzania, a następnie przetwarzanie może przejść przez pola dla danego kraju i odpowiednio przetworzyć. Utrzymanie odbywa się wtedy głównie w bazie danych, a nowy kod jest potrzebny tylko wtedy, gdy potrzebne są nowe kroki, a dane mogą być czytelne dla ludzi w bazie danych. Zakłada się, że kroki są niezależne i nie kolidują ze sobą; jeśli tak nie jest, sprawy są skomplikowane.
źródło