オブジェクト指向プログラミング-変数によってわずかに異なるプロセスでの重複を回避する方法


64

私の現在の仕事でかなり多く浮かび上がっているのは、一般化されたプロセスが発生する必要があるということですが、そのプロセスの奇妙な部分は特定の変数の値に応じてわずかに異なる方法で発生する必要があります。これを処理する最もエレガントな方法は何でしょうか。

私たちが通常使用している例を使用します。これは、処理する国に応じて少し異なる動作をします。

だから私はクラスを持っています、それを呼びましょう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ノットに陥っていると思いますが、それを解くための良い方法がよくわかりません。このタイプのプロセスに役立つパターンがそこにあるかどうか疑問に思いましたか?


PreProcess一部の国に基づいて異なる方法で実装できる機能DetermineSeparatorがあり、すべての国で使用できる機能があるようですPostProcess。それらすべてをprotected virtual voidデフォルトの実装にすることができ、Processors国ごとに特定のものにすることができます
Icepickle

あなたの仕事は、与えられた時間枠内で、あなたや他の誰かが機能し、予見可能な将来に維持できる何かを作ることです。いくつかのオプションが両方の条件を満たすことができる場合は、好みに応じて、それらのいずれかを自由に選択できます。
Dialecticus

2
あなたにとって実行可能なオプションは、設定をすることです。したがって、コードでは特定の国をチェックするのではなく、特定の構成オプションをチェックします。ただし、各国にはこれらの構成オプションの特定のセットがあります。たとえば、if (country == "DEU")あなたの代わりにチェックしてくださいif (config.ShouldRemovePunctuation)
Dialecticus

11
国は、さまざまなオプションを持っている場合は、その理由である文字列ではなくクラスをモデルこれらのオプションのインスタンスは?country
Damien_The_Unbeliever

@Damien_The_Unbeliever-これについて少し詳しく教えてください。Robert Brautigamの回答は、あなたが提案しているものに沿っていますか?-ああ、今あなたの答えを見ることができます、ありがとう!
John Darvill、

回答:


53

すべてのオプションを1つのクラスにカプセル化することをお勧めします。

public class ProcessOptions
{
  public bool Capitalise { get; set; }
  public bool RemovePunctuation { get; set; }
  public bool Replace { get; set; }
  public char ReplaceChar { get; set; }
  public char ReplacementChar { get; set; }
  public char JoinChar { get; set; }
  public char SplitChar { get; set; }
}

そしてそれをProcessメソッドに渡します:

public string Process(ProcessOptions options, string text)
{
  if(options.Capitalise)
    text.Capitalise();

  if(options.RemovePunctuation)
    text.RemovePunctuation();

  if(options.Replace)
    text.Replace(options.ReplaceChar, options.ReplacementChar);

  var split = text.Split(options.SplitChar);

  string.Join(options.JoinChar, split);
}

4
ジャンプする前にこのようなものが試されなかった理由がCountrySpecificHandlerFactoryわかりません... o_0
Mateen Ulhaq

特別なオプションがない限り、私は間違いなくこの方法をとります。オプションがテキストファイルにシリアル化されている場合、プログラマー以外でも、アプリケーションに変更を加えることなく、新しいバリアントを定義したり、既存のバリアントを更新したりできます。
トム

4
それpublic class ProcessOptionsは本当に[Flags] enum class ProcessOptions : int { ... }
酒に酔ったコードモンキー

そして、もし彼らが必要なら、彼らは国の地図を持っているかもしれないと思いProcessOptionsます。とても便利。
theonlygusti

24

.NETフレームワークがこの種の問題の処理に着手したとき、すべてをとしてモデル化していませんでしたstring。たとえば、次のCultureInfoクラスがあります。

特定のカルチャ(アンマネージコード開発のロケールと呼ばれる)に関する情報を提供します。情報には、カルチャの名前、書記体系、使用するカレンダー、文字列のソート順、日付と数値のフォーマットが含まれます。

現在、このクラスには必要な特定の機能が含まれていない場合がありますが、類似の機能を作成することはできます。次に、Processメソッドを変更します。

public string Process(CountryInfo country, string text)

あなたのCountryInfoクラスは、次に持つことができbool RequiresCapitalization、あなたの助けなどプロパティ、Processメソッドが適切にその処理を指示します。


13

多分あなたはProcessor国ごとに1 つ持つことができますか?

public class FrProcessor : Processor {
    protected override string Separator => ".";

    protected override string ProcessSpecific(string text) {
        return text.Replace("é", "e");
    }
}

public class UsaProcessor : Processor {
    protected override string Separator => ",";

    protected override string ProcessSpecific(string text) {
        return text.Capitalise().RemovePunctuation();
    }
}

そして、処理の共通部分を処理する1つの基本クラス:

public abstract class Processor {
    protected abstract string Separator { get; }

    protected virtual string ProcessSpecific(string text) { }

    private string ProcessCommon(string text) {
        var split = text.Split(Separator);
        return string.Join("|", split);
    }

    public string Process(string text) {
        var s = ProcessSpecific(text);
        return ProcessCommon(s);
    }
}

また、戻り値の型は、記述したとおりにコンパイルされないため、再作成する必要がありますstring。メソッドが何も返さない場合もあります。


継承のマントラを超えた構成を追おうとしていたのではないでしょうか。しかし、はい、それは間違いなくオプションです。返信ありがとうございます。
John Darvill

けっこうだ。継承は正当化される場合もあると思いますが、それは実際には、メソッドの読み込み/保存/呼び出し/変更をどのように計画するかに依存します。
Corentin Pane

3
時々、継承は仕事に適したツールです。いくつかの異なる状況でほとんど同じように動作するプロセスがあり、異なる状況で異なる動作をするいくつかの部分があるプロセスがある場合、それは継承の使用を検討する必要がある良い兆候です。
Tanner Swett、

5

Processメソッドで共通のインターフェースを作成できます...

public interface IProcessor
{
    string Process(string text);
}

次に、国ごとに実装します...

public class Processors
{
    public class GBR : IProcessor
    {
        public string Process(string text)
        {
            return $"{text} (processed with GBR rules)";
        }
    }

    public class FRA : IProcessor
    {
        public string Process(string text)
        {
            return $"{text} (processed with FRA rules)";
        }
    }
}

次に、各国関連のクラスをインスタンス化して実行するための共通のメソッドを作成できます...

// also place these in the Processors class above
public static IProcessor CreateProcessor(string country)
{
    var typeName = $"{typeof(Processors).FullName}+{country}";
    var processor = (IProcessor)Assembly.GetAssembly(typeof(Processors)).CreateInstance(typeName);
    return processor;
}

public static string Process(string country, string text)
{
    var processor = CreateProcessor(country);
    return processor?.Process(text);
}

次に、そのようなプロセッサを作成して使用する必要があります...

// create a processor object for multiple use, if needed...
var processorGbr = Processors.CreateProcessor("GBR");
Console.WriteLine(processorGbr.Process("This is some text."));

// create and use a processor for one-time use
Console.WriteLine(Processors.Process("FRA", "This is some more text."));

これが実際のドットネットフィドルの例です...

国別のすべての処理を国別クラスに配置します。実際のすべての個々のメソッドに共通のクラスを(Processingクラス内に)作成します。これにより、各国のプロセッサは、各国のクラスのコードをコピーするのではなく、他の一般的な呼び出しのリストになります。

注:追加する必要があります...

using System.Assembly;

静的メソッドが国クラスのインスタンスを作成するために。


リフレクションなしのコードと比較して、リフレクションは著しく遅くありませんか?この場合、それは価値がありますか?
jlvaquero

@jlvaqueroいいえ、リフレクションは著しく遅くはありません。もちろん、設計時にタイプを指定するよりもパフォーマンスに影響がありますが、それは実際には無視できるほどのパフォーマンスの違いであり、使いすぎた場合にのみ顕著になります。私は汎用オブジェクト処理を中心に構築された大規模なメッセージングシステムを実装しましたが、パフォーマンスに疑問を投げかける理由はまったくありませんでした。これには大量のスループットが伴います。パフォーマンスに目立った違いはないので、私は常にこのようにコードを保守するためのシンプルな方法を使います。
モニカチェリオを

反映している場合、国の文字列をへの各呼び出しから削除しProcess、代わりに1回使用して正しいIProcessorを取得しませんか?通常、同じ国の規則に従って多くのテキストを処理します。
Davislor

@Davislorそれはまさにこのコードが行うことです。呼び出すProcess("GBR", "text");と、GBRプロセッサのインスタンスを作成し、その上でProcessメソッドを実行する静的メソッドが実行されます。その特定の国タイプに対して、1つのインスタンスでのみ実行されます。
モニカチェリオを

@Archerそうです。同じ国のルールに従って複数の文字列を処理する一般的なケースでは、インスタンスを一度作成するか、ハッシュテーブル/辞書で定数インスタンスを検索して返す方が効率的です。それへの参照。その後、同じインスタンスでテキスト変換を呼び出すことができます。呼び出しごとに新しいインスタンスを作成して破棄するのは、すべての呼び出しで再利用するのではなく、無駄です。
Davislor、

3

数バージョン前、C#swtichはパターンマッチングを完全にサポートしていました。そのため、「複数の国が一致する」ケースは簡単に実行できます。フォールスルー能力はまだありませんが、1つの入力がパターンマッチングで複数のケースに一致する可能性があります。if-spamが少し明確になるかもしれません。

通常、スイッチはコレクションに置き換えることができます。デリゲートとディクショナリを使用する必要があります。プロセスは置き換えることができます。

public delegate string ProcessDelegate(string text);

次に、辞書を作成します。

var Processors = new Dictionary<string, ProcessDelegate>(){
  { "USA", EnglishProcessor },
  { "GBR", EnglishProcessor },
  { "DEU", GermanProcessor }
}

デリゲートを渡すためにfunctionNamesを使用しました。しかし、Lambda構文を使用して、コード全体をそこに提供できます。そうすれば、他の大規模なコレクションと同じように、コレクション全体を非表示にできます。そして、コードは単純なルックアップになります:

ProcessDelegate currentProcessor = Processors[country];
string processedString = currentProcessor(country);

これらはほぼ2つのオプションです。文字列の代わりに列挙型を使用して照合することを検討することもできますが、これは細かいことです。


2

おそらく(ユースケースの詳細に応じて)Country文字列ではなく「実際の」オブジェクトであると思います。キーワードは「ポリモーフィズム」。

したがって、基本的には次のようになります。

public interface Country {
   string Process(string text);
}

次に、必要な国に特化した国を作成できます。注:あなたが作成する必要はありませんCountryすべての国のオブジェクトを、あなたが持つことができるLatinlikeCountry、あるいはGenericCountry。そこで、次のように他の人を再利用することで、何をすべきかを収集できます。

public class France {
   public string Process(string text) {
      return new GenericCountry().process(text)
         .replace('a', 'b');
   }
}

または類似。Country実際Languageには、ユースケースについてはわかりませんが、私はあなたがポイントを獲得します。

また、メソッドはもちろんProcess()、実際に実行する必要があるものであってはなりません。好きWords()か何でも。


1
私はもっ​​と表現力のあるものを書いたが、これが基本的に私が一番好きなものだと思う。ユースケースが国の文字列に基づいてこれらのオブジェクトを検索する必要がある場合、これでクリストファーのソリューションを使用できます。インターフェースの実装は、インスタンスが時間ではなくスペースを最適化するためにMichalの答えのように特性を設定するクラスであることさえできます。
Davislor

1

あなたは自分の文化について知っている何かに(責任の連鎖にうなずく)委任したいと思います。したがって、他の回答で前述したように、CountryまたはCultureInfoタイプの構成を使用または作成します。

しかし、一般的かつ根本的にあなたの問題は、「プロセッサー」のような手続き構造を取り、それをオブジェクト指向に適用することです。OOとは、ビジネスまたは問題のドメインからの現実世界の概念をソフトウェアで表すことです。プロセッサは、ソフトウェア自体を除いて、現実の世界では何にも変換されません。Processor、Manager、Governorなどのクラスがある場合は常に、警報ベルが鳴ります。


0

このタイプのプロセスに役立つパターンがそこにあるかどうか疑問に思いました

責任の連鎖はあなたが探しているようなものですが、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)コストで必要なときに取得できます。


0

多くの人があまり重要ではないアイデアに集中できるようになるため、このトピックの「オブジェクト」という用語をかなり以前に作り出したのは残念です。大きなアイデアはメッセージングです。

〜Alan Kay、メッセージングについて

私は単にルーチン実装しCapitaliseRemovePunctuationを使ってメッセージが送らすることができますサブプロセスとしてなどを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.
}

0

国に関する情報は、コードではなくデータで保持する必要があると思います。そのため、CountryInfoクラスやCapitalisationApplicableCountries辞書の代わりに、国ごとのレコードと処理ステップごとのフィールドを持つデータベースを作成し、処理は特定の国のフィールドを通過してそれに応じて処理することができます。メンテナンスは主にデータベースで行われ、新しいコードが必要になるのは新しい手順が必要な場合のみであり、データベースでデータを人間が読み取れるようにすることができます。これは、手順が独立していて、互いに干渉しないことを前提としています。そうでなければ、物事は複雑です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.