私の現在の仕事でかなり多く浮かび上がっているのは、一般化されたプロセスが発生する必要があるということですが、そのプロセスの奇妙な部分は特定の変数の値に応じてわずかに異なる方法で発生する必要があります。これを処理する最もエレガントな方法は何でしょうか。
私たちが通常使用している例を使用します。これは、処理する国によって少し異なる動作をします。
クラスがあるので、それを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);
}
}
ただし、特定の国ではこれらのアクションの一部のみを実行する必要があります。たとえば、6か国のみが資本化ステップを必要とします。国によって分割するキャラクターが異なる場合があります。国によっては、アクセント付き'e'
の交換のみが必要になる場合があります。
明らかに、次のようなことで解決できます。
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);
}
しかし、世界のすべての可能な国を扱っているとき、それは非常に面倒になります。それに関係なく、if
ステートメントはロジックを読みにくくし(少なくとも、例よりも複雑な方法を想像している場合)、循環的複雑度はかなり速く上昇し始めます。
だから今のところ、私は次のようなことをしている:
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);
}
}
ハンドラー:
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; }
}
同様に私は本当に好きかどうかはわかりません。ロジックは、すべてのファクトリ作成によってまだいくらか隠されており、元のメソッドを単に見て、たとえば「GBR」プロセスが実行されたときに何が起こるかを確認することはできません。また、GbrPunctuationHandler
、UsaPunctuationHandler
などのスタイルで多くのクラスを(これよりも複雑な例では)作成することになります。つまり、いくつかの異なるクラスを調べて、句読点の処理中に発生する可能性のあるすべてのアクションを理解します。明らかに、10億個のif
ステートメントを持つ1つの巨大なクラスは必要ありませんが、ロジックがわずかに異なる20個のクラスも不格好に感じます。
基本的に、私は自分が何らかのOOP=ノットになっていると思います。それを解くための良い方法がよくわからないのです。そこに役立つパターンがあるのかと思っていました。このタイプのプロセス?
このタイプのプロセスに役立つパターンがそこにあるのかと思っていました
責任の連鎖 はあなたが探しているようなものですが、OOPはやや面倒です...
C#を使用したより機能的なアプローチについてはどうですか?
using System;
namespace Kata {
class Kata {
static void Main() {
var text = " testing this thing for DEU ";
Console.WriteLine(Process.For("DEU")(text));
text = " testing this thing for USA ";
Console.WriteLine(Process.For("USA")(text));
Console.ReadKey();
}
public static class Process {
public static Func<string, string> For(string country) {
Func<string, string> baseFnc = (string text) => text;
var aggregatedFnc = ApplyToUpper(baseFnc, country);
aggregatedFnc = ApplyTrim(aggregatedFnc, country);
return aggregatedFnc;
}
private static Func<string, string> ApplyToUpper(Func<string, string> currentFnc, string country) {
string toUpper(string text) => currentFnc(text).ToUpper();
Func<string, string> fnc = null;
switch (country) {
case "USA":
case "GBR":
case "DEU":
fnc = toUpper;
break;
default:
fnc = currentFnc;
break;
}
return fnc;
}
private static Func<string, string> ApplyTrim(Func<string, string> currentFnc, string country) {
string trim(string text) => currentFnc(text).Trim();
Func<string, string> fnc = null;
switch (country) {
case "DEU":
fnc = trim;
break;
default:
fnc = currentFnc;
break;
}
return fnc;
}
}
}
}
注:もちろん、すべてが静的である必要はありません。プロセスクラスが状態を必要とする場合、インスタンス化されたクラスまたは部分的に適用された関数を使用できます;)。
起動時に国ごとにプロセスを構築し、それぞれをインデックス付きコレクションに保存して、必要に応じてO(1) cost)で取得できます。
国に関する情報は、コードではなくデータで保持する必要があると思います。そのため、CountryInfoクラスやCapitalisationApplicableCountries辞書の代わりに、国ごとのレコードと処理ステップごとのフィールドを持つデータベースを作成し、処理は特定の国のフィールドを通過してそれに応じて処理することができます。メンテナンスは主にデータベースで行われ、新しいコードが必要になるのは新しい手順が必要な場合のみであり、データベースでデータを人間が読み取れるようにすることができます。これは、手順が独立していて、互いに干渉しないことを前提としています。そうでなければ、物事は複雑です。
多くの人があまり重要ではないアイデアに集中できるようになるため、このトピックの「オブジェクト」という用語をかなり以前に作り出したのは残念です。 大きなアイデアはメッセージングです。
〜Alan Kay、 メッセージングについて
Capitalise
およびRemovePunctuation
パラメータでメッセージを送信できるサブプロセスとして、ルーチンtext
、country
などを単純に実装し、処理されたテキストを返します。
辞書を使用して、特定の属性に適合する国をグループ化します(リストを使用したい場合は、わずかなパフォーマンスコストで機能します)。例:CapitalisationApplicableCountries
およびPunctuationRemovalApplicableCountries
。
/// Runs like a pipe: passing the text through several stages of subprocesses
public string Process(string country, string text)
{
text = Capitalise(country, text);
text = RemovePunctuation(country, text);
// And so on and so forth...
return text;
}
private string Capitalise(string country, string text)
{
if ( ! CapitalisationApplicableCountries.ContainsKey(country) )
{
/* skip */
return text;
}
/* do the capitalisation */
return capitalisedText;
}
private string RemovePunctuation(string country, string text)
{
if ( ! PunctuationRemovalApplicableCountries.ContainsKey(country) )
{
/* skip */
return text;
}
/* do the punctuation removal */
return punctuationFreeText;
}
private string Replace(string country, string text)
{
// Implement it following the pattern demonstrated earlier.
}