Nesneye Yönelik Programlama - bir değişkene bağlı olarak biraz farklılık gösteren işlemlerde çoğaltma nasıl önlenir


64

Şu anki işimde çok fazla ortaya çıkan bir şey, gerçekleşmesi gereken genelleştirilmiş bir süreç olduğu, ancak o zaman bu sürecin garip kısmının belirli bir değişkenin değerine bağlı olarak biraz farklı olması gerekiyor ve ben değilim bununla başa çıkmanın en zarif yolunun ne olduğundan emin.

Genelde sahip olduğumuz, üzerinde çalıştığımız ülkeye bağlı olarak işleri biraz farklı yapan örneği kullanacağım.

Bir dersim var, diyelim ki 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);
    }
}

Ancak bu eylemlerin sadece bir kısmının belirli ülkeler için yapılması gerekir. Örneğin, yalnızca 6 ülke kapitalizasyon adımına ihtiyaç duymaktadır. Bölünecek karakter ülkeye bağlı olarak değişebilir. Aksanlı olanların değiştirilmesi 'e'yalnızca ülkeye bağlı olarak gerekebilir.

Açıkçası böyle bir şey yaparak çözebilirsiniz:

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

Ancak dünyadaki tüm olası ülkelerle uğraşırken, bu çok hantal hale gelir. Ve ne olursa olsun, ififadeler mantığı okumayı zorlaştırır (en azından örnekten daha karmaşık bir yöntem hayal ediyorsanız) ve siklomatik karmaşıklık oldukça hızlı bir şekilde kaymaya başlar.

Şu anda böyle bir şey yapıyorum:

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

Eylemciler:

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

Hangi eşit sevmiyorum gerçekten emin değilim. Mantık hala tüm fabrika oluşturma işlemleri tarafından gizlenmiştir ve örneğin orijinal yönteme bakamaz ve örneğin bir "GBR" işlemi yürütüldüğünde ne olacağını göremezsiniz. Ayrıca tarzında (bundan daha karmaşıktır örneklerde), birçok sınıfı oluşturma sonunda GbrPunctuationHandler, UsaPunctuationHandlernoktalama işaretsiz sırasında olabilirdi olası tüm işlemleri dışarı şekil için birkaç farklı sınıflara bakmak zorunda olduğunu hangi araçlar ... vb, taşıma. Açıkçası if, milyarlarca ifadeye sahip dev bir sınıf istemiyorum , ama aynı derecede biraz farklı mantığa sahip 20 sınıf da tıknaz hissediyor.

Temelde kendimi bir çeşit OOP düğümüne aldım ve onu çözmek için iyi bir yol bilmiyorum. Orada bu tür işlemlere yardımcı olacak bir model olup olmadığını merak ediyordum.


Görünüşe PreProcessgöre, bazı ülkelere göre farklı şekilde uygulanabilen bir işlevsellik DetermineSeparatorvar, hepsi için orada olabilir ve a PostProcess. Hepsi protected virtual voidvarsayılan bir uygulama ile olabilir ve daha sonra Processorsher ülke için belirli olabilir
Icepickle

İşiniz, belirli bir zaman diliminde, sizin ya da bir başkası tarafından çalışan ve öngörülebilir gelecekte sürdürülebilecek bir şey yapmaktır. Birkaç seçenek her iki koşulu da karşılayabilirse, beğenilerinize göre bunlardan herhangi birini seçebilirsiniz.
Dialecticus

2
Sizin için uygun bir seçenek bir yapılandırmaya sahip olmaktır. Kodunuzda belirli bir ülkeyi değil, belirli yapılandırma seçeneğini kontrol edersiniz. Ancak her ülkenin bu yapılandırma seçeneklerinden belirli bir kümesi olacaktır. Örneğin if (country == "DEU")kontrol edin if (config.ShouldRemovePunctuation).
Dialecticus

11
Ülkeler çeşitli seçenekler varsa, neden olduğu countrybir dize yerine bir sınıf modelleyen bu seçenekler örneğidir?
Damien_The_Unbeliever

@Damien_The_Unbeliever - bu konuda biraz ayrıntı verebilir misiniz? Robert Brautigam'ın cevabı, önerileriniz doğrultusunda aşağıda mı? - ah cevabını şimdi görebilirsin, teşekkürler!
John Darvill

Yanıtlar:


53

Tüm seçenekleri bir sınıfta kapsüllemenizi öneririm:

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

ve Processyönteme aktarın:

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
Neden böyle bir şey atlamadan önce denenmedi emin değilim CountrySpecificHandlerFactory... o_0
Mateen Ulhaq

Çok özel seçenekler olmadığı sürece, kesinlikle bu şekilde giderdim. Seçenekler metin dosyasına serileştirilirse, programcı olmayanların uygulamada değişiklik yapmadan yeni varyantlar tanımlamasına / var olanları güncellemesine de olanak tanır.
Tom

4
Bu public class ProcessOptionsgerçekten olmalı [Flags] enum class ProcessOptions : int { ... }...
Sarhoş Kod Maymun

Sanırım ihtiyaçları varsa, bir haritaya sahip olabilirler ProcessOptions. Çok uygun.
theonlygusti

24

.NET çerçevesi bu tür sorunları ele almak için yola çıktığında, her şeyi modellemedi string. Yani, örneğin, CultureInfosınıfa sahipsiniz :

Belirli bir kültür hakkında bilgi sağlar (yönetilmeyen kod geliştirme için yerel ayar denir). Bilgiler, kültürün adlarını, yazma sistemini, kullanılan takvimi, dizelerin sıralama düzenini ve tarihler ve sayılar için biçimlendirmeyi içerir.

Şimdi, bu sınıf ihtiyacınız olan belirli özellikleri içermeyebilir , ancak açıkça benzer bir şey oluşturabilirsiniz. Ve sonra Processyönteminizi değiştirirsiniz :

public string Process(CountryInfo country, string text)

Daha CountryInfosonra sınıfınızda, yönteminizin işlenmesini uygun şekilde yönlendirmesine bool RequiresCapitalizationyardımcı olan bir özellik vb Process. Bulunabilir.


13

Belki Processorher ülke için bir tane olabilir ?

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

Ve işlemin ortak bölümlerini işlemek için bir temel sınıf:

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

Ayrıca, dönüş türlerinizi yeniden yazmalısınız çünkü yazdığınız gibi derlenmez - bazen bir stringyöntem hiçbir şey döndürmez.


Sanırım kompozisyonu kalıtım mantra üzerinde takip etmeye çalışıyordum. Ama evet bu kesinlikle bir seçenek, cevap için teşekkürler.
John Darvill

Yeterince adil. Bazı durumlarda kalıtımın haklı olduğunu düşünüyorum ama gerçekten nasıl yükleyeceğiniz / depolayacağınız / çağırdığınız / yöntemlerinizi ve işleminizi nasıl değiştireceğinize bağlı.
Corentin Pane

3
Bazen, kalıtım iş için doğru araçtır. Birkaç farklı durumda çoğunlukla aynı şekilde davranacak bir süreciniz varsa , ancak farklı durumlarda farklı davranacak birkaç parçaya sahipseniz, bu kalıtım kullanmayı düşünmeniz gereken iyi bir işarettir.
Tanner Swett

5

Bir Processyöntemle ortak bir arayüz oluşturabilirsiniz ...

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

Sonra her ülke için uygularsınız ...

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

Daha sonra ülkeyle ilgili her sınıfı başlatmak ve yürütmek için ortak bir yöntem oluşturabilirsiniz ...

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

O zaman böyle işlemcileri oluşturmanız ve kullanmanız gerekir ...

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

İşte çalışan dotnet keman örneği ...

Ülkeye özgü tüm işlemleri her ülke sınıfına yerleştirirsiniz. Tüm gerçek yöntemler için ortak bir sınıf (İşleme sınıfında) oluşturun, böylece her ülke işlemcisi her ülke sınıfındaki kodu kopyalamak yerine diğer ortak çağrıların bir listesi haline gelir.

Not: Eklemeniz gerekecek ...

using System.Assembly;

statik yöntemin ülke sınıfının bir örneğini oluşturması için.


Yansıma, yansıyan koda kıyasla oldukça yavaş değil mi? bu dava için buna değer mi?
jlvaquero

@jlvaquero Hayır, yansıma hiç de yavaş değil. Tabii ki, tasarım zamanında bir tür belirtmekle ilgili bir performans isabet var, ama gerçekten ihmal edilebilir bir performans farkı ve sadece aşırı kullandığınızda fark edilebilir. Genel nesne işleme üzerine kurulu büyük mesajlaşma sistemleri uyguladım ve performansı hiç sorgulamak için hiçbir nedenimiz yoktu ve bu da büyük miktarda verim ile. Performansta gözle görülür bir fark olmadan, kodu korumak için her zaman basit olacağım, böyle.
Monica Cellio

Yansıtıyorsanız, ülke dizesini her çağrıdan kaldırmak Processve bunun yerine doğru IPişlemciyi almak için bir kez kullanmak istemez miydiniz ? Genellikle aynı ülkenin kurallarına göre çok fazla metin işliyorsunuzdur.
Davislor

@Davislor Bu kod tam olarak bunu yapıyor. Aradığınızda Process("GBR", "text");, GBR işlemcisinin bir örneğini oluşturan statik yöntemi yürütür ve bu işlemde İşlem yöntemini yürütür. Belirli bir ülke türü için yalnızca bir örnek üzerinde yürütür.
Monica Cellio

@Archer Doğru, bu nedenle aynı ülkenin kurallarına göre birden fazla dizeyi işlediğiniz tipik durumda, örneği bir kez oluşturmak veya bir karma tabloda / Sözlüğe sabit bir örneği aramak ve geri dönmek daha verimli olur. buna bir referans. Daha sonra aynı örnekte metin dönüşümünü çağırabilirsiniz. Her çağrı için yeni bir örnek oluşturmak ve her çağrı için yeniden kullanmak yerine onu silmek israftır.
Davislor

3

Birkaç versiyon önce, C # swtich'e desen eşleştirme için tam destek verildi . Böylece "birden fazla ülke eşleşmesi" durumu kolayca yapılabilir. Hâlâ düşme yeteneği olmamasına rağmen, bir giriş desen eşleşmesi ile birden fazla durumu eşleştirebilir. Belki de if-spam'i biraz daha açık hale getirebilir.

Npw anahtarı genellikle Koleksiyon ile değiştirilebilir. Delegeler ve Sözlük kullanmanız gerekir. İşlem ile değiştirilebilir.

public delegate string ProcessDelegate(string text);

Sonra bir Sözlük yapabilirsiniz:

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

Temsilci teslim etmek için functionNames kullandım. Ancak tüm kodu orada sağlamak için Lambda sözdizimini kullanabilirsiniz. Bu şekilde, tüm Koleksiyonu diğer büyük koleksiyonlarda olduğu gibi gizleyebilirsiniz. Ve kod basit bir arama haline gelir:

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

Bunlar hemen hemen iki seçenek. Eşleme için dizeler yerine Numaralandırmalar kullanmayı düşünebilirsiniz, ancak bu küçük bir ayrıntıdır.


2

Belki de (kullanım durumunuzun ayrıntılarına bağlı olarak) Countrybir dize yerine "gerçek" bir nesne olmak ile gider . Anahtar kelime "polimorfizm" dir.

Temel olarak şöyle görünecektir:

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

O zaman ihtiyacınız olanlar için özel ülkeler oluşturabilirsiniz. Not: CountryTüm ülkeler için nesne oluşturmanız gerekmez LatinlikeCountry, sahip olabilir veya hatta yapabilirsiniz GenericCountry. Orada yapılması gerekenler toplayabilir, hatta başkalarını yeniden kullanarak:

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

Veya benzeri. Countryaslında Language, kullanım durumundan emin değilim, ama anladım.

Ayrıca, elbette yöntem, Process()aslında ihtiyacınız olan şey olmalıdır. Beğen Words()ya da her neyse.


1
Daha kötü bir şey yazdım, ama bence bu en çok sevdiğim şey. Kullanım durumunun bu nesneleri bir ülke dizgisine göre araması gerekiyorsa, Christopher'ın çözümünü bununla birlikte kullanabilir. Arayüzlerin uygulanması, zamandan ziyade alanı optimize etmek için örnekleri Michal'in cevabındaki gibi özellikler belirleyen bir sınıf bile olabilir.
Davislor

1

Kendi kültürünü bilen bir şeye (sorumluluk zincirine başını salmak) yetki vermek istiyorsunuz. Bu yüzden yukarıda diğer yanıtlarda belirtildiği gibi bir Ülke veya CultureInfo türü yapı kullanın veya yapın.

Ancak genel olarak ve temelde sizin probleminiz 'işlemci' gibi prosedürel yapıları alıp OO'ya uygulamanızdır. OO, yazılımdaki bir işletme veya problem alanından gerçek dünya kavramlarını temsil etmekle ilgilidir. İşlemci, yazılımın dışında gerçek dünyada hiçbir şeye tercüme etmez. İşlemci veya Yönetici veya Vali gibi sınıflarınız olduğunda alarm zillerinin çalması gerekir.


0

Orada bu tür süreçlere yardımcı olacak bir model olup olmadığını merak ediyordum

Sorumluluk zinciri, aradığınız bir şeydir, ancak OOP'ta biraz hantal ...

C # ile daha işlevsel bir yaklaşıma ne dersiniz?

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

NOT: Elbette hepsi statik olmak zorunda değildir. Process sınıfının durumu gerekiyorsa, örneklenmiş bir sınıf veya kısmen uygulanan bir işlev kullanabilirsiniz;).

İşlemi başlangıçta her ülke için oluşturabilir, her birini dizinlenmiş bir koleksiyonda saklayabilir ve gerektiğinde O (1) maliyetle alabilirsiniz.


0

Uzun zaman önce bu konu için “nesneler” terimini yarattığım için özür dilerim, çünkü birçok insan daha az fikre odaklanıyor. Büyük fikir mesajlaşma .

~ Alan Kay, Mesajlaşma Üzerine

Ben sadece bir ve parametreleri ile mesajlaşabilir ve işlenmiş bir metin dönecek alt işlemler olarak rutinleri Capitalise, RemovePunctuationvb uygulamak .textcountry

Belirli bir özelliğe uyan ülkeleri gruplamak için sözlükler kullanın (listeleri tercih ederseniz, bu yalnızca küçük bir performans maliyetiyle de işe yarayabilir). Örneğin: CapitalisationApplicableCountriesve 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

Ülkeler hakkındaki bilgilerin kodda değil, verilerinde saklanması gerektiğini düşünüyorum. Bu nedenle, bir CountryInfo sınıfı veya CapitalisationApplicableCountries sözlüğü yerine, her ülke için bir kayıt ve her işlem adımı için bir alan içeren bir veritabanınız olabilir ve daha sonra işlem, belirli bir ülke için alanlardan geçebilir ve buna göre işlem yapabilir. Bakım daha sonra veritabanında, yeni kod sadece yeni adımlar gerektiğinde gereklidir ve veriler veritabanında okunabilir. Bu, adımların bağımsız olduğunu ve birbirlerini etkilemediğini varsayar; eğer öyle değilse işler karmaşıktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.