Es posible evitar un abatido?
-
20-08-2019 - |
Pregunta
Tengo algo de lógica, que define y utiliza algunos tipos definidos por el usuario, como estos:
class Word
{
System.Drawing.Font font; //a System type
string text;
}
class Canvass
{
System.Drawing.Graphics graphics; //another, related System type
... and other data members ...
//a method whose implementation combines the two System types
internal void draw(Word word, Point point)
{
//make the System API call
graphics.DrawString(word.text, word.font, Brushes.Block, point);
}
}
La lógica, después de hacer los cálculos con el tipo (por ejemplo,para localizar cada uno de los Word
ejemplo), de forma indirecta utiliza algunos System
Api, por ejemplo mediante la invocación de la Canvass.draw
método.
Me gustaría hacer esta lógica independiente de la System.Drawing
espacio de nombres:en su mayoría, con el fin de ayudar con la unidad de pruebas (creo que las pruebas de unidad salida sería más fácil de comprobar si el draw
método de dibujo para algo que no sea real System.Drawing.Graphics
instancia).
Para eliminar la lógica de la dependencia en la System.Drawing
espacio de nombres, yo pensaba que iba a declarar algunos de los nuevos interfaces para actuar como marcadores de posición para el System.Drawing
tipos, por ejemplo:
interface IMyFont
{
}
interface IMyGraphics
{
void drawString(string text, IMyFont font, Point point);
}
class Word
{
IMyFont font; //no longer depends on System.Drawing.Font
string text;
}
class Canvass
{
IMyGraphics graphics; //no longer depends on System.Drawing.Graphics
... and other data ...
internal void draw(Word word, Point point)
{
//use interface method instead of making a direct System API call
graphics.drawText(word.text, word.font, point);
}
}
Si yo hice esto, entonces los diferentes asambleas podrían tener diferentes implementaciones de la IMyFont
y IMyGraphics
la interfaz, por ejemplo ...
class MyFont : IMyFont
{
System.Drawing.Font theFont;
}
class MyGraphics : IMyGraphics
{
System.Drawing.Graphics theGraphics;
public void drawString(string text, IMyFont font, Point point)
{
//!!! downcast !!!
System.Drawing.Font theFont = ((MyFont)font).theFont;
//make the System API call
theGraphics.DrawString(word.text, theFont, Brushes.Block, point);
}
}
...sin embargo, la aplicación tendría un abatido como se ilustra arriba.
Mi pregunta es, es allí una manera de hacer esto sin necesidad de un abatido en la aplicación? Por "esto", me refiero a "la definición de tipos definidos por el usuario como Word
y Canvass
que no dependen de hormigón específicos System
tipos"?
Una alternativa sería abstracto tipos definidos por el usuario ...
class Word
{
//System.Drawing.Font font; //declared in a subclass of Word
string text;
}
class Canvass
{
//System.Drawing.Graphics graphics; //declared in a subclass of Canvass
//concrete draw method is defined in a subclass of Canvass
internal abstract void draw(Word word, Point point);
}
...pero esto también tendría un abatido en la aplicación de la subclase.
También he pensado en usar el doble de envío modismo, pero depende de la denominación de las diversas subclases en la Api.
O, si no con interfaces o subclases, hay alguna manera de usar los delegados?
--Edit:--
Ha habido dos respuestas posibles.
Una respuesta es el uso de medicamentos genéricos, precisamente, como sugiere el Señor 'Utiliza' respuesta a continuación, y como se sugiere por el blog para que Juan Skeet vinculados.Sospecho que esto iba a funcionar bien en la mayoría de los escenarios.El lado negativo desde mi punto de vista es que significa la introducción de TFont
como un parámetro de plantilla:no es sólo una clase de Word
(que contiene un Font
instancia), que debe convertirse en una clase genérica (como WordT<TFont>
) ...también que cualquier clase que contiene un WordT<TFont>
(por ejemplo, Paragraph
ahora también debe ser genérico con un TFont
parámetro (por ejemplo, ParagraphT<TFont>
).Finalmente, casi todos los de la clase en la asamblea se ha convertido en una clase genérica.Este ¿ preservar la seguridad de tipos y evitar la necesidad de abatido ... pero es un poco feo, y perturba la ilusión de encapsulación (la ilusión de que 'Fuente' es un opaco detalle de implementación).
Otra respuesta es el uso de un mapa o un diccionario en la clase de usuario.En lugar de Font
en la biblioteca reutilizable, y en lugar de una interfaz abstracta, definir una "manija" de la clase como:
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
Entonces, en lugar de downcasting de FontHandle
, mantener un Dictionary<int, Font>
instancia que asigna FontHandle
los valores de Font
las instancias.
Solución
Primero, me pregunto si todo el escenario no es un poco artificial; ¿realmente vas a necesitar este nivel de abstracción? Quizás suscribirse a YAGNI ?
¿Por qué su MyGraphics
solo funciona con un MyFont
? ¿Puede funcionar con un IFont
? Sería un mejor uso de las interfaces y evitaría todo este problema ...
Una opción podría ser un poco rediseñado, de modo que Point
solo describa los metadatos para la fuente (tamaño, fuente, etc.), y tenga cosas en concreto <=> como:
[public|internal] MyFont GetFont(IFont font) {...} // or just Font
y se convierte en el trabajo de los gráficos hacer la traducción, por lo que usó algo como:
public void drawString(string text, IMyFont font, Point point)
{
using(System.Drawing.Font theFont = GetFont(font))
{
theGraphics.DrawString(word.text, theFont, Brushes.Block, point);
}
// etc
}
Por supuesto, <=> también podría necesitar traducción ;-p
Otros consejos
Que efectivamente está diciendo: "yo sé mejor que el compilador - sé que está destinado a ser una instancia de MyFont
." En ese punto tienes MyFont
y MyGraphics
está estrechamente unida de nuevo, lo que reduce el punto de la interfaz un poco.
Debe MyGraphics
trabajar con cualquier IFont
, o sólo una MyFont
?Si usted puede hacer que funcione con cualquier IFont
vas a estar bien.De lo contrario, puede que tenga que buscar en el complicado genéricos a hacer todo tipo en tiempo de compilación seguro.Usted puede encontrar a mi post sobre los medicamentos genéricos en los Búferes de Protocolo útil como una situación similar.
(Lado de la sugestión, el código será más idiomáticamente .NET-como si usted sigue las convenciones de nomenclatura, que incluye Pascal caso de métodos).
Actualmente no estoy al tanto de C #, ya ha pasado algún tiempo. Pero si no desea lanzar todas sus cosas allí, es posible que se vea obligado a usar genéricos.
Solo puedo proporcionar código Java, pero C # debería poder hacer lo mismo a través de la palabra clave where
.
Convierta su interfaz en una interfaz genérica. En Java eso sería
IMyGraphics<T extends IMyFont>
y luego MyGraphics : IMyGraphics<MyFont>
Luego redefina la Firma drawString
para tomar T font
como el segundo parámetro en lugar de IMyFont
. Esto debería permitirle escribir
public void drawString(string text, MyFont font, Point point)
directamente en su MyGraphics
clase.
En C # que public interface IMyGraphics<T> where T:IMyFont
debería ser <=>, pero no estoy 100% seguro de eso.
¿No te gusta el reparto de IFont
a MyFont
? Puedes hacer esto:
interface IFont {
object Font { get; }
}
class MyFont : IFont {
object Font { get { return ...; } }
}
Seguro que todavía necesita convertir de System.Object
a System.Drawing.Font
en el método de dibujo, pero acaba de eliminar la dependencia de una implementación de clase particular (<=>).
public void DrawString(string text, IFont font, Point point)
{
System.Drawing.Font f = (Font)font.Font;
graphics.DrawString(text, f, Brushes.Block, point);
}