Come modellare questa relazione in modo che sia valida per costruzione?

4

Immagina una classe device che rappresenta un dispositivo mobile fisico reale, con campi come Enabled , Platform , Model IMEI , ecc. Quindi, una classe operation , che è qualcosa che deve essere eseguito su un determinato dispositivo, ad esempio DeviceEnablementOperation o LockOperation . Questa è una relazione "un dispositivo su più operazioni" e un'operazione non può esistere senza essere correlata a un dispositivo esistente. Posso modellarlo in modo tale che il sistema non consenta determinati tipi di operazioni ai dispositivi con determinate piattaforme? Ad esempio, un dispositivo Android non consentirebbe un DeviceEnablementOperation collegato ad esso, mentre un dispositivo Windows Phone sarebbe.

La nostra modellazione attuale è iniziata considerando solo i dispositivi Android. All'epoca, abbiamo creato il concetto di operazioni come fonte di eventi di un uomo povero, in modo da poter tenere traccia di ciò che è stato fatto contro un determinato dispositivo tramite la nostra API. In seguito, abbiamo iniziato a supportare anche i dispositivi iOS e abbiamo introdotto il campo Platform utilizzando una classe di enumerazione per differenziarli, in questo modo:

public enum PlatformType : short
{
    /// <summary>
    /// Device is an Android.
    /// </summary>
    Android = 0,

    /// <summary>
    /// Device is an Windows platform.
    /// </summary>
    Windows = 1,

    //// Can not place the name iOS.

    /// <summary>
    /// Device is an iOS platform.
    /// </summary>
    Ios = 2,
}

In quel momento, i requisiti venivano soddisfatti correttamente, ma ora è necessario limitare determinati tipi di operazioni a determinate piattaforme. Il modello corrente non lo limita automaticamente, quindi l'implementazione corrente considera un elenco hardcoded di operazioni supportate per ogni piattaforma e lo usa per rifiutare la creazione di alcune operazioni su determinati dispositivi.

Quello che mi piacerebbe fare è cambiare la modellazione in modo che questa restrizione venga applicata in modo più efficace. Ho pensato di utilizzare l'ereditarietà e di creare AndroidDevice , IosDevice e WindowsPhoneDevice classi, ma non riesco ancora a vedere come potrei limitare la relazione con le operazioni stesse per coprire questo requisito. Un dispositivo ha operazioni, ma non tutti i dispositivi supportano tutte le operazioni.

La modellazione per le operazioni stesse è un'eredità molto semplice, come questa:

public abstract class Operation
{
    protected Operation()
    {
        this.Logs = new List<OperationLog>();
    }

    public DateTime? CreationDate { get; set; }

    public virtual Device Device { get; set; }

    public int DeviceId { get; set; }

    public OperationStatus GeneralStatus { get; set; }

    public int Id { get; set; }

    public DateTime? LastUpdate { get; set; }

    public ICollection<OperationLog> Logs { get; internal set; }

    public DateTime? ReceiveDate { get; set; }
}

E poi i tipi di operazione derivati, come quelli che ho menzionato:

public sealed class LockOperation : Operation
{
    /// <summary>
    /// Gets or sets a value indicating whether the device must be locked or not.
    /// </summary>
    public bool Lock { get; set; }

    /// <summary>
    /// Gets or sets the device's lock password.
    /// </summary>
    public string Password { get; set; }
}

e

public sealed class EnablementOperation : Operation
{
    /// <summary>
    /// Gets or sets a value indicating whether the device should be enabled or disabled.
    /// </summary>
    public bool Enabled { get; set; }
}

È possibile modellarlo per riflettere meglio il nuovo requisito che voglio? In qualche modo avrei bisogno di un modo per dire se una determinata operazione è supportata per un determinato dispositivo e consentire solo la relazione di esistere se l'operazione è supportata.

Suppongo di poter provare qualcosa utilizzando i generici e l'ereditarietà sia sul dispositivo che sulle classi operative, ad esempio:

public abstract class Device<TOperation>
{
    public ICollection<TOperation> Operations { get; set; }
    ...
}

public sealed class AndroidDevice : Device<AndroidOperation> {...}

public abstract class Operation {...}

public abstract class AndroidOperation {...}

public sealed LockOperation : AndroidOperation {...}

Ma questo particolare approccio avrebbe bisogno di più eredità per funzionare, e dal momento che sto usando C # non è possibile. Potrei quindi provare a utilizzare le interfacce per differenziare le operazioni, ma ritengo sospetto che sarebbe un problema quando si effettua il mapping su un database in un secondo momento utilizzando un ORM.

Inoltre non riesco davvero a rompere tutto nelle proprie classi completamente isolate, perché ho bisogno del concetto generale di "dispositivi che hanno operazioni", indipendentemente dalla piattaforma o dal tipo di operazione. Ecco perché le classi di base esistono nel nostro modello attuale.

    
posta julealgon 05.05.2015 - 20:59
fonte

3 risposte

2

Se ti preoccupi della persistenza degli ORM, ecco una possibile soluzione:

Ogni operazione dichiara la sua piattaforma supportata, simile all'approccio all'interfaccia, ma usa invece la proprietà flags (si noti che cambio l'enum in flag e aggiungo un'opzione per tipo sconosciuto)

[Flags]
public enum PlatformType : short
{
    Unknown = 0,
    /// <summary>
    /// Device is an Android.
    /// </summary>
    Android = 1,

    /// <summary>
    /// Device is an Windows platform.
    /// </summary>
    Windows = 2,

    //// Can not place the name iOS.

    /// <summary>
    /// Device is an iOS platform.
    /// </summary>
    Ios = 4
}

public abstract class Operation
{
    public string Name { get; set; }
    public abstract PlatformType SupportedPlatforms { get; }

    //Other properties
}

public sealed class DeviceEnablementOperation : Operation
{
    public override PlatformType SupportedPlatforms
    {
        get
        {
            return PlatformType.Windows | PlatformType.Ios;
        }
    }

    //Other properties
}

La classe di dispositivi astratti conterrà un elenco di operazioni e assicurerà di accettare solo le operazioni supportate (nel metodo constructor e AddOperation ()). Genera immediatamente un'eccezione quando viene violato in modo che lo sviluppatore possa riconoscere rapidamente:

public abstract class Device
{
    public PlatformType Platform { get; private set; }
    private ICollection<Operation> _operations;

    protected Device(PlatformType platform)
    {
        Platform = platform;
        _operations = new List<Operation>();
    }

    protected Device(PlatformType platform, ICollection<Operation> operations)
    {
        Platform = platform;

        var violatedOperation = operations.FirstOrDefault(o => !OperationSupportedByThisDevice(o));
        if(violatedOperation != null)
        {
            throw new ApplicationException(string.Format("Operation {0} not supported by platform {1}", violatedOperation.Name, Platform));
        }

        _operations = operations;
    }

    public void AddOperation(Operation operation)
    {
        if(!OperationSupportedByThisDevice(operation))
        {
            throw new ApplicationException(string.Format("Operation {0} not supported by platform {1}", operation.Name, Platform));
        }

        _operations.Add(operation);
    }

    private bool OperationSupportedByThisDevice(Operation operation)
    {
        return operation.SupportedPlatforms.HasFlag(Platform);
    }
}

public class AndroidDevice : Device
{
    public AndroidDevice() : base(PlatformType.Android)
    {
    }

    public AndroidDevice(ICollection<Operation> operations) : base(PlatformType.Android, operations)
    {
    }
}

Quindi, se un dispositivo Android viene creato in questo modo, genera un'eccezione poiché DeviceEnablementOperation non è supportato dal dispositivo Android:

var androidDevice = new AndroidDevice(new List<Operation> { new DeviceEnablementOperation() });

Se ti preoccupi dell'utilizzo dei flag per PlatformType per dispositivo (.i.e. un dispositivo può essere erroneamente assegnato con più piattaforme), quindi crea un enumeratore separato per operazione, ma ti viene l'idea

    
risposta data 06.05.2015 - 05:15
fonte
1

Che ne dici di aggiungere interfacce per operazioni specifiche della piattaforma? vale a dire.

public interface IAndroidOperation : IOperation {}
public interface IIoSOperation : IOperation {}

Quindi le operazioni implementano le interfacce per cui sono

public class LockOperation : IAndroidOperation, IIoSOperation {}

Quindi il dispositivo consente solo l'aggiunta dell'interfaccia corretta

public class AndroidDevice : Device
{
    public List<IAndroidOperation> Operations {get;,set;}
}

Aggiornamento:

hai ragione l'esposizione delle operazioni generiche non è banale. Ci sono un certo numero di approcci. Ne ho incluso uno semplice sotto, che non è forse l'ideale. ma evita di dover creare il tuo IEnumberable

using System.Linq;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject2
{
    public interface IOperation 
    {
        void DoStuff();
    }
    public interface IAndroidOperation : IOperation { }
    public interface IoSOperation : IOperation { }

    public class LockOperation : IAndroidOperation
    {
        public void DoStuff(){}
    }

    public class BendOperation : IoSOperation
    {
        public void DoStuff() { }
    }
    public interface IDevice
    {
        IEnumerable<IOperation> GetOperations();
    }
    public abstract class Device<T> : IDevice where T : IOperation
    {
        public abstract List<T> Operations { get; set; }

        public IEnumerable<IOperation> GetOperations()
        {
            return this.Operations.Select(o => (IOperation)o);
        }
    }

    public class IoSDevice : Device<IoSOperation>
    {
        public override List<IoSOperation> Operations { get; set; }
    }

    public class AndroidDevice : Device<IAndroidOperation>
    {
        public override List<IAndroidOperation> Operations { get; set; }
    }

    public class TestMe
    {
        [TestMethod]
        public void test()
        {
            List<IDevice> devices = new List<IDevice>();
            AndroidDevice a = new AndroidDevice();
            IoSDevice i = new IoSDevice();

            devices.Add(a);
            devices.Add(i);

            LockOperation l = new LockOperation();
            BendOperation b = new BendOperation();

            a.Operations = new List<IAndroidOperation>();
            i.Operations = new List<IoSOperation>();

            a.Operations.Add(l); //ok
            //a.Operations.Add(b); //compilation error
            i.Operations.Add(b); // ok

            foreach (IDevice d in devices)
            {
                foreach (IOperation o in d.GetOperations())
                {
                    o.DoStuff();
                }
            }

        }
    }
}
    
risposta data 05.05.2015 - 22:33
fonte
1

Penso che le interfacce sotto forma di pattern wrapper siano la strada da percorrere.

Se hai un'interfaccia IPlatform e IAndroid , IOS (gioco di parole) ecc. che la estendono, puoi scrivere operazioni come oggetti wrapper seguendo il modello wrapepr.

Disclaimer: this is pseudo code, it's been too long since I did C#, you get the idea

Ecco un'operazione che funziona su tutti i dispositivi.

class BreakDisplayOperation implements IPlatform {

BreakDisplayOperation (IPlatform platform) { // this is the  constructor

Uno che funziona solo su IWindows come nel tuo.

class DeviceEnablementOperation implements IWindows {

DeviceEnablementOperation (IWindows platform) { // constructor

L'idea alla base di questo è delegare tutte le chiamate di metodo dell'interfaccia all'oggetto ricevuto nel costruttore, ad eccezione di quelle che si desidera manipolare tramite l'operazione.

IWindows (o IPlatform ) probabilmente include un metodo getter per abilitare

bool isEnabled(); // or do this via get keyword, which C# has, if I remember correctly.

DeviceEnablementOperation sovrascrive questo metodo e restituisce un valore che si definisce, invece di ricevere quel valore delegando la chiamata all'oggetto IWindows spostato, in questo modo

class DeviceEnablementOperation implementa IWindows {

bool isEnabled()
{
    return true;
}

}

di nuovo, le altre cose che non sono manipolate dall'operazione sono solo delegate all'oggetto wrapepd (assumendo un membro con lo stesso nome del parametro costruttore:

class DeviceEnablementOperation implements IWindows {

    bool isEnabled() // modified
    {
        return true;
    }

    IMEI getIMEI() // delegated
    {
        return platform.getIMEI()
    }
}

Negli esempi precedenti, dovresti eseguire la delega per tutti Operation s ancora e ancora. Per evitare questo boilerplate, crea delle super classi che eseguono tutte le deleghe, quindi ogni specifico Operation può sovrascrivere i metodi se necessario.

Tutto funziona perché l'ereditarietà dell'interfaccia si dirama, rendendo le singole piattaforme incompatibili perché sono fratelli nella gerarchia.

So che questo riassume il problema in un modo diverso mentre lo stai facendo al momento. È un po 'come Platform, Device e Operation si fondono tutti nella stessa cosa.

Puoi applicare molte operazioni avvolgendo i wrapper.

Per ottenere un elenco di tutte le operazioni, aggiungi il metodo getOperations(); .

La super classe di tutti Platform s restituirà un nuovo oggetto elenco vuoto. La super classe di tutti Operation s delegherà nuovamente tale chiamata all'oggetto spostato (che alla fine sarà un Platform ), si aggiungerà all'elenco e restituirà l'elenco. (per possibili altri wrapper per farsi pubblicità)

I problemi di questo approccio:

  • qualcuno potrebbe implementare entrambe le interfacce per unirle, rompere la separazione
  • un wrapper IPlatform sarà di quel tipo, per usare di nuovo uno specifico wrapper, è necessario un cast. È possibile aggirare il problema duplicando i wrapper generici specificatamente digitati per ogni piattaforma. Non sono sicuro che i generici potrebbero aiutarti con questo, ma ho la sensazione che lo facciano.
risposta data 07.05.2015 - 17:20
fonte

Leggi altre domande sui tag