OO design: separazione delle preoccupazioni

0

Ho una collezione di classi che modellano le espressioni logiche (booleane).

Esiste una classe astratta base LogicalExpression e classi derivate: UnaryLogicalExpression , BinaryLogicalExpression , LogicalParameter , LogicalValue .

Alcuni esempi di codice

public abstract class LogicalExpression
{
    public string[] parameterNames;
    public abstract Boolean Evaluate(bool[] values);
}

public class BinaryLogicalExpression : LogicalExpression
{
    public LogicalExpression LeftSide { get; set; }
    public LogicalExpression RightSide { get; set; }
    public LogicalOperator Operator { get; set; }

    public override bool Evaluate(bool[] values)
    {
        bool leftSideResult = LeftSide.Evaluate(values);
        bool rightSideResult = RightSide.Evaluate(values);
        switch (Operator)
        {
            case LogicalOperator.And:
                {
                    return leftSideResult && rightSideResult;
                }
            case LogicalOperator.Or:
                {
                    return leftSideResult || rightSideResult;
                }
            case LogicalOperator.Xor:
                {
                    return leftSideResult ^ rightSideResult;
                }
            default:
                {
                    throw new InvalidOperationException("Operator not valid!");
                }
        }
    }
    public BinaryLogicalExpression(LogicalOperator Operator, LogicalExpression LeftSide, LogicalExpression RightSide)
    {
        this.Operator = Operator;
        this.LeftSide = LeftSide;
        this.RightSide = RightSide;
    }
}

public class LogicalParameter : LogicalExpression
{
    public int Index { get; set; }
    public string Name { get; set; }

    public override bool Evaluate(bool[] values)
    {
        try
        {
            return values[Index];
        }
        catch (IndexOutOfRangeException ex)
        {
            throw new IndexOutOfRangeException("The array of values must match the parameter list length!");
        }
    }
    public LogicalParameter(int Index, string Name)
    {
        this.Name = Name;
        this.Index = Index;
    }
    public override string ToString()
    {
        return Name;
    }
}

Il problema / dillema qui è la classe LogicalParameter. Al momento è molto semplice, rappresenta una variabile booleana atomica, ma ho bisogno che faccia di più.

Voglio avere un'espressione logica che può avere parametri più complessi (non solo variabili booleane atomiche).

Ad esempio, ho un altro segmento di classe che implementa un'interfaccia ISegment. Qualsiasi tipo di ISegment può controllare se un profilo è associato a quel segmento.

public interface ISegment
{
    bool CheckFor(Guid ProfileId);
}

Ora voglio mettere insieme questi due. Voglio avere un'espressione logica che può avere segmenti invece di variabili bool atomiche e quindi voglio valutarla fornendo un profiloId.

Qual è il modo migliore per farlo?

Potrei cambiare la classe LogicalExpression , sovrascrivere Evaluate() in modo che prenda un profileId invece di una matrice di valori e poi faccia ereditare le mie classi Segment da LogicalParameter. Ma questo è semplicemente orribile, non c'è separazione di preoccupazioni.

La mia classe LogicalExpression non ha bisogno di conoscere i segmenti giusto? Quindi, come dovrei affrontare questo? Ho la sensazione che sia qualcosa di molto semplice che mi manca.

    
posta Adrian 26.02.2016 - 11:07
fonte

2 risposte

1

Ci sono un paio di concetti di implementazione linguistica che potrebbero aiutarti qui. Il primo è il concetto di un ambiente . Attualmente, la tua gerarchia di espressioni tiene traccia anche dei nomi delle variabili, ecc. Ci sono aspetti in cui ciò potrebbe essere utile, ma l'ambiente in genere viene estratto in un oggetto diverso. Questo ambiente mappa i nomi delle variabili in valori. Quando valutiamo un'espressione, l'oggetto ambiente viene passato come argomento alla funzione di valutazione. Ora, i nodi di espressione non hanno bisogno di tenere traccia di un array di nomi di parametri e non devono utilizzare indici soggetti a errori piuttosto che nomi (gli indici sono migliori se si compila l'albero delle espressioni con qualche codice byte, ma sono altrimenti difficile da mantenere corretto se chi crea un LogicalParameter deve tenere traccia degli indici usati).

Quando viene creato un nuovo ambiente, vogliamo assicurarci che contenga tutte le variabili richieste. Si controlla che durante la valutazione if e l'indice siano fuori intervallo, ma possiamo fare meglio: un metodo FreeVariables che restituisce le variabili in un'espressione. Questa è una matrice di un singolo elemento per un LogicalParameter e la concatenazione delle variabili libere del lato sinistro con le variabili libere del lato destro per un'espressione binaria. Dato un elenco di variabili libere, il costruttore di un oggetto ambiente può far rispettare tutte le variabili fornite. Nota che gli oggetti espressione devono essere immutabili in modo che l'elenco di variabili libere rimanga sempre corretto.

Successivamente, stai tentando di fornire variabili di tipi diversi: bool e ProfileId . Abbiamo quindi bisogno di un tipo di sistema di tipi nel nostro valutatore di espressioni e nel nostro ambiente. Esistono due approcci: tipizzazione statica e dinamica. Con la digitazione dinamica, potremmo creare i nostri tipi che rappresentano valori nel sistema, ad es. un'interfaccia IValue implementata da MyBool e MyProfileId ecc. in modo che l'ambiente possa contenere solo una mappatura di nomi su IValue oggetti (in C # o in altre lingue con una gerarchia di tipi unificata anche questo può essere fatto utilizzando object come tipo root). Il metodo Evaluate restituisce anche IValue oggetti anziché bool . Ogni operazione che utilizza IValue deve prima eseguire il cast del valore del tipo di calcestruzzo previsto o emettere un errore di tipo runtime. Questo è abbastanza semplice da implementare, ma è possibile ottenere più sicurezza.

Se vogliamo la digitazione statica, notiamo che alcune espressioni potrebbero restituire bool e altre potrebbero restituire ProfileId . Questo può essere modellato tramite generici. Ora avremmo:

interface Expression<T> {
  T Evaluate(Environment);
}

class BinaryExpression : Expression<bool> {
  BinaryExpression(Operator op, Expression<bool> left, Expression<bool> right) { … }
  override bool Evaluate(Environment) { … }
}

class Variable<T> : Expression<T> {
  Variable(string name) { … }
  override T Evaluate(Environment) { … }
}

class SegmentExpression : Expression<bool> {
  SegmentExpression(ISegment segment, Expression<ProfileId> value) { … }
  override bool Evaluate(Environment env) {
    return Segment.CheckFor(Value.Evaluate(env));
  }
}

Quindi in sostanza, usiamo i generici per fare il back-up sul sistema dei tipi della lingua ospite. Funziona bene finché il sistema di tipi è sufficientemente espressivo. Environment potrebbe ancora dover memorizzare i valori come tipo dinamico, ma possiamo verificare che tutti i tipi corrispondano alla prima della valutazione. Tutti i cast di runtime sono quindi garantiti sicuri. Sebbene questo sia implementato come digitazione dinamica, questo fornisce in modo efficace la digitazione statica alla tua lingua (ma lo stesso è vero per qualsiasi altra lingua poiché i set di istruzioni del processore tipicamente non sono tipizzati).

    
risposta data 26.02.2016 - 12:34
fonte
1

Personalmente ritengo che manchi un livello. Dovresti (forse) avere un servizio / modulo, prendendo l'elenco delle variabili ISegment , elaborare l'elenco in questa classe di servizio e passare il risultato elaborato a un'implementazione LogicalEpxression (prendendo ancora le variabili bool come parametro).

Sembra che tu stia programmando in C #, se questo è il caso, un'altra possibilità sta trasformando LogicalExpression in una classe generica e con implementazioni specializzate, ma personalmente non penso che risolverà qualcosa, ma condurrò a accoppiamento ancora più stretto.

A proposito di separazione delle preoccupazioni, lo scenario switch/case nella tua BinaryLogicalExpression dovrebbe essere effettivamente refactored aggiungendo un metodo Evaluate alla tua interfaccia LogicalOperator , prendendo due parametri LogicalExpression (lato sinistro e destro) e dovresti fornire implementazioni per fare le giuste operazioni.

    
risposta data 26.02.2016 - 12:19
fonte

Leggi altre domande sui tag