Motivo di progettazione per la classificazione di un set di dati

0

Sto lavorando su un codice legacy che riguarda gli ordini effettuati dai clienti. C'è un nuovo requisito per classificare ogni ordine per Business Unit. Il nome dell'unità di business verrà archiviato in una nuova colonna nella tabella dell'ordine.

L'ordine non è un oggetto reale. Ho un numero d'ordine, che posso usare per recuperare due diversi documenti XML da un database. Bene , un documento XML che rappresenta l'intero ordine e un documento XML che rappresenta una definizione del prodotto, che viene recuperato utilizzando un ID prodotto dal documento XML dell'ordine. I documenti XML del prodotto sono tutti conformi allo stesso schema.

Ho un set di regole da applicare sui valori ottenuti dai documenti XML per determinare la classificazione della Business Unit.

Sto cercando di evitare di usare molte istruzioni if / else perché sembra disordinato e non molto mantenibile. Tutto ciò di cui ho bisogno è un string contenente il nome dell'unità di business. Il metodo deve essere chiamato in due flussi separati, quindi l'ho messo in una classe.

Ecco come appare il codice per restituire il nome dell'unità di business:

public class BusinessUnitName
{

    public static string GetBusinessUnitName(string orderNumber)
    {
        var order = getOrder(orderNumber);
        var orderType = order.SelectSingleNode("/Order/Type").InnerText;

        if (orderType.Equals("Remake"))
        {
            var remakeReason = order.SelectSingleNode("/Order/RemakeReason").InnerText;
            if (remakeReason.Equals("SHIPDMG") || remakeReason.Equals("WRNGITM"))
            {
                return "Shipping";
            }
            else
            {
                return "Customer Service";
            }
        }
        else if (orderType.Equals("Design"))
        {
            List<string> illustrationProductTypes = new List<string> { "1204", "1205", "1206", "1207" };
            List<string> customProductTypes = new List<string> { "9204", "9205", "9206", "9207" };

            var firstProductId = order.SelectSingleNode("/Order/Products/Product[1]/Id").InnerText;
            var firstProductDefinition = getProductDefinition(firstProductId);
            var firstProductType = firstProductDefinition.SelectSingleNode("/Product/Type").InnerText;

            if (illustrationProductTypes.Contains(firstProductType))
            {
                return "Design-Illustration";
            }
            else if (customProductTypes.Contains(firstProductType))
            {
                return "Design-Custom";
            }
            else
            {
                return "Design-Other";
            }                
        }
        else if (orderType.Equals("Samples"))
        {
            var firstProductId = order.SelectSingleNode("/Order/Products/Product[1]/Id").InnerText;
            var firstProductDefinition = getProductDefinition(firstProductId);
            var departmentCode = firstProductDefinition.SelectSingleNode("/Product/DepartmentCode").InnerText;
            if (departmentCode.Equals("JWL"))
            {
                return "Jewelry";
            }
            else if (departmentCode.Equals("SPPLS"))
            {
                return "Craft Supplies";
            }
        }
    }

    private static XmlDocument getOrder(string orderNumber)
    {
        XmlDocument order = new XmlDocument();
        /* execute code to retrieve XML from database 
           and load into order */
        return order;
    }

    private static XmlDocument getProductDefinition(string productId)
    {
        XmlDocument productDefinition = new XmlDocument();
        /* execute code to retrieve XML from database 
           and load into productDefinitionXmlDocument */
        return productDefinition;
    }
}

Questo è abbastanza simile al codice attuale, ma l'elenco di istruzioni if / else è circa tre volte più lungo. Ogni regola consiste semplicemente in una combinazione di valori nei documenti XML.

Non so se sono sulla strada giusta, ma stavo considerando l'utilizzo del modello di strategia. Potrei creare un'interfaccia IBusinessUnitStrategy con un metodo ExecuteRules che restituisce un valore bool che indica se una combinazione di regole è una corrispondenza e un campo che indica il nome dell'unità di business. Dovrei quindi implementare l'IBusinessUnitStrategy per ciascun nome di unità aziendale.

Avrei bisogno di un'altra classe che generi un elenco di istanze di IBusinessUnitStrategy per ogni classe di Business Unit concreta, quindi chiama il metodo ExecuteRules in sequenza su ogni classe di Business Unit finché uno di questi restituisce true , quindi restituisce il valore del campo Nome unità di business per quella classe.

Questo approccio avrebbe senso / essere un po 'più lungo in termini di design, o è eccessivo?

    
posta Poosh 28.08.2018 - 02:06
fonte

1 risposta

2

Come altri hanno suggerito nei commenti, un modo per andare, se possibile, è modificare il modello di dati in modo che faciliti l'elaborazione, ma potresti non essere in grado di farlo.

Quindi proporrò un modo per affrontare questa complessità. In primo luogo, crea un metodo di supporto che trasforma il tuo ordine (un XmlDocument) in una struttura dati adeguata: raccogli le proprietà che ti servono.

// Think of this as being a data bag
public class OrderData   // not a great name, I know 
{
    public int OrderNumber { get; set; }
    public string OrderType { get; set; }
    public string RemakeReason { get; set; }

    // you may want to include properties from a related table,  
    // and possibly populate these in advance
    public string FirstProductType { get; set; }  

    //...
}

In pratica, prendi tutto il codice che usi già per estrarre questi valori e spostalo su quel metodo di supporto, ma invece di avere un gruppo di variabili locali, popolare le proprietà di un'istanza di OrderData.

private OrderData getOrderData(XmlDocument order) { /* ... */ }

Ora, l'approccio descritto di seguito non è particolarmente complicato, ma incorpora idee da un paio di schemi, quindi abbi pazienza con me.

Quello che vuoi è un metodo che accetta un OrderData e restituisce una stringa di classificazione (Business Unit Name), ma vuoi che sia flessibile e mantenibile. L'idea alla base del mio suggerimento è che, insieme a OrderData, si passa un oggetto (o solo un delegato) che rappresenta una collezione di regole di classificazione (mi riferirò a questi come "classificatori" d'ora in poi). Quindi, in un certo senso, è come il modello della strategia. Ma in un altro senso, si comporta anche come il pattern Composite, perché avresti una collezione di classificatori che agirà da classificatore - avrà la stessa interfaccia (o la stessa firma, se è un delegato) come ciascuno dei suoi bambini. E poi avresti un certo numero di classificatori individuali. Ad esempio, potresti averne uno che riconosce "Spedizione", un altro che riconosce "Servizio clienti" e un altro che riconosce "Disegno-Illustrazione", e così via. (Nota che puoi raggruppare alcuni di questi sotto lo stesso classificatore, se ritieni che farlo abbia senso, ad esempio puoi pensare a un classificatore che funziona a livello del Tipo di Ordine, ma puoi farlo in modo più granulare se vuoi.)

Quindi devi semplicemente passare l'oggetto OrderData al classificatore composito, che a sua volta lo passerà ai suoi figli, uno per uno, fino a quando c'è una corrispondenza, o fino a quando non viene trovata alcuna corrispondenza. Quindi, a tal proposito, si comporta in qualche modo come il pattern Chain of Responsibility, ma ha più struttura e un'interfaccia più specifica perché è davvero un Composite (almeno, è così che l'ho concettualizzato qui).

Quindi l'idea principale è isolare la logica di classificazione in questi classificatori separati e separare le responsabilità di eseguire la classificazione effettiva (classificatori), gestire il flusso generale (classificatore composito) e decidere quali regole di classificazione usare (il codice che chiama getCompositeClassifier).

Userò un esempio basato sui delegati per semplicità, ma se si vogliono usare oggetti pieni, e si ritiene che non sia eccessivo, o che i pro superino gli aspetti negativi, provaci.

public string GetBusinessUnitName(string orderNumber)
{
    // NOTE: "classifier" here is used in the sense of "classification rule".
    // Create the composite classifier from the available individual
    // classifiers, in a way that meets your requirements. 
    // Note that you can easily add or remove individual classifiers 
    // without worrying (too much) how they are implemented
    var compositeClassifier = getCompositeClassifier(new List<Func<OrderData, string>> {
        classifyRemakeOrder, 
        classifyDesignOrder, 
        classifySamplesOrder
        });

    var orderData = getOrderData(getOrder(orderNumber));
    return compositeClassifier(orderData);
}

// This "constructs" the composite classifier;
// this code probably won't have to change as much or as often (or at all).
// Also, you don't want to call this redundantly; create it once use that instance.
// This and the individual classifier methods could be in the same file, 
// or someplace else in your codebase, depending on how your code is organized 
// (this will also determine if these methods should be public, internal, or private).
private Func<OrderData, string> getCompositeClassifier(
    IEnumerable<Func<OrderData, string>>  children)
{
    Func<OrderData, string> compositeClassifier = (orderData) => {
        foreach(var classifier in children) 
        {
            var buName = classifier(orderData);
            if (buName != null)
                return buName;
        }

        // I'm using null to indicate that classification was not possible within the context of this classifier
        // You can use some other convention
        return null;
    };

    // Note that this method returns a delegate
    return compositeClassifier;
}

// Then for each new classifier, just add a new method with the appropriate signature
private string classifyRemakeOrder(OrderData orderData)
{
     if (orderData.RemakeReason.Equals("SHIPDMG") || orderData.RemakeReason.Equals("WRNGITM"))
        {
            return "Shipping";
        }
        else
        {
            return "Customer Service";
        }

        // I'm using null to indicate that classification was not possible within the context of this classifier
        // You can use some other convention
        return null;  
}

private string classifyDesignOrder(OrderData orderData) 
{ /* ... */ }

private string classifySamplesOrder(OrderData orderData) 
{ /* ... */ }

A seconda di come esattamente si implementano i classificatori e sulla complessità della logica coinvolta, si può avere una certa ridondanza nei controlli, ma ciò può essere perfettamente accettabile - ad es. potrebbe non influire sulle prestazioni o sulla manutenibilità in alcun modo significativo.

Un'altra cosa che puoi fare, una volta che hai questo tipo di design, è fare leva su LINQ (se devi elaborare una serie di ordini contemporaneamente). Ottieni tutte le rappresentazioni XML di tutti gli ordini necessari in una volta sola e usa orderCollection.Select(getOrderData).Select(compositeClassifier) per trasformare l'intera cosa in una raccolta di nomi di unità aziendali.
Se sono necessari sia i dati dell'ordine che il nome dell'unità aziendale associata, è possibile utilizzare una classe anonima per mantenerli entrambi nell'intera query LINQ o sfruttare la classe OrderData stessa - aggiungere la proprietà BusinessUnitName e modificare i classificatori in modo da impostarli proprietà.

Per quanto riguarda il fatto che a volte è necessario recuperare dati aggiuntivi, come le definizioni del prodotto, provare a ottenere tutti i dati in una volta sola, in anticipo, potrebbe funzionare bene. Puoi andare su di esso in diversi modi - puoi recuperare tutti i dati aggiuntivi indipendentemente dal fatto che siano effettivamente necessari, o avere una logica più complessa che lo faccia in un modo più intelligente (ma non andare per il secondo a meno che non ci sia un buon ragione per questo, fintanto che funziona bene, di solito è più semplice).
Ci possono anche essere cose che puoi fare dal lato del database; potresti essere in grado di sfruttare il database per fare il duro lavoro di unire i dati insieme, quindi guarda anche quello.

    
risposta data 28.08.2018 - 15:33
fonte

Leggi altre domande sui tag