Pemrograman Berorientasi Objek - cara menghindari duplikasi dalam proses yang sedikit berbeda tergantung pada variabel

64

Sesuatu yang muncul cukup banyak dalam pekerjaan saya saat ini adalah bahwa ada proses umum yang perlu terjadi, tetapi kemudian bagian aneh dari proses itu perlu terjadi sedikit berbeda tergantung pada nilai variabel tertentu, dan saya tidak cukup yakin apa cara paling elegan untuk menangani ini.

Saya akan menggunakan contoh yang biasanya kita miliki, yang melakukan hal-hal yang sedikit berbeda tergantung pada negara yang kita hadapi.

Jadi saya punya kelas, sebut saja 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);
    }
}

Kecuali bahwa hanya beberapa tindakan yang perlu terjadi untuk negara-negara tertentu. Misalnya, hanya 6 negara yang memerlukan langkah kapitalisasi. Karakter untuk dibagi mungkin berubah tergantung pada negara. Mengganti aksen 'e'mungkin hanya diperlukan tergantung pada negara.

Jelas Anda bisa menyelesaikannya dengan melakukan sesuatu seperti ini:

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

Tetapi ketika Anda berurusan dengan semua negara yang mungkin ada di dunia, itu menjadi sangat rumit. Dan terlepas dari itu, ifpernyataan membuat logika lebih sulit untuk dibaca (setidaknya, jika Anda membayangkan metode yang lebih kompleks daripada contoh), dan kompleksitas siklomatik mulai merayap cukup cepat.

Jadi saat ini saya sedang melakukan sesuatu seperti ini:

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

Penangan:

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

Yang sama saya tidak yakin saya suka. Logikanya masih agak dikaburkan oleh semua pembuatan pabrik dan Anda tidak bisa begitu saja melihat metode asli dan melihat apa yang terjadi ketika proses "GBR" dijalankan, misalnya. Anda juga akhirnya menciptakan banyak kelas (dalam contoh yang lebih kompleks dari ini) dalam gaya GbrPunctuationHandler,, UsaPunctuationHandlerdll ... yang berarti Anda harus melihat beberapa kelas yang berbeda untuk mencari tahu semua tindakan yang mungkin bisa terjadi selama tanda baca penanganan. Jelas saya tidak ingin satu kelas raksasa dengan satu miliar ifpernyataan, tetapi sama-sama 20 kelas dengan logika yang sedikit berbeda juga terasa kikuk.

Pada dasarnya saya pikir saya telah mendapatkan semacam simpul OOP dan tidak tahu cara yang baik untuk menguraikannya. Saya bertanya-tanya apakah ada pola di luar sana yang akan membantu dengan jenis proses ini?

John Darvill
sumber
Sepertinya Anda memiliki PreProcessfungsi, yang dapat diimplementasikan secara berbeda berdasarkan pada beberapa negara, DetermineSeparatordapat ada untuk mereka semua, dan a PostProcess. Semuanya dapat protected virtual voiddengan implementasi default, dan kemudian Anda dapat memiliki spesifik Processorsper negara
Icepickle
Tugas Anda adalah membuat kerangka waktu tertentu menjadi sesuatu yang berhasil, dan dapat dipertahankan di masa mendatang, oleh Anda atau orang lain. Jika beberapa opsi dapat memenuhi kedua kondisi tersebut maka Anda bebas untuk memilih salah satu dari mereka, sesuai dengan keinginan Anda.
Dialecticus
2
Opsi yang layak untuk Anda adalah memiliki konfigurasi. Jadi dalam kode Anda, Anda tidak memeriksa negara tertentu, tetapi untuk opsi konfigurasi tertentu. Tetapi setiap negara akan memiliki serangkaian opsi konfigurasi tersebut. Misalnya bukannya if (country == "DEU")Anda periksa if (config.ShouldRemovePunctuation).
Dialecticus
11
Jika negara memiliki berbagai pilihan, mengapa countrysebuah string yang bukan sebuah instance dari kelas yang model orang-orang pilihan?
Damien_The_Unbeliever
@Damien_The_Unbeliever - dapatkah Anda menguraikan sedikit tentang ini? Apakah jawaban oleh Robert Brautigam di bawah ini sesuai dengan saran Anda? - ah dapat melihat jawaban Anda sekarang, terima kasih!
John Darvill

Jawaban:

53

Saya akan menyarankan merangkum semua opsi dalam satu kelas:

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

dan meneruskannya ke Processmetode:

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);
}
Michał Turczyn
sumber
4
Tidak yakin mengapa hal seperti ini tidak dicoba sebelum melompat ke CountrySpecificHandlerFactory... o_0
Mateen Ulhaq
Selama tidak ada opsi terlalu khusus, saya pasti akan pergi dengan cara ini. Jika opsi ini diserialisasi ke dalam file teks, ini juga memungkinkan non-programmer untuk menentukan varian baru / memperbarui yang sudah ada tanpa perlu mengubah aplikasi.
Tom
4
Itu public class ProcessOptionsseharusnya benar-benar [Flags] enum class ProcessOptions : int { ... }...
Drunken Code Monkey
Dan saya kira jika mereka membutuhkan, mereka dapat memiliki peta negara ProcessOptions. Sangat mudah.
theonlygusti
24

Ketika .NET framework ditetapkan untuk menangani masalah-masalah semacam ini, itu tidak memodelkan semuanya sebagai string. Jadi Anda memiliki, misalnya, CultureInfokelas :

Memberikan informasi tentang budaya tertentu (disebut lokal untuk pengembangan kode yang tidak dikelola). Informasi mencakup nama-nama untuk budaya, sistem penulisan, kalender yang digunakan, urutan pengurutan, dan pemformatan untuk tanggal dan angka.

Sekarang, kelas ini mungkin tidak mengandung fitur spesifik yang Anda butuhkan, tetapi Anda jelas dapat membuat sesuatu yang analog. Dan kemudian Anda mengubah Processmetode Anda :

public string Process(CountryInfo country, string text)

CountryInfoKelas Anda kemudian dapat memiliki abool RequiresCapitalization properti, dll, yang membantu Processmetode Anda mengarahkan pemrosesan dengan tepat.

Damien_The_Unbeliever
sumber
13

Mungkin Anda bisa memiliki satu Processorper negara?

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

Dan satu kelas dasar untuk menangani bagian umum dari pemrosesan:

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

Selain itu, Anda harus mengolah kembali tipe pengembalian Anda karena tidak akan dikompilasi saat Anda menulisnya - terkadang stringmetode tidak mengembalikan apa pun.

Corentin Pane
sumber
Saya kira saya mencoba untuk mengikuti komposisi di atas mantra warisan. Tapi ya itu jelas pilihan, terima kasih atas jawabannya.
John Darvill
Cukup adil. Saya pikir warisan dibenarkan dalam beberapa kasus tetapi itu benar-benar tergantung pada bagaimana Anda berencana untuk memuat / menyimpan / memanggil / mengubah metode dan pemrosesan Anda pada akhirnya.
Corentin Pane
3
Terkadang, warisan adalah alat yang tepat untuk pekerjaan itu. Jika Anda memiliki proses yang akan berperilaku sebagian besar dengan cara yang sama dalam beberapa situasi yang berbeda, tetapi juga memiliki beberapa bagian yang akan berperilaku berbeda dalam situasi yang berbeda, itu pertanda baik yang harus Anda pertimbangkan untuk menggunakan pewarisan.
Tanner Swett
5

Anda dapat membuat antarmuka umum dengan Processmetode ...

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

Maka Anda menerapkannya untuk setiap negara ...

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

Anda kemudian dapat membuat metode umum untuk membuat instance dan mengeksekusi masing-masing kelas terkait negara ...

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

Maka Anda hanya perlu membuat dan menggunakan prosesor seperti ...

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

Berikut ini contoh biola dotnet yang berfungsi ...

Anda menempatkan semua pemrosesan khusus negara di setiap kelas negara. Buat kelas umum (dalam kelas Pemrosesan) untuk semua metode individu yang sebenarnya, sehingga setiap prosesor negara menjadi daftar panggilan umum lainnya, daripada menyalin kode di setiap kelas negara.

Catatan: Anda harus menambahkan ...

using System.Assembly;

agar metode statis untuk membuat turunan dari kelas negara.

Pasang kembali Monica Cellio
sumber
Apakah refleksi sangat lambat dibandingkan dengan tidak ada kode tercermin? apakah itu layak untuk kasus ini?
jlvaquero
@ jlvaquero Tidak, refleksi sama sekali tidak lambat. Tentu saja ada kinerja yang melebihi menentukan jenis pada waktu desain, tetapi itu benar-benar perbedaan kinerja yang dapat diabaikan dan hanya terlihat ketika Anda menggunakannya secara berlebihan. Saya telah menerapkan sistem olahpesan besar yang dibangun di sekitar penanganan objek generik dan kami tidak punya alasan untuk mempertanyakan kinerja sama sekali, dan itu dengan jumlah throughput yang sangat besar. Tanpa perbedaan mencolok dalam kinerja saya akan selalu pergi dengan mudah untuk mempertahankan kode, seperti ini.
Pasang kembali Monica Cellio
Jika Anda merefleksikan, tidakkah Anda ingin menghapus string negara dari setiap panggilan Process, dan alih-alih menggunakannya sekali untuk mendapatkan IProcessor yang benar? Anda biasanya memproses banyak teks sesuai dengan aturan negara yang sama.
Davislor
@ Davidvis Itulah yang dilakukan oleh kode ini. Ketika Anda menyebutnya Process("GBR", "text");mengeksekusi metode statis yang membuat instance dari prosesor GBR dan mengeksekusi metode Proses itu. Itu hanya mengeksekusi pada satu contoh, untuk jenis negara tertentu.
Pasang kembali Monica Cellio
@Archer Benar, jadi dalam kasus umum di mana Anda memproses beberapa string sesuai dengan aturan untuk negara yang sama, akan lebih efisien untuk membuat instance satu kali — atau mencari instance konstan dalam tabel hash / Kamus dan mengembalikan referensi untuk itu. Anda kemudian dapat memanggil transformasi teks pada contoh yang sama. Membuat instance baru untuk setiap panggilan dan kemudian membuangnya, daripada menggunakannya kembali untuk setiap panggilan, adalah pemborosan.
Davislor
3

Beberapa versi yang lalu, C # swtich diberi dukungan penuh untuk pencocokan pola . Sehingga kasus "banyak negara cocok" mudah dilakukan. Meskipun masih tidak memiliki kemampuan jatuh, satu input dapat mencocokkan banyak kasus dengan pencocokan pola. Mungkin bisa membuat itu jika-spam sedikit lebih jelas.

Npw, sebuah sakelar biasanya dapat diganti dengan Koleksi. Anda harus menggunakan Delegasi dan Kamus. Proses dapat diganti dengan.

public delegate string ProcessDelegate(string text);

Maka Anda bisa membuat Kamus:

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

Saya menggunakan functionNames untuk menyerahkan Delegasi. Tapi Anda bisa menggunakan sintaks Lambda untuk menyediakan seluruh kode di sana. Dengan begitu, Anda bisa menyembunyikan seluruh Koleksi itu seperti halnya koleksi besar lainnya. Dan kode menjadi pencarian sederhana:

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

Itu adalah dua pilihan. Anda mungkin ingin mempertimbangkan untuk menggunakan Enumerasi alih-alih string untuk pencocokan, tetapi itu adalah detail kecil.

Christopher
sumber
2

Saya mungkin (tergantung pada detail kasus penggunaan Anda) pergi dengan Country menjadi objek "nyata", bukan string. Kata kuncinya adalah "polimorfisme".

Jadi pada dasarnya akan terlihat seperti ini:

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

Kemudian Anda dapat membuat negara khusus untuk yang Anda butuhkan. Catatan: Anda tidak harus membuat Countryobjek untuk semua negara, Anda bisa LatinlikeCountry, atau bahkan GenericCountry. Di sana Anda dapat mengumpulkan apa yang harus dilakukan, bahkan menggunakan kembali yang lain, seperti:

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

Atau serupa. Countrymungkin sebenarnya Language, saya tidak yakin tentang kasus penggunaan, tapi saya mengerti maksudnya.

Juga, metode tentu saja tidak harus Process()itu harus menjadi hal yang benar-benar perlu Anda lakukan. Suka Words()atau apalah.

Robert Bräutigam
sumber
1
Saya menulis sesuatu yang lebih kata-kata, tetapi saya pikir inilah yang paling saya sukai. Jika use case perlu mencari objek-objek ini berdasarkan string negara, itu dapat menggunakan solusi Christopher dengan ini. Implementasi antarmuka bahkan bisa menjadi kelas yang instansnya mengatur sifat-sifat seperti dalam jawaban Michal, untuk mengoptimalkan ruang daripada waktu.
Davislor
1

Anda ingin mendelegasikan (mengangguk ke rantai tanggung jawab) sesuatu yang tahu tentang budaya sendiri. Jadi gunakan atau buat tipe Negara atau CultureInfo, seperti yang disebutkan di atas dalam jawaban lain.

Tetapi secara umum dan mendasar masalah Anda adalah Anda mengambil konstruksi prosedural seperti 'prosesor' dan menerapkannya pada OO. OO adalah tentang mewakili konsep dunia nyata dari bisnis atau domain masalah dalam perangkat lunak. Prosesor tidak menerjemahkan apa pun di dunia nyata selain dari perangkat lunak itu sendiri. Setiap kali Anda memiliki kelas seperti Prosesor atau Manajer atau Gubernur, bel alarm akan berbunyi.

jujur
sumber
0

Saya bertanya-tanya apakah ada pola di luar sana yang akan membantu dengan jenis proses ini

Rantai tanggung jawab adalah jenis hal yang mungkin Anda cari tetapi dalam OOP agak rumit ...

Bagaimana dengan pendekatan yang lebih fungsional dengan 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;
      }
    }
  }
}

CATATAN: Tentu saja tidak harus statis. Jika kelas Proses memerlukan status Anda dapat menggunakan kelas instances atau fungsi yang diterapkan sebagian;).

Anda dapat membangun Proses untuk setiap negara pada permulaan, menyimpan masing-masing dalam koleksi yang diindeks dan mengambilnya saat dibutuhkan dengan biaya O (1).

jlvaquero
sumber
0

Saya minta maaf karena saya telah lama menciptakan istilah "objek" untuk topik ini karena itu membuat banyak orang untuk fokus pada ide yang lebih rendah. Ide besarnya adalah olahpesan .

~ Alan Kay, On Messaging

Saya hanya akan menerapkan rutinitas Capitalise, RemovePunctuationdll. Sebagai subproses yang dapat mengirim pesan dengan a textdan countryparameter, dan akan mengembalikan teks yang diproses.

Gunakan kamus untuk mengelompokkan negara yang sesuai dengan atribut tertentu (jika Anda lebih suka daftar, itu akan bekerja dengan baik hanya dengan sedikit biaya kinerja). Misalnya: CapitalisationApplicableCountriesdan 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.
}
Igwe Kalu
sumber
0

Saya merasa bahwa informasi tentang negara harus disimpan dalam data, bukan dalam kode. Jadi, alih-alih kelas CountryInfo atau CapitalisationApplicableCountries dictionary, Anda bisa memiliki database dengan catatan untuk setiap negara dan bidang untuk setiap langkah pemrosesan, dan kemudian pemrosesan bisa melalui bidang untuk negara tertentu dan memproses sesuai. Pemeliharaan kemudian terutama dalam database, dengan kode baru hanya diperlukan ketika langkah-langkah baru diperlukan, dan data dapat dibaca manusia dalam database. Ini mengasumsikan langkah-langkahnya independen dan tidak saling mengganggu; jika tidak demikian maka segala sesuatunya rumit.

Steve J
sumber