Lập trình hướng đối tượng - cách tránh trùng lặp trong các quy trình hơi khác nhau tùy thuộc vào một biến


64

Một điều xuất hiện khá nhiều trong công việc hiện tại của tôi là có một quy trình tổng quát cần phải xảy ra, nhưng sau đó phần lẻ của quy trình đó cần xảy ra hơi khác nhau tùy thuộc vào giá trị của một biến nhất định, và tôi thì không khá chắc chắn những gì thanh lịch nhất để xử lý này.

Tôi sẽ sử dụng ví dụ mà chúng ta thường có, đang làm những việc hơi khác nhau tùy thuộc vào quốc gia chúng ta đang làm việc.

Vì vậy, tôi có một lớp học, hãy gọi nó là 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);
    }
}

Ngoại trừ việc chỉ một số trong những hành động đó cần phải xảy ra đối với một số quốc gia nhất định. Ví dụ, chỉ có 6 quốc gia yêu cầu bước viết hoa. Ký tự để phân chia có thể thay đổi tùy thuộc vào quốc gia. Thay thế dấu 'e'chỉ có thể được yêu cầu tùy thuộc vào quốc gia.

Rõ ràng bạn có thể giải quyết nó bằng cách làm một cái gì đó như thế này:

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

Nhưng khi bạn giao dịch với tất cả các quốc gia có thể trên thế giới, điều đó sẽ trở nên rất cồng kềnh. Và bất kể, các ifcâu lệnh làm cho logic khó đọc hơn (ít nhất, nếu bạn tưởng tượng ra một phương thức phức tạp hơn ví dụ), và độ phức tạp chu kỳ bắt đầu tăng lên khá nhanh.

Vì vậy, hiện tại tôi đang làm một cái gì đó như thế này:

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

Xử lý:

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

Mà tôi cũng không chắc là mình thích. Logic vẫn còn bị che khuất bởi tất cả các sáng tạo của nhà máy và bạn không thể chỉ nhìn vào phương thức ban đầu và xem điều gì sẽ xảy ra khi quá trình "GBR" được thực thi. Bạn cũng có kết thúc việc tạo nhiều lớp (trong ví dụ phức tạp hơn này) theo phong cách GbrPunctuationHandler, UsaPunctuationHandler, vv ... mà có nghĩa là bạn phải nhìn vào các lớp học khác nhau để tìm ra tất cả các hành động có thể có thể xảy ra trong quá trình chấm câu sự điều khiển. Rõ ràng tôi không muốn một lớp khổng lồ với một tỷ ifcâu lệnh, nhưng cũng có 20 lớp với logic hơi khác nhau cũng cảm thấy lộn xộn.

Về cơ bản, tôi nghĩ rằng tôi đã tham gia vào một loại nút OOP nào đó và hoàn toàn không biết cách tháo gỡ nó. Tôi đã tự hỏi nếu có một mô hình ngoài đó sẽ giúp với loại quy trình này?


Có vẻ như bạn có một PreProcesschức năng, có thể được triển khai khác nhau dựa trên một số quốc gia, DetermineSeparatorcó thể có ở đó cho tất cả các quốc gia đó và a PostProcess. Tất cả chúng đều có thể được protected virtual voidtriển khai mặc định và sau đó bạn có thể có cụ thể Processorstheo từng quốc gia
Icepickle

Công việc của bạn là tạo ra khung thời gian nhất định một cái gì đó hoạt động và có thể được duy trì trong tương lai gần, bởi bạn hoặc người khác. Nếu một số tùy chọn có thể đáp ứng cả hai điều kiện thì bạn có thể tự do lựa chọn bất kỳ điều kiện nào, theo sở thích của bạn.
Dialecticus

2
Một lựa chọn khả thi cho bạn là có một cấu hình. Vì vậy, trong mã của bạn, bạn không kiểm tra quốc gia cụ thể, nhưng để biết tùy chọn cấu hình cụ thể. Nhưng mỗi quốc gia sẽ có một bộ tùy chọn cấu hình cụ thể. Ví dụ thay vì if (country == "DEU")bạn kiểm tra if (config.ShouldRemovePunctuation).
Dialecticus

11
Nếu các quốc gia có nhiều tùy chọn khác nhau, tại sao countrymột chuỗi chứ không phải là một thể hiện của một lớp mô hình hóa các tùy chọn đó?
Damien_The_Unbeliever

@Damien_The_Unbeliever - bạn có thể nói rõ hơn một chút về điều này không? Câu trả lời của Robert Brautigam dưới đây có phải là gợi ý của bạn không? - ah có thể thấy câu trả lời của bạn bây giờ, cảm ơn!
John Darvill

Câu trả lời:


53

Tôi sẽ đề nghị gói gọn tất cả các tùy chọn trong một lớp:

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

và truyền nó vào Processphương thức:

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
Không chắc chắn tại sao những thứ như thế này không được thử trước khi nhảy tới CountrySpecificHandlerFactory... o_0
Mateen Ulhaq

Miễn là không có lựa chọn quá chuyên biệt, tôi chắc chắn sẽ đi theo con đường này. Nếu các tùy chọn được tuần tự hóa thành tệp văn bản, nó cũng cho phép những người không lập trình xác định các biến thể mới / cập nhật các biến thể hiện có mà không cần phải thay đổi thành ứng dụng.
Tom

4
Đó public class ProcessOptionsthực sự chỉ là [Flags] enum class ProcessOptions : int { ... }...
Drunken Code Monkey

Và tôi đoán nếu họ cần, họ có thể có một bản đồ các quốc gia ProcessOptions. Rât thuận tiện.
theonlygusti

24

Khi .NET framework được thiết lập để xử lý các loại vấn đề này, nó không mô hình hóa mọi thứ như string. Vì vậy, bạn có, ví dụ, CultureInfolớp :

Cung cấp thông tin về một nền văn hóa cụ thể (được gọi là miền địa phương để phát triển mã không được quản lý). Thông tin bao gồm tên của văn hóa, hệ thống chữ viết, lịch được sử dụng, thứ tự sắp xếp chuỗi và định dạng cho ngày và số.

Bây giờ, lớp này có thể không chứa các tính năng cụ thể mà bạn cần, nhưng rõ ràng bạn có thể tạo ra một cái gì đó tương tự. Và sau đó bạn thay đổi Processphương pháp của mình :

public string Process(CountryInfo country, string text)

CountryInfoLớp của bạn sau đó có thể có một thuộc bool RequiresCapitalizationtính, v.v., giúp Processphương thức của bạn điều khiển quá trình xử lý của nó một cách thích hợp.


13

Có lẽ bạn có thể có một Processorcho mỗi quốc gia?

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

Và một lớp cơ sở để xử lý các phần chung của quá trình xử lý:

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

Ngoài ra, bạn nên làm lại các kiểu trả về của mình vì nó sẽ không biên dịch như bạn đã viết chúng - đôi khi một stringphương thức không trả về bất cứ thứ gì.


Tôi đoán rằng tôi đã cố gắng theo dõi các thành phần trên thần chú thừa kế. Nhưng có, nó chắc chắn là một lựa chọn, cảm ơn vì đã trả lời.
John Darvill

Đủ công bằng. Tôi nghĩ rằng sự kế thừa là hợp lý trong một số trường hợp nhưng nó thực sự phụ thuộc vào cách bạn lên kế hoạch tải / lưu trữ / gọi / thay đổi phương thức của bạn và xử lý cuối cùng.
Corentin Pane

3
Đôi khi, thừa kế là công cụ phù hợp cho công việc. Nếu bạn có một quy trình sẽ hành xử gần như giống nhau trong một số tình huống khác nhau, nhưng cũng có một số phần sẽ hành xử khác nhau trong các tình huống khác nhau, đó là một dấu hiệu tốt bạn nên xem xét sử dụng thừa kế.
Tanner Swett

5

Bạn có thể tạo một giao diện chung với một Processphương thức ...

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

Sau đó, bạn thực hiện nó cho mỗi quốc gia ...

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

Sau đó, bạn có thể tạo một phương thức chung để khởi tạo và thực thi mỗi lớp liên quan đến quốc gia ...

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

Sau đó, bạn chỉ cần tạo và sử dụng các bộ xử lý như vậy ...

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

Đây là một ví dụ fiddle dotnet hoạt động ...

Bạn đặt tất cả các chế biến cụ thể theo quốc gia trong mỗi lớp quốc gia. Tạo một lớp chung (trong lớp Xử lý) cho tất cả các phương thức riêng lẻ thực tế, để mỗi bộ xử lý quốc gia trở thành một danh sách các cuộc gọi chung khác, thay vì sao chép mã trong mỗi lớp quốc gia.

Lưu ý: Bạn sẽ cần thêm ...

using System.Assembly;

để phương thức tĩnh tạo một thể hiện của lớp quốc gia.


Không phải là phản xạ chậm đáng kể so với không có mã phản ánh? nó có đáng cho trường hợp này không?
jlvaquero

@jlvaquero Không, phản xạ không chậm một cách đáng kể nào cả. Tất nhiên có một điểm nhấn về hiệu suất khi chỉ định một loại tại thời điểm thiết kế, nhưng nó thực sự là một sự khác biệt hiệu suất không đáng kể và chỉ đáng chú ý khi bạn lạm dụng nó. Tôi đã triển khai các hệ thống nhắn tin lớn được xây dựng xung quanh việc xử lý đối tượng chung chung và chúng tôi không có lý do gì để đặt câu hỏi về hiệu suất cả, và đó là lượng thông lượng khổng lồ. Không có sự khác biệt đáng chú ý trong hiệu suất, tôi sẽ luôn đi với mã đơn giản để duy trì, như thế này.
Phục hồi Monica Cellio

Nếu bạn đang phản ánh, bạn có muốn xóa chuỗi quốc gia khỏi mỗi cuộc gọi đến không Process, và thay vào đó hãy sử dụng nó một lần để có được Bộ xử lý IP chính xác? Thông thường, bạn sẽ xử lý nhiều văn bản theo các quy tắc của cùng một quốc gia.
Davislor

@Davislor Đó chính xác là những gì mã này làm. Khi bạn gọi Process("GBR", "text");nó sẽ thực thi phương thức tĩnh tạo một thể hiện của bộ xử lý GBR và thực thi phương thức Process trên đó. Nó chỉ thực hiện nó trên một thể hiện, cho loại quốc gia cụ thể đó.
Phục hồi Monica Cellio

@Archer Phải, vì vậy, trong trường hợp điển hình khi bạn xử lý nhiều chuỗi theo quy tắc cho cùng một quốc gia, sẽ hiệu quả hơn khi tạo cá thể một lần, hoặc tìm kiếm một thể hiện không đổi trong bảng băm / Từ điển và trả về một tài liệu tham khảo đến đó. Sau đó, bạn có thể gọi chuyển đổi văn bản trên cùng một ví dụ. Tạo một phiên bản mới cho mỗi cuộc gọi và sau đó loại bỏ nó, thay vì sử dụng lại nó cho mỗi cuộc gọi, thật lãng phí.
Davislor

3

Một vài phiên bản trước, C # swtich đã được hỗ trợ đầy đủ cho việc khớp mẫu . Vì vậy, trường hợp "nhiều quốc gia phù hợp" được thực hiện dễ dàng. Mặc dù nó vẫn không có khả năng thông qua, một đầu vào có thể khớp nhiều trường hợp với khớp mẫu. Nó có thể làm cho if-spam rõ ràng hơn một chút.

Npw một công tắc thường có thể được thay thế bằng Bộ sưu tập. Bạn cần sử dụng Đại biểu và Từ điển. Quá trình có thể được thay thế bằng.

public delegate string ProcessDelegate(string text);

Sau đó, bạn có thể làm một từ điển:

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

Tôi đã sử dụng hàmNames để trao cho Đại biểu. Nhưng bạn có thể sử dụng cú pháp Lambda để cung cấp toàn bộ mã ở đó. Bằng cách đó, bạn có thể ẩn toàn bộ Bộ sưu tập đó giống như bất kỳ bộ sưu tập lớn nào khác. Và mã trở thành một tra cứu đơn giản:

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

Đó là khá nhiều hai lựa chọn. Bạn có thể muốn xem xét sử dụng Số liệt kê thay vì chuỗi cho khớp, nhưng đó là một chi tiết nhỏ.


2

Tôi có lẽ (tùy thuộc vào chi tiết về trường hợp sử dụng của bạn) đi với việc Countrylà một đối tượng "thực" thay vì một chuỗi. Từ khóa là "đa hình".

Về cơ bản nó sẽ trông như thế này:

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

Sau đó, bạn có thể tạo các quốc gia chuyên biệt cho những nước bạn cần. Lưu ý: bạn không phải tạo Countryđối tượng cho tất cả các quốc gia, bạn có thể có LatinlikeCountryhoặc thậm chí GenericCountry. Ở đó bạn có thể thu thập những gì nên làm, thậm chí sử dụng lại những người khác, như:

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

Hoặc tương tự. Countrycó thể thực sự Language, tôi không chắc về trường hợp sử dụng, nhưng tôi hiểu rõ.

Ngoài ra, phương pháp tất nhiên không nên là Process()nó mà bạn thực sự cần phải làm. Thích Words()hay sao cũng được.


1
Tôi đã viết một cái gì đó dài dòng hơn, nhưng tôi nghĩ rằng về cơ bản đây là những gì tôi thích nhất. Nếu trường hợp sử dụng cần tra cứu các đối tượng này dựa trên chuỗi quốc gia, nó có thể sử dụng giải pháp của Christopher với điều này. Việc thực hiện các giao diện thậm chí có thể là một lớp có các thể hiện đặt các đặc điểm như trong câu trả lời của Michal, để tối ưu hóa cho không gian hơn là thời gian.
Davislor

1

Bạn muốn ủy thác cho (gật đầu với chuỗi trách nhiệm) một cái gì đó biết về văn hóa của chính nó. Vì vậy, sử dụng hoặc tạo cấu trúc kiểu Quốc gia hoặc Văn hóa, như đã đề cập ở trên trong các câu trả lời khác.

Nhưng nói chung và về cơ bản, vấn đề của bạn là bạn đang thực hiện các cấu trúc thủ tục như 'bộ xử lý' và áp dụng chúng cho OO. OO là về việc thể hiện các khái niệm trong thế giới thực từ một lĩnh vực kinh doanh hoặc vấn đề trong phần mềm. Bộ xử lý không dịch sang bất cứ thứ gì trong thế giới thực ngoài phần mềm. Bất cứ khi nào bạn có các lớp như Bộ xử lý hoặc Người quản lý hoặc Thống đốc, tiếng chuông báo thức sẽ vang lên.


0

Tôi đã tự hỏi nếu có một mô hình ngoài đó sẽ giúp với loại quy trình này

Chuỗi trách nhiệm là loại điều bạn có thể đang tìm kiếm nhưng trong OOP thì hơi cồng kềnh ...

Điều gì về một cách tiếp cận chức năng hơn với 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;
      }
    }
  }
}

LƯU Ý: Tất nhiên nó không phải là tĩnh. Nếu lớp Process cần trạng thái, bạn có thể sử dụng lớp được kích hoạt hoặc hàm được áp dụng một phần;).

Bạn có thể xây dựng Quy trình cho mỗi quốc gia khi khởi động, lưu trữ từng quốc gia trong bộ sưu tập được lập chỉ mục và truy xuất chúng khi cần với chi phí O (1).


0

Tôi xin lỗi vì từ lâu tôi đã đặt ra thuật ngữ đối tượng trực tuyến cho chủ đề này bởi vì nó khiến nhiều người tập trung vào ý tưởng ít hơn. Ý tưởng lớn là nhắn tin .

~ Alan Kay, trên tin nhắn

Tôi chỉ đơn giản là thực hiện các thường trình Capitalise, RemovePunctuationv.v. như các quy trình con có thể bị rối với a textcountrycác tham số, và sẽ trả về một văn bản được xử lý.

Sử dụng từ điển để nhóm các quốc gia phù hợp với một thuộc tính cụ thể (nếu bạn thích danh sách, điều đó sẽ hoạt động tốt chỉ với một chi phí hiệu suất nhỏ). Ví dụ: CapitalisationApplicableCountriesPunctuationRemovalApplicableCountries.

/// 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

Tôi cảm thấy rằng thông tin về các quốc gia nên được lưu giữ trong dữ liệu, không phải trong mã. Vì vậy, thay vì một lớp CountryInfo hoặc từ điển CapitalisationApplossibleCountries, bạn có thể có một cơ sở dữ liệu với một bản ghi cho từng quốc gia và một trường cho mỗi bước xử lý, sau đó quá trình xử lý có thể đi qua các trường cho một quốc gia nhất định và xử lý theo đó. Việc bảo trì sau đó chủ yếu là trong cơ sở dữ liệu, với mã mới chỉ cần thiết khi cần các bước mới và dữ liệu có thể được con người đọc được trong cơ sở dữ liệu. Điều này giả định các bước là độc lập và không can thiệp lẫn nhau; Nếu đó không phải là những thứ phức tạp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.