Vamos supor que eu tenha duas classes parecidas com esta (o primeiro bloco de código e o problema geral estão relacionados ao C #):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
Essas classes não podem ser alteradas de forma alguma (elas fazem parte de uma montagem de terceiros). Portanto, não posso fazê-los implementar a mesma interface ou herdar a mesma classe que conteria IntProperty.
Eu quero aplicar alguma lógica na IntProperty
propriedade de ambas as classes, e em C ++ eu poderia usar uma classe de modelo para fazer isso facilmente:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
E então eu poderia fazer algo assim:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
Dessa forma, eu poderia criar uma única classe de fábrica genérica que funcionaria para a ClassA e a ClassB.
No entanto, em C #, tenho que escrever duas classes com duas where
cláusulas diferentes, mesmo que o código para a lógica seja exatamente o mesmo:
public class LogicAToBeApplied<T> where T : ClassA, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
public class LogicBToBeApplied<T> where T : ClassB, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
Eu sei que se eu quero ter classes diferentes na where
cláusula, elas precisam estar relacionadas, ou seja, herdar a mesma classe, se eu quiser aplicar o mesmo código a elas no sentido que descrevi acima. É apenas muito chato ter dois métodos completamente idênticos. Também não quero usar a reflexão por causa dos problemas de desempenho.
Alguém pode sugerir alguma abordagem em que isso possa ser escrito de uma maneira mais elegante?
Respostas:
Inclua uma interface de proxy (às vezes chamada de adaptador , ocasionalmente com diferenças sutis), implemente
LogicToBeApplied
em termos de proxy e adicione uma maneira de construir uma instância desse proxy a partir de duas lambdas: uma para a propriedade get e outra para o conjunto.Agora, sempre que você precisar passar um IProxy, mas possuir uma instância de classes de terceiros, basta passar algumas lambdas:
Além disso, você pode escrever ajudantes simples para construir instâncias do LamdaProxy a partir de instâncias de A ou B. Eles podem até ser métodos de extensão para fornecer um estilo "fluente":
E agora a construção de proxies fica assim:
Quanto à sua fábrica, verificaria se você pode refatorá-la para um método "principal" de fábrica que aceite um IProxy e execute toda a lógica nele e outros métodos que apenas passam
new A().Proxied()
ounew B().Proxied()
:Não há como fazer o equivalente ao seu código C ++ em C # porque os modelos C ++ dependem da digitação estrutural . Desde que duas classes tenham o mesmo nome e assinatura de método, em C ++, você pode chamar esse método genericamente em ambas. C # possui digitação nominal - o nome de uma classe ou interface faz parte de seu tipo. Portanto, as classes
A
eB
não podem ser tratadas da mesma forma em nenhuma capacidade, a menos que um relacionamento explícito "é um" seja definido por herança ou implementação de interface.Se o clichê da implementação desses métodos por classe for muito grande, você pode escrever uma função que pega um objeto e cria um reflexivo
LambdaProxy
procurando um nome de propriedade específico:Isso falha abismalmente quando dados objetos do tipo incorreto; A reflexão introduz inerentemente a possibilidade de falhas que o sistema do tipo C # não pode impedir. Felizmente, você pode evitar a reflexão até que a carga de manutenção dos ajudantes se torne muito grande, porque você não precisa modificar a interface IProxy ou a implementação LambdaProxy para adicionar o açúcar reflexivo.
Parte da razão pela qual isso funciona é que
LambdaProxy
é "maximamente genérico"; ele pode adaptar qualquer valor que implemente o "espírito" do contrato IProxy, porque a implementação do LambdaProxy é completamente definida pelas funções getter e setter fornecidas. Até funciona se as classes tiverem nomes diferentes para a propriedade, ou tipos diferentes que sejam representáveis de forma sensata e segura comoint
s, ou se houver alguma maneira de mapear o conceito queProperty
deve representar para outros recursos da classe. A indireção fornecida pelas funções oferece a máxima flexibilidade.fonte
ReflectiveProxier
, você poderia criar um proxy usando adynamic
palavra - chave? Parece-me que você teria os mesmos problemas fundamentais (ou seja, erros que são detectados apenas no tempo de execução), mas a sintaxe e a manutenção seriam muito mais simples.Aqui está um resumo de como usar adaptadores sem herdar de A e / ou B, com a possibilidade de usá-los para objetos A e B existentes:
Normalmente, eu preferiria esse tipo de adaptador de objeto a proxies de classe; eles evitam problemas feios com os quais você pode se deparar com herança. Por exemplo, esta solução funcionará mesmo que A e B sejam classes seladas.
fonte
new int Property
? você não está escondendo nada.Você pode se adaptar
ClassA
eClassB
através de uma interface comum. Dessa forma, seu códigoLogicAToBeApplied
permanece o mesmo. Não é muito diferente do que você tem.fonte
A
,B
tipos de uma interface comum. A grande vantagem é que não precisamos duplicar a lógica comum. A desvantagem é que a lógica agora instancia o wrapper / proxy em vez do tipo real.LogicToBeApplied
tem uma certa complexidade e não deve ser repetido em dois locais da base de código sob nenhuma circunstância. Em seguida, o código adicional padrão é geralmente insignificante.A versão C ++ funciona apenas porque seus modelos usam "digitação estática de pato" - qualquer coisa é compilada desde que o tipo forneça os nomes corretos. É mais como um sistema macro. O sistema genérico de C # e outros idiomas funciona de maneira muito diferente.
As respostas de devnull e Doc Brown mostram como o padrão do adaptador pode ser usado para manter seu algoritmo geral e ainda operar em tipos arbitrários ... com algumas restrições. Notavelmente, agora você está criando um tipo diferente do que realmente deseja.
Com um pouco de truque, é possível usar exatamente o tipo pretendido sem nenhuma alteração. No entanto, agora precisamos extrair todas as interações com o tipo de destino em uma interface separada. Aqui, essas interações são construção e atribuição de propriedades:
Em uma interpretação OOP, este seria um exemplo do padrão de estratégia , embora misturado com genéricos.
Em seguida, podemos reescrever sua lógica para usar essas interações:
As definições de interação seriam parecidas com:
A grande desvantagem dessa abordagem é que o programador precisa escrever e passar uma instância de interação ao chamar a lógica. Isso é bastante semelhante às soluções baseadas em padrão de adaptador, mas é um pouco mais geral.
Na minha experiência, este é o mais próximo possível das funções de modelo em outros idiomas. Técnicas semelhantes são usadas em Haskell, Scala, Go e Rust para implementar interfaces fora de uma definição de tipo. No entanto, nessas linguagens, o compilador intervém e seleciona implicitamente a instância de interação correta, para que você não veja o argumento extra. Isso também é semelhante aos métodos de extensão do C #, mas não está restrito aos métodos estáticos.
fonte
Se você realmente quer ter cuidado com o vento, pode usar "dinâmico" para fazer com que o compilador cuide de toda a sujeira da reflexão. Isso resultará em um erro de tempo de execução se você passar um objeto para SetSomeProperty que não possui uma propriedade chamada SomeProperty.
fonte
As outras respostas identificam corretamente o problema e fornecem soluções viáveis. O C # não suporta (geralmente) a "digitação de pato" ("Se andar como um pato ..."), portanto não há como forçar
ClassA
eClassB
ser intercambiável se eles não foram projetados dessa maneira.No entanto, se você já está disposto a aceitar o risco de uma falha de tempo de execução, há uma resposta mais fácil do que usar o Reflection.
C # tem a
dynamic
palavra - chave que é perfeita para situações como essa. Ele diz ao compilador "Eu não saberei que tipo é esse até o tempo de execução (e talvez nem mesmo assim), então permita-me fazer qualquer coisa a ele".Usando isso, você pode criar exatamente a função que deseja:
Observe o uso da
static
palavra - chave também. Isso permite que você use isso como:Não há implicações no desempenho geral do uso
dynamic
, da maneira como ocorre o sucesso único (e a complexidade adicional) do uso do Reflection. A primeira vez que seu código atingir a chamada dinâmica com um tipo específico terá uma pequena sobrecarga , mas as chamadas repetidas serão tão rápidas quanto o código padrão. No entanto, você irá obter umRuntimeBinderException
se você tentar passar em algo que não tem essa propriedade, e não há nenhuma boa maneira de verificar que antes do tempo. Você pode lidar especificamente com esse erro de uma maneira útil.fonte
Você pode usar a reflexão para extrair a propriedade pelo nome.
Obviamente, você corre o risco de um erro de tempo de execução com este método. É o que o C # está tentando impedir você de fazer.
Eu li em algum lugar que uma versão futura do C # permitirá que você passe objetos como uma interface que eles não herdam, mas correspondem. O que também resolveria seu problema.
(Vou tentar desenterrar o artigo)
Outro método, embora não tenha certeza de que poupa algum código, seria subclassificar A e B e também herdar uma Interface com IntProperty.
fonte
Eu só queria usar
implicit operator
conversões junto com a abordagem delegado / lambda da resposta de Jack.A
eB
são como assumidos:É fácil obter uma sintaxe agradável com conversões implícitas definidas pelo usuário (não são necessários métodos de extensão ou similares):
Ilustração de uso:
O
Initialize
método mostra como você pode trabalharAdapter
sem se preocupar se é umA
ou umB
ou algo mais. As invocações doInitialize
método mostram que não precisamos de nenhum molde (visível) ou.AsProxy()
ou similar para tratar concretoA
ouB
como umAdapter
.Considere se você deseja lançar um
ArgumentNullException
nas conversões definidas pelo usuário se o argumento passado for uma referência nula ou não.fonte