객체 지향 프로그래밍-변수에 따라 약간 다른 프로세스에서 중복을 피하는 방법


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진술을 가진 거대한 클래스 하나를 원하지 않지만 약간 다른 논리를 가진 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

모든 옵션을 한 클래스로 캡슐화하는 것이 좋습니다.

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

너무 전문화 된 옵션이없는 한, 나는 확실히 이런 식으로 갈 것입니다. 옵션이 텍스트 파일로 직렬화되면 프로그래머가 아닌 사용자도 응용 프로그램을 변경할 필요없이 새로운 변형을 정의하고 기존 변형을 업데이트 할 수 있습니다.
Tom

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나라 당 하나를 가질 수 있습니까?

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();
    }
}

그리고 처리의 공통 부분을 처리하는 하나의 기본 클래스 :

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 4

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."));

작동하는 닷넷 바이올린 예제는 다음과 같습니다.

모든 국가 별 처리는 각 국가 클래스에 배치합니다. 모든 실제 개별 메소드에 대해 공통 클래스 (처리 클래스에서)를 작성하여 각 국가 별 프로세서가 각 국가 클래스의 코드를 복사하는 대신 다른 공통 호출 목록이됩니다.

참고 : 추가해야합니다 ...

using System.Assembly;

정적 메소드가 국가 클래스의 인스턴스를 작성하기 위해.


반사 코드가없는 것에 비해 반사가 현저하게 느리지 않습니까? 이 경우 가치가 있습니까?
jlvaquero

@jlvaquero 아니오, 반사가 전혀 느리지 않습니다. 물론 디자인 타임에 유형을 지정하면 성능이 저하되지만 실제로는 성능 차이가 무시할 만하고 과도하게 사용하는 경우에만 눈에.니다. 일반 객체 처리를 중심으로 구축 된 대규모 메시징 시스템을 구현했으며 성능에 전혀 의문의 여지가 없었으며 이는 엄청난 양의 처리량입니다. 성능에 눈에 띄는 차이가 없으면 항상 이와 같이 코드를 유지 관리하기가 간단합니다.
Monica Monica Cellio 복원

반영하는 경우에 대한 각 호출에서 국가 문자열을 제거 Process하고 대신 한 번만 사용하여 올바른 IProcessor를 얻으시겠습니까? 일반적으로 같은 국가의 규칙에 따라 많은 텍스트를 처리합니다.
Davislor 2011

@Davislor 이것이 바로이 코드가하는 일입니다. 호출 Process("GBR", "text");하면 GBR 프로세서의 인스턴스를 작성하는 정적 메소드를 실행하고 그에 대한 Process 메소드를 실행합니다. 특정 국가 유형에 대해 하나의 인스턴스에서만 실행됩니다.
복원 Monica Monica Cellio

@Archer 맞습니다. 따라서 같은 국가의 규칙에 따라 여러 문자열을 처리하는 일반적인 경우 인스턴스를 한 번 생성하는 것이 더 효율적입니다. 또는 해시 테이블 / 사전에서 상수 인스턴스를 찾아서 반환하는 것이 더 효율적입니다. 그것에 대한 참조. 그런 다음 동일한 인스턴스에서 텍스트 변환을 호출 할 수 있습니다. 모든 통화에 대해 새 인스턴스를 생성 한 다음 모든 통화에 재사용하지 않고 버리는 것은 낭비입니다.
Davislor

3

몇 가지 버전 전에 C # swtich가 주어졌습니다. 패턴 일치를 완벽하게 지원했습니다 . 따라서 "여러 국가 일치"사례를 쉽게 수행 할 수 있습니다. 여전히 통과 능력은 없지만 하나의 입력으로 여러 경우를 패턴 일치로 일치시킬 수 있습니다. 스팸 일 경우 좀 더 명확하게 만들 수 있습니다.

Npw는 일반적으로 스위치를 컬렉션으로 교체 할 수 있습니다. 대리인과 사전을 사용해야합니다. 프로세스를 대체 할 수 있습니다.

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

아마 (귀하의 유스 케이스의 세부 사항에 따라) 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
나는 좀 더 말을 썼지 만 이것이 기본적으로 내가 가장 좋아하는 것 같아요. 유스 케이스가 국가 문자열을 기반으로 이러한 오브젝트를 찾아야하는 경우 Christopher의 솔루션을 사용할 수 있습니다. 인터페이스 구현은 시간이 아닌 공간을 최적화하기 위해 인스턴스가 Michal의 답변과 같은 특성을 설정하는 클래스 일 수도 있습니다.
Davislor

1

당신은 자신의 문화에 대해 알고있는 무언가에 책임을 져야합니다. 따라서 다른 답변에서 위에서 언급 한 것처럼 Country 또는 CultureInfo 유형 구문을 사용하거나 만드십시오.

그러나 일반적으로 근본적으로 문제는 '프로세서'와 같은 절차 적 구성을 취하여 OO에 적용하는 것입니다. OO는 소프트웨어의 비즈니스 또는 문제 영역에서 실제 개념을 나타내는 것입니다. 프로세서는 소프트웨어 자체와는 별도로 현실 세계의 어떤 것도 번역하지 않습니다. 프로세서, 관리자 또는 주지사와 같은 수업이있을 때마다 알람 벨이 울립니다.


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;
      }
    }
  }
}

참고 : 물론 모든 정적 일 필요는 없습니다. Process 클래스에 상태가 필요한 경우 인스턴스화 된 클래스 또는 부분적으로 적용되는 함수를 사용할 수 있습니다.).

시작할 때 각 국가에 대한 프로세스를 구축하고, 각 국가를 인덱스 컬렉션에 저장하고 필요할 때 O (1) 비용으로 검색 할 수 있습니다.


0

오래 전에이 주제에 대해“개체”라는 용어를 만들어서 유감스럽게 생각합니다. 왜냐하면 많은 사람들이 더 적은 아이디어에 집중할 수 있기 때문입니다. 큰 아이디어는 메시지 입니다.

~ Alan Kay, 메시징

나는 간단하게 루틴을 구현하는 것이 Capitalise, RemovePunctuation를 사용해 메세지가 될 수있다 서브 프로세스로 등 textcountry매개 변수, 처리 된 텍스트를 반환합니다.

사전을 사용하여 특정 속성에 맞는 국가를 그룹화하십시오 (목록을 선호하는 경우 약간의 성능 비용으로도 잘 작동 함). 예를 들면 다음 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.