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 istanzaIOpaqueHandle
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.
-
Uso di una sottoclasse con un upcast:
class Resource : IOpaqueHandle { internal static Resource getResource(IOpaqueHandle handle) { return (Resource)handle; } }
-
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);
}
}
}