É possível evitar uma abatido?
-
20-08-2019 - |
Pergunta
Eu tenho alguma lógica, que define e usa alguns tipos definidos pelo usuário, como estes:
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);
}
}
A lógica, depois de fazer cálculos com os tipos (por exemplo, para localizar cada instância Word
), indiretamente utiliza algumas APIs System
, por exemplo, chamando o método Canvass.draw
.
Eu gostaria de fazer este independente lógica do namespace System.Drawing
: na maior parte, a fim de ajudar com os testes de unidade (acho saída testes de unidade seria mais fácil para verificar se o método draw
estavam chegando para algo diferente de um verdadeiro instância System.Drawing.Graphics
).
Para eliminar a dependência da lógica no namespace System.Drawing
, eu pensei que eu iria declarar algumas novas interfaces para agir como espaços reservados para os tipos System.Drawing
, por exemplo:
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);
}
}
Se eu fizesse isso, então conjuntos diferentes pode ter diferentes implementações da interface IMyFont
e IMyGraphics
, por exemplo ...
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);
}
}
... no entanto a implementação seria necessário um abatido como ilustrado acima.
A minha pergunta é, há uma maneira de fazer isso sem precisar de um abatido na implementação? por "isto", quero dizer "definir UDTs como Word
e Canvass
que não dependem específica tipos System
concretas "?
Uma alternativa seria abstrato UDTs ...
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);
}
... mas isso também iria precisar de um abatido na implementação da subclasse.
Eu também pensei em usar a linguagem dupla expedição, mas isso depende de nomear as várias subclasses nas APIs.
Ou, se não com interfaces ou subclasses, há alguma maneira usando delegados?
- Edit: -
Houve duas respostas possíveis.
Uma resposta é a utilização de genéricos, precisamente como sugerido pela resposta 'Sir Lantis' abaixo, e como sugere o post em que John Skeet ligados. Eu suspeito que isso iria funcionar bem na maioria dos cenários. O lado negativo do meu ponto de vista é que ele significa introduzir TFont
como um parâmetro de modelo: não é só uma classe como Word
(que contém uma instância Font
) que precisa para se tornar uma classe genérica (como WordT<TFont>
) ... é também que qualquer classe que contém uma WordT<TFont>
(por exemplo Paragraph
) agora também precisa ser genérica com um parâmetro TFont
(por exemplo ParagraphT<TFont>
). Eventualmente, quase todas as classes na montagem tornou-se uma classe genérica. Este não preservar tipo-segurança e evitar a necessidade de abatido ... e é meio feio, e perturba a ilusão de encapsulamento (a ilusão de que 'Font' é um detalhe de implementação opaco).
Outra resposta é usar um mapa ou dicionário na classe de usuário. Em vez de Font
na biblioteca reutilizável, e em vez de uma interface abstrata, definir uma classe 'pega' como:
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
Então, em vez de downcasting de FontHandle
, mantenha uma instância Dictionary<int, Font>
que mapeia valores FontHandle
a instâncias Font
.
Solução
Em primeiro lugar, gostaria de saber se todo o cenário não é um pouco artificial; você está realmente indo para necessidade este nível de abstração? Talvez subscrever YAGNI ?
Por que seu MyGraphics
só funcionam com um MyFont
? ele pode trabalhar com um IFont
? Isso seria uma melhor utilização de interfaces, e evitaria toda esta questão ...
Uma opção poderia ser um pouco de um re-design, de modo que o IFont
apenas descreve os metadados para o tipo de letra (tamanho, font-face, etc), e você tem coisas na MyGraphics
concreto como:
[public|internal] MyFont GetFont(IFont font) {...} // or just Font
e torna-se o trabalho dos gráficos para fazer a tradução - algo tão então utilizado 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
}
Claro, Point
pode precisar de tradução também ;-p
Outras dicas
Você está dizendo efetivamente "Eu sei melhor do que o compilador - Eu sei que ele é obrigado a ser uma instância de MyFont
." Nesse ponto você tem MyFont
e MyGraphics
sendo fortemente acoplado de novo, o que reduz o ponto da interface um pouco.
deve funcionar MyGraphics
com qualquer IFont
, ou apenas uma MyFont
? Se você pode fazê-lo funcionar com qualquer IFont
você vai ficar bem. Caso contrário, você pode precisar de olhar para os genéricos complicadas para torná-lo todo o tipo de tempo de compilação segura. Você pode encontrar meu post sobre os genéricos no Protocolo Buffers útil como uma situação semelhante.
(sugestão Side -. Seu código será mais idiomaticamente .NET-como se você seguir as convenções de nomenclatura, que inclui caso Pascal para métodos)
Atualmente, estou não completamente consciente de C # mais - tem sido algum tempo. Mas se você não quer para lançar todas as suas coisas lá, você pode ser forçado a medicamentos genéricos de uso.
Eu posso apenas fornecer o código Java, mas C # deve ser capaz de fazer o mesmo através da palavra-chave where
.
Faça sua interface uma interface genérica. Em Java que seria
IMyGraphics<T extends IMyFont>
e depois MyGraphics : IMyGraphics<MyFont>
Em seguida, redefinir a assinatura drawString
tomar T font
como o segundo parâmetro em vez de IMyFont
. Isso deve permitir que você escreva
public void drawString(string text, MyFont font, Point point)
diretamente em sua classe MyGraphics
.
Em C # que IMyGraphics<T extends IMyFont>
deve ser public interface IMyGraphics<T> where T:IMyFont
, mas não estou 100% de certeza sobre isso.
Você não gosta do elenco de IFont
para MyFont
? Você pode fazer isso:
interface IFont {
object Font { get; }
}
class MyFont : IFont {
object Font { get { return ...; } }
}
Claro que você ainda precisa de elenco de System.Object
para System.Drawing.Font
no método de desenho, mas você acabou eliminado a dependência especial implementação de classe (MyFont
).
public void DrawString(string text, IFont font, Point point)
{
System.Drawing.Font f = (Font)font.Font;
graphics.DrawString(text, f, Brushes.Block, point);
}