Come implementare un handle opaco in C #

0

Sto scrivendo una libreria da utilizzare con diverse applicazioni.

Alcune interfacce che la mia libreria dichiara e utilizza e che sono implementate dall'applicazione, assomigliano a questo:

interface IOpaqueHandle : IDisposable {}

interface IPaintFactory
{
    IOpaqueHandle Create(string configData);
}

interface IPainter
{
    void Paint(IOpaqueHandle handle);
}

Il metodo Create con il parametro configData indica all'applicazione di allocare alcune risorse e restituire un handle a tali risorse:

  • La mia libreria non sa quali tipi di risorse sono allocati dall'applicazione, né esattamente quali metodi supportano quelle risorse.

  • Le risorse probabilmente provengono da varie librerie specifiche di applicazioni di terze parti, note all'applicazione ma non conosciute dalla mia libreria.

  • Create verrà chiamato più volte con valori di parametro diversi, per allocare risorse diverse ciascuna con la propria istanza IOpaqueHandle

Il metodo Paint è un esempio di dire all'applicazione di fare qualcosa con una risorsa specificata.

La mia domanda è, qual è il modo migliore per implementare la mappatura da handle a risorsa nell'applicazione?

Ad esempio, supponendo che le risorse dell'applicazione siano definite da una classe denominata Resources , posso pensare a due modi per implementarla.

  1. Uso di una sottoclasse con un upcast:

    class Resource : IOpaqueHandle
    {
        internal static Resource getResource(IOpaqueHandle handle)
        {
            return (Resource)handle;
        }
    }
    
  2. Utilizzare un dizionario per mappare dall'handle all'oggetto:

    class Resource : IDisposable
    {
        internal static Resource getResource(IOpaqueHandle handle)
        {
            return s_dictionary[handle];
        }
    
        class Handle : IOpaqueHandle
        {
            public void Dispose()
            {
                Resource resource = getResource(this);
                resource.Dispose();
                s_dictionary.Remove(this);
            }
        }
    
        static Dictionary<IOpaqueHandle, Resource> s_dictionary =
            new Dictionary<IOpaqueHandle, Resource>();
    
        internal static IOpaqueHandle Create(string configData);
        {
            Handle handle = new Handle();
            Resource resource = new Resource(configData);
            s_dictionary.Add(handle, resource);
            return handle;
        }
    
        Resource(string configData) { ... }
    }
    

La sottoclasse è più semplice con upcast (da implementare) e più veloce (in fase di esecuzione) e quindi preferibile?

La seconda soluzione ha qualche vantaggio? Evita l'upcast, perché vale la pena? È così bello?

(prima modifica)

Potresti suggerire di dichiarare Paint come metodo dell'interfaccia dell'handle - MA questo non è possibile a causa di diverse vite:

  • Un IOpaqueHandle è un oggetto longevo, ad es. creato una volta all'avvio dell'oggetto
  • Un IPainter è un oggetto di breve durata, ad es. ne viene creato uno nuovo ogni volta che è necessario ridisegnare la finestra O / S.

Un'istanza di breve durata IPainter viene creata dall'applicazione e inoltrata alla mia libreria, che chiama il suo metodo Paint . L'implementazione di IPainter contiene alcune risorse di breve durata. Il metodo Paint deve combinare le risorse di breve durata (contenute in IPainter) con le risorse di lunga durata (contenute nell'handle).

In teoria potrei dichiarare un metodo come questo, invece di mettere il metodo Paint nell'interfaccia IPainter :

interface IOpaqueHandle
{
    // paint using short-lived resources contained in IPainter
    void Paint(IPainter painter);
}

... ma ciò equivale allo stesso problema, ad esempio come estrarre risorse specifiche dell'implementazione dall'interfaccia opaque IPainter .

(Seconda modifica)

A rischio di fare questa domanda troppo a lungo, ecco alcuni esempi di codice.

Il seguente codice si trova nella libreria, oltre alle interfacce dichiarate in alto.

class Node
{
    internal readonly IOpaqueHandle handle;

    // plus a lot of other data members
    // e.g. to determine whether this node is visible and should be painted

    internal Node(string configData, IPaintFactory factory)
    {
        // get the handle which we'll pass back to the application if we paint this node
        handle = factory.Create(configData);
    }

    internal bool IsVisible { get { ... } }
}

// facade
public class MyLibrary
{
    List<Node> nodes = new List<Node>();

    // initialize data using config data,
    // and using app-specific paint factory passed-in by application
    public void Initialize(List<string> configStrings, IPaintFactory factory)
    {
        configStrings.ForEach(configData => nodes.Add(new Node(configData, factory)));
    }

    // plus a lot of other methods to manipulate the Nodes

    // called from application when its window wants repainting
    public void OnPaint(IPainter painter)
    {
        nodes.ForEach(node => { if (node.IsVisible) painter.Paint(node.handle); });
    }
}

Quanto sopra è una semplificazione. Poiché Node e MyLibrary sono in realtà centinaia di classi, non sarebbe conveniente racchiuderli tutti in una singola classe generica in modo che condividano un parametro di modello comune di tipo T .

Il seguente codice è nell'applicazione. Tutti i tipi nello spazio dei nomi Os appartengono ad alcune librerie che l'applicazione sta utilizzando e di cui la mia biblioteca non sa nulla.

class Resource : IOpaqueHandle
{
    readonly Os.Font font;
    readonly string text;

    internal Resource(string configData) { ... }

    // app-specific method using app-specific types
    // which my library doesn't know about and which
    // therefore isn't declared in the library's IOpaqueHandle interface
    internal void GraphicalPaint(Os.Graphics graphics)
    {
        graphics.DrawText(this.font, this.text);
    }
}

class PaintFactory : IPaintFactory
{
    public IOpaqueHandle Create(string configData)
    {
        return new Resource(configData);
    }
}

class Painter : IPainter, IDisposable
{
    internal readonly Os.Graphics graphics;

    internal Painter(Os.Graphics graphics)
    {
        this.graphics = graphics;
    }

    public void Dispose() { graphics.Dispose(); }

    public void Paint(IOpaqueHandle handle)
    {
        // use magic in question to retrieve app-specific resource from opaque handle
        Resource resource = (Resource)handle;

        // invoke app-specific method to paint this resource
        resource.GraphicalPaint(this.graphics);
    }
}

class Main : Os.Window
{
    MyLibrary myLibrary = new MyLibrary();

    void initialize(List<string> configStrings)
    {
        PaintFactory paintFactory = new PaintFactory();
        myLibrary.Initialize(configStrings, paintFactory);
    }

    // plus other methods to poke the data in MyLibrary

    // event handler called from Os when Window needs repainting
    void onPaint(Os.Graphics graphics)
    {
        using (Painter painter = new Painter(graphics))
        {
            myLibrary.OnPaint(painter);
        }
    }
}
    
posta ChrisW 25.02.2018 - 12:17
fonte

3 risposte

1

La semplice risposta è che qualsiasi tipo di mappatura o up-casting è negativo in quanto rischia un errore di runtime quando potrebbe essere altrimenti impedito da classi strongmente tipizzate.

Modifica: OP incluso codice app.

La soluzione è Generics, che ti consente di specificare un tipo di cui la tua biblioteca non è a conoscenza.

IPaintFactory<T>
{
    IOpaqueHandle<T> Create(config);
}

IOpaqueHandle<T>
{
    T Resource {get;}
}

Ciò consente all'app di specificare il tipo di risorsa sotto il gestore. il pittore può quindi essere:

App.Painter : IPainter<App.MyResource>
{
    void Paint(IOpaqueHandle<App.MyResource> handle)
    {
        handle.Resource.MySecretPaintMethod(); 
    }
}

Nel tuo caso il tipo che devi usare ma la libreria non sa è OS.Graphics .

Perché la classe della libreria stessa ha il metodo che ha bisogno di questo tipo myLibrary.OnPaint La libreria dovrà essere un tipo generico. Il parametro di tipo Generico si propaga fino a dove viene specificata questa funzione.

Probabilmente potresti refactoring in modo che qualche sottoclasse di librerie abbia il metodo OnPaint, quindi potresti limitare i generici a quel sottotipo.

    
risposta data 25.02.2018 - 15:59
fonte
0

OK ha incluso la funzione myLibrary.Onpaint () critica, ma ecco come vorrei refactoring il tuo nuovo codice per evitare l'inutile casting, i generici anche se risolvono totalmente il problema e aggiungendo il fabbrica al gestore risorse.

Quindi questa sarà la soluzione 3.

Mi sento come se avessi intenzione di andare "oh non funzionerebbe perché .. aggiunge altro codice"

class Main : Os.Window
{
    MyLibrary myLibrary = new MyLibrary();
    PaintFactory paintFactory;
    void initialize(List<string> configStrings)
    {
        paintFactory = new PaintFactory();
        myLibrary.Initialize(configStrings, paintFactory);
    }

    // plus other methods to poke the data in MyLibrary

    // event handler called from Os when Window needs repainting
    void onPaint(Os.Graphics graphics)
    {
        foreach(var r in paintFactory.ResList)
        {
            r.graphics = graphics;
        }

        myLibrary.OnPaint();
    }
}

Modifica: Jeeze Ho perso i nodi. OK, devi solo cambiarlo e impostare gli Os.Graphics su ogni risorsa

public void OnPaint()
{
    nodes.ForEach(node => { if (node.IsVisible) node.handle.Paint(); });
}

class Resource : IOpaqueHandle
{
    readonly Os.Font font;
    readonly string text;

    internal Resource(string configData) { }

    public void Dispose()
    {
        throw new NotImplementedException();
    }

    // app-specific method using app-specific types
    // which my library doesn't know about and which
    // therefore isn't declared in the library's IOpaqueHandle interface
    internal void GraphicalPaint(Os.Graphics graphics)
    {
        graphics.DrawText(this.font, this.text);
    }

    public PaintFactory parent;
    public Os.Graphics graphics { get; set; }
    public void Paint()
    {
        GraphicalPaint(graphics);
    }
}

Un modo piuttosto brutto per evitare i farmaci generici

    
risposta data 25.02.2018 - 20:01
fonte
0

A volte puoi implementarlo (es. distribuire su due tipi di base per ottenere i tipi di foglia) usando " doppio invio ". Ma penso che in questo caso non puoi farlo, perché i tipi di base o le interfacce (di proprietà della libreria) non possono nominare i tipi di foglia implementati dall'applicazione.

In questo caso, se hai intenzione di usare il metodo Dictionary per ottenere il tipo Resource foglia, sarebbe meglio (più convenzionale) usare un tipo di valore come Dictionary , usando invece di un'interfaccia senza metodi come una maniglia opaca (cioè utilizzando un tipo di riferimento come chiave del dizionario).

  • Assegna un nome al valore resourceId o qualcosa del genere.
  • Il tipo del valore potrebbe essere int o Guid ; oppure, per la sicurezza del testo e per aiutare a rendere l'API auto-documentante, un tipo di valore personalizzato come struct ResourceId { int idValue; ... } .

interface IPaintFactory
{
    // create the resource and return a resource id
    int Create(string configData);
}

interface IPainter
{
    void Paint(int resourceId);
}

Non sono sicuro di quale sia l'argomento contro upcasting (invece di usare Dictionary ).

  • Sembra abbastanza sicuro - se l'applicazione è l'unico software nel processo che sta creando oggetti derivati da IOpaqueHandle ie Resource oggetti, allora sicuramente è sicuro assumere che ogni IOpaqueHandle istanza isa % istanza diResource.

  • E immagino che l'upcasting potrebbe essere microscopicamente più veloce di una ricerca nel dizionario.

  • E il Dictionary deve essere mantenuto dall'applicazione e la durata delle voci del dizionario deve essere gestita - con il metodo "handle", la libreria memorizza gli handle e può Dispose ogni handle quando non è più necessario, invece della libreria che memorizza gli ID (chiavi) e utilizza un'altra API personalizzata (diversa da Dispose ) per dire all'applicazione di cancellare la voce Dictionary e disporre del corrispondente Resource .

Alla fine, sebbene un dizionario con ID possa essere migliore anche se teoricamente è peggio (più lento e complicato) rispetto a upcasting da un handle - perché a quanto pare alcuni programmatori pensano che l'upcasting sia il diavolo, e presumiamo che se tu " stai facendo ciò che stai facendo male e ci deve essere qualche alternativa.

    
risposta data 26.02.2018 - 10:53
fonte

Leggi altre domande sui tag