Controllo della sanità per il modello di progettazione utilizzato con un modello di calcolo complesso

1

Sto lavorando a un progetto che genera brochure tecniche in batch. L'API di terze parti in uso prevede POCO con nomi di proprietà corrispondenti ai nomi dei campi utilizzati in ciascuno dei modelli di brochure. Il compito che sto cercando di dare consigli è con l'origine dati che verrà utilizzata per popolare questi POCO. I dati si basano su calcoli a cascata rispetto a un modello di dominio. Inizialmente avevo intrattenuto l'idea che il dominio si potesse auto-calcolare, ma ci sono così tanti calcoli necessari che mi è sembrato ovvio che questo doveva essere sottratto.

Ho creato un codice ipotetico che imita da vicino il modello di progettazione in questione, e sono aperto a qualsiasi suggerimento o riaffermazione sul fatto che mi stia avvicinando correttamente a questo.

Le mie preoccupazioni principali:

  1. Il costruttore per la classe "AirplaneResultsContext", che si inietta a classi di calcolo che sono anche proprietà esposte pubblicamente. Questo crea percorsi ricorsivi?

  2. La possibile occorrenza di riferimenti circolari con chiamate di calcolo a cascata a varie proprietà / metodi in varie altre classi. Si noti che sto riutilizzando la maggior parte della logica di calcolo (come richiesto), ma posso proporre la necessità che venga refactored, se necessario.

Ho notato che ho usato la parola "Intricato", che di solito è un avvertimento rivelatore per il codice che deve essere estratto, ma sto facendo fatica a vedere come posso renderlo migliore.

L'entità che viene calcolata rispetto a . Immaginalo come una radice aggregata per un modello di dominio più grande.

class Airplane : Entity<int>
{
    public double WingLength { get; set; }
    public double FuselageCircumference { get; set; }
    public double FuselageLength { get; set; }
    public double SeatWidth { get; set; }
    public double SeatDepth { get; set; }
    public double SeatHeight { get; set; }
    public double IsleWidth { get; set; }
    public double LegRoomArea { get; set; }
}

Il contesto di calcolo . L'intento è che questo oggetto venga passato come parte di un processo batch: una fabbrica astratta che consuma un'interfaccia rappresentata da questa classe di contesto e genera gli oggetti POCO. Nota che ho omesso anche le interfacce e le classi astratte che sarebbero state utilizzate per diverse strategie di calcolo per brevità.

class AirplaneResultsContext
{
    public AirplaneResultsContext(Airplane airplane)
    {
        Airplane = airplane;
        Fuselage = new FuselageCalculator(this);
        Seating = new SeatingCalculator(this);
    }

    public Airplane Airplane { get; private set; }
    public FuselageCalculator Fuselage { get; private set; }
    public SeatingCalculator Seating { get; private set; }

    public double ComputeWidth()
    {
        return Airplane.WingLength*2 + Fuselage.ComputeDiameter();
    }
}

Calcolatrici . Nel mio progetto non ipotetico, questi metodi sono privati / protetti, memorizzati nella prima chiamata e riletti da proprietà di sola lettura, ma volevo solo mostrare una rappresentazione di base di alcune delle complessità di calcolo.

class SeatingCalculator
{
    private readonly AirplaneResultsContext _context;

    public SeatingCalculator(AirplaneResultsContext context)
    {
        _context = context;
    }

    public int ComputeNumberOfSeats()
    {
        double isleArea = _context.Airplane.IsleWidth* _context.Airplane.FuselageLength;
        double availableArea = (_context.Fuselage.ComputeDiameter()*_context.ComputeWidth()) - isleArea;
        double seatArea = _context.Airplane.SeatDepth * _context.Airplane.SeatWidth + _context.Airplane.LegRoomArea;

        return (int) Math.Floor(availableArea/seatArea);
    }
}
class FuselageCalculator
{
    private readonly AirplaneResultsContext _context;

    public FuselageCalculator(AirplaneResultsContext context)
    {
        _context = context;
    }

    public double ComputeDiameter()
    {
        return Math.PI / _context.Airplane.FuselageCircumference;
    }
}
    
posta bnice7 16.07.2014 - 00:44
fonte

2 risposte

1

Sembra che il tuo algoritmo di calcolo consista di quanto segue:

  1. Una grande quantità di dati non elaborati.
  2. Un gruppo di classi (sigillate o sigillabili) che calcolano vari risultati, che sono singoletti nel contesto di ciascun calcolo specifico. Per esempio. c'è un FuselageCalculator, un SeatingCalculator e così via, per ogni brochure da generare.

In questo caso, puoi combinare lo schema di fabbrica astratto con un semplice algoritmo di colorazione del grafico in un gestore di calcolo che catturerà qualsiasi riferimento circolare. Finché ogni fase di calcolo esegue tutti i suoi calcoli nel suo costruttore e richiede incondizionatamente tutti i risultati precedenti, idealmente all'inizio del costruttore , il gestore può tenere traccia dei calcoli che vengono costruiti e generano un'eccezione ogni volta che qualcuno tenta di nidificare la creazione dello stesso tipo di calcolo. Quindi ogni fase di calcolo diventa un nodo grafico implicito con tre stati possibili: non calcolato, calcolato e fatto il calcolo.

Ecco una rapida implementazione del prototipo:

public interface IRawData
{
}

public interface IResult<TRawData> where TRawData : class, IRawData 
{
}

public interface IResultFactory<TRawData> where TRawData : class, IRawData 
{
    Type ResultType { get; }
    IResult<TRawData> CreateResult(CalculationManager<TRawData> manager, TRawData rawData);
}

public class CalculationManagerException : Exception
{
}

public class CircularReferenceException : CalculationManagerException
{
    readonly Type type;
    public CircularReferenceException(Type type)
        : base()
    {
        this.type = type;
    }

    public override string Message
    {
        get
        {
            return GetType().Name + ": Circular reference calculating " + type.Name;
        }
    }
}

public class UnknownResultTypeException : CalculationManagerException
{
}

public class CalculationManager<TRawData> where TRawData : class, IRawData 
{
    class DummyResult : IResult<TRawData>
    {
        internal DummyResult() { }
    }

    readonly TRawData rawData;

    readonly Dictionary<Type, IResult<TRawData>> results = new Dictionary<Type, IResult<TRawData>>();
    readonly Dictionary<Type, IResultFactory<TRawData>> resultFactory;
    readonly DummyResult calculatingFlag = new DummyResult();

    public CalculationManager(TRawData rawData, IEnumerable<IResultFactory<TRawData>> factories)
    {
        this.resultFactory = factories.ToDictionary(x => x.ResultType, x => x );
        this.rawData = rawData;
    }

    public TResult DemandResult<TResult>() where TResult : class, IResult<TRawData>
    {
        IResult<TRawData> iResult;
        if (!results.TryGetValue(typeof(TResult), out iResult))
        {
            IResultFactory<TRawData> factory;
            if (!resultFactory.TryGetValue(typeof(TResult), out factory))
                throw new UnknownResultTypeException();
            Debug.Assert(typeof(TResult).IsSealed);
            results[typeof(TResult)] = calculatingFlag; // Synthetic flag that we are in the middle of calculating results.
            iResult = factory.CreateResult(this, rawData);
            if (iResult.GetType() != typeof(TResult))
            {
                Debug.Assert(iResult.GetType() == typeof(TResult));
                throw new UnknownResultTypeException();
            }
            results[typeof(TResult)] = iResult; 
        }
        else
        {
            if (iResult == calculatingFlag)
                throw new CircularReferenceException(typeof(TResult));
        }
        return (TResult)iResult;
    }
}

(Forse c'è un modo più pulito per fare il modello astratto di fabbrica in c # 4.5 usando la covarianza? Per vari motivi sono bloccato al c # 3.0. Forse Activator.CreateInstance sarebbe più carino, saltando del tutto le classi factory?)

Ora proviamo. Ecco alcune classi senza riferimento circolare. Result2 dipende da Result1 che dipende da TestData :

public sealed class TestData : IRawData
{
    public double Field0 { get { return 0; } }
}

public sealed class Result1 : IResult<TestData>
{
    readonly double field1;
    public Result1(CalculationManager<TestData> manager, TestData rawData)
    {
        field1 = rawData.Field0 + 1;
    }

    public double Field1 { get { return field1; } }
}

public sealed class Result1Factory : IResultFactory<TestData>
{
    #region IResultFactory<FirstResult> Members

    public Type ResultType { get { return typeof(Result1); } }

    public IResult<TestData> CreateResult(CalculationManager<TestData> manager, TestData rawData)
    {
        return new Result1(manager, rawData);
    }

    #endregion
}

public sealed class Result2 : IResult<TestData>
{
    readonly double field2;
    public Result2(CalculationManager<TestData> manager, TestData rawData)
    {
        var res1 = manager.DemandResult<Result1>();
        field2 = res1.Field1 + 1;
    }

    public double Field2 { get { return field2; } }
}

public sealed class Result2Factory : IResultFactory<TestData>
{
    #region IResultFactory<FirstResult> Members

    public Type ResultType { get { return typeof(Result2); } }

    public IResult<TestData> CreateResult(CalculationManager<TestData> manager, TestData rawData)
    {
        return new Result2(manager, rawData);
    }

    #endregion
}

Ora aggiungiamo alcune classi con riferimenti circolari (omettendo le fabbriche noiose):

public sealed class Result3 : IResult<TestData>
{
    readonly double field3;
    public Result3(CalculationManager<TestData> manager, TestData rawData)
    {
        var res1 = manager.DemandResult<Result1>();
        var res4 = manager.DemandResult<Result4>();

        field3 = res1.Field1 + res4.Field4;
    }

    public double Field3 { get { return field3; } }
}

public sealed class Result4 : IResult<TestData>
{
    readonly double field4;
    public Result4(CalculationManager<TestData> manager, TestData rawData)
    {
        var res1 = manager.DemandResult<Result1>();
        var res2 = manager.DemandResult<Result2>();
        var res3 = manager.DemandResult<Result3>();

        if (res1.Field1 > 2)
            field4 = res2.Field2 + res3.Field3;
        else
            field4 = res2.Field2;
    }

    public double Field4 { get { return field4; } }
}

E ora proviamo:

public static class TestResult {
    public static void Test()
    {
        try
        {
            TestData data = new TestData();
            CalculationManager<TestData> manager = new CalculationManager<TestData>(data, new IResultFactory<TestData>[] { new Result1Factory(), new Result2Factory(), new Result3Factory(), new Result4Factory() });

            var field2 = manager.DemandResult<Result2>().Field2; // Get two as expected.
            var field4 = manager.DemandResult<Result4>().Field4; // No result -- exception thrown.
        }
        catch (CalculationManagerException e)
        {
            Debug.WriteLine(e.GetType().Name + " " + e.ToString()); // Exception caught: CircularReferenceException: Circular reference calculating Result4
        }
    }
}

Sicuramente, Result2 viene calcolato correttamente (e, indirettamente, Result1), ma il calcolo di Result4 genera l'eccezione.

È quello che volevi?

Aggiornamento - Ho aggiunto il vincolo di stile di codifica che i risultati precedenti dovrebbero essere richiesti all'inizio di ogni costruttore e aggiornato il mio caso di test per mostrare questo stile.

A proposito, la progettazione potrebbe essere vulnerabile a un altro tipo di errore: errori di disallineamento dell'unità. Ho notato che alcuni dei tuoi risultati sono lineari, alcuni sono aree e forse alcuni sono privi di dimensione. È del tutto possibile che da qualche parte nella catena di calcolo si formi un'ipotesi errata sulle unità di un calcolo precedente. Suggerirei di avvolgere ogni risultato numerico in una sorta di struttura unitaria.

    
risposta data 17.07.2014 - 08:40
fonte
1
  1. Può. Se esporti la tua calcolatrice per essere chiamata da altri calcolatori è possibile annidarli con un design scadente.
  2. Questa sembra essere la stessa domanda del # 1

La mia raccomandazione per il tuo progetto non è quella di passare il contesto nei tuoi calcolatori. Qualsiasi calcolo che dipende da più calcoli secondari dovrebbe essere proprietà del contesto. Invece basta passare in aereo alla calcolatrice.

Il mio stile personale sarebbe quello di non chiamare i calcolatori dei calcolatori, ma trattarli come oggetti di dominio (Fusoliera, Cabina, Ala ecc.) ed esporre proprietà, non metodi (Diametro, non ComputeDiametro). Le proprietà espongono i calcoli, mentre i metodi eseguono operazioni (calcolando e restituendo valori o modificando lo stato)

E FYI c'è uno scambio di stack per la revisione del codice @ Vorrei raccomandare di passare a Code Review ( link )

    
risposta data 16.07.2014 - 02:50
fonte