I'm writing a library to be used by several applications.
Some interfaces which my library declares and uses, and which are implemented by the application, look something like this:
interface IOpaqueHandle : IDisposable {}
interface IPaintFactory
{
IOpaqueHandle Create(string configData);
}
interface IPainter
{
void Paint(IOpaqueHandle handle);
}
The Create
method with configData
parameter tells the application to allocate some resources and return a handle to those resources:
My library doesn't know what types of resource are allocated by the application, nor exactly what methods those resources support.
The resources probably come from various 3rd-party application-specific library, known to the application but not known by my library.
Create
will be called multiple times with different parameter values, to allocate different resources each with their ownIOpaqueHandle
instance
The Paint
method is an example of telling the application to do something with a specified resource.
My question is, what's the best way to implement the handle-to-resource mapping in the application?
For example, assuming that the application's resources are defined by a class named Resources
, I can think of two ways to implement it.
Using a subclass with an upcast:
class Resource : IOpaqueHandle { internal static Resource getResource(IOpaqueHandle handle) { return (Resource)handle; } }
Using a dictionary to map from the handle to the object:
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) { ... } }
Is the subclass with an upcast simpler (to implement) and faster (at run-time) and therefore preferable?
Does the second solution have any advantages? It avoids an upcast, for what's that worth -- is that good?
(First edit)
You might suggest declaring Paint
as a method of the handle interface --
BUT that's not possible because of different lifetimes:
- An
IOpaqueHandle
is a long-lived object, e.g. created once at object startup - An
IPainter
is a short-lived object, e.g. a new one is created each time the O/S window needs repainting.
A short-lived IPainter
instance is created by the application and passed-in to my library, which calls its Paint
method. The implementation of IPainter
contains some short lived resources. The Paint
method must combine the short-lived resources (contained in the IPainter) with the long-lived resources (contained in the handle).
Theoretically I could declare a method like this, instead of putting the Paint
method in the IPainter
interface:
interface IOpaqueHandle
{
// paint using short-lived resources contained in IPainter
void Paint(IPainter painter);
}
... but that amounts to the same problem, i.e. how to extract implementation-specific resources from the opaque IPainter
interface.
(Second edit)
At the risk of making this question too long, here's some sample code.
The following code is in the library, in addition to the interfaces declared at the top.
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); });
}
}
The above is a simplification. Because Node
and MyLibrary
are actually hundreds of classes, it wouldn't be convenient to wrap them all in a single generic class so that they share a common template parameter of type T
.
The following code is in the application.
All the types in the Os
namespace belong to some library
which the application is using and which my library doesn't know about.
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);
}
}
}
Paint
method was supplied as a method of the returned object, for example. Even if the object you return doesn't include the methods that use it, it can just delegate to them, at which point you have a useful object rather than a handle.IPainter
defines a lot of other methods as well, which don't depend on those allocated resource parameters ... so I put all those painting methods in one interface, instead of putting some of them in the resource-specific interface.