Supongamos que tengo dos clases que se ven así (el primer bloque de código y el problema general están relacionados con C #):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
Estas clases no se pueden cambiar de ninguna manera (son parte de una asamblea de terceros). Por lo tanto, no puedo hacer que implementen la misma interfaz o hereden la misma clase que luego contendría IntProperty.
Quiero aplicar algo de lógica en la IntProperty
propiedad de ambas clases, y en C ++ podría usar una clase de plantilla para hacerlo con bastante facilidad:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
Y luego podría hacer algo como esto:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
De esa manera podría crear una única clase de fábrica genérica que funcionaría tanto para ClassA como para ClassB.
Sin embargo, en C #, tengo que escribir dos clases con dos where
cláusulas diferentes a pesar de que el código para la lógica es exactamente el mismo:
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;
}
}
Sé que si quiero tener diferentes clases en la where
cláusula, deben estar relacionadas, es decir, heredar la misma clase, si quiero aplicarles el mismo código en el sentido que describí anteriormente. Es solo que es muy molesto tener dos métodos completamente idénticos. Tampoco quiero usar la reflexión debido a los problemas de rendimiento.
¿Alguien puede sugerir algún enfoque en el que esto se pueda escribir de una manera más elegante?
Respuestas:
Agregue una interfaz proxy (a veces llamada adaptador , ocasionalmente con diferencias sutiles), implemente
LogicToBeApplied
en términos del proxy, luego agregue una forma de construir una instancia de este proxy a partir de dos lambdas: una para la propiedad get y otra para el conjunto.Ahora, cada vez que necesite pasar un IProxy pero tener una instancia de las clases de terceros, puede pasar algunas lambdas:
Además, puede escribir ayudantes simples para construir instancias de LamdaProxy a partir de instancias de A o B. Incluso pueden ser métodos de extensión para darle un estilo "fluido":
Y ahora la construcción de proxies se ve así:
En cuanto a su fábrica, vería si puede refactorizarlo en un método de fábrica "principal" que acepte un IProxy y realice toda la lógica en él y otros métodos que simplemente pasan
new A().Proxied()
onew B().Proxied()
:No hay forma de hacer el equivalente de su código de C ++ en C # porque las plantillas de C ++ se basan en la tipificación estructural . Siempre que dos clases tengan el mismo nombre y firma del método, en C ++ puede llamar a ese método genéricamente en ambos. C # tiene una tipificación nominal : el nombre de una clase o interfaz es parte de su tipo. Por lo tanto, las clases
A
yB
no se pueden tratar de la misma manera, a menos que se defina una relación explícita "es una" mediante herencia o implementación de interfaz.Si la base de implementación de estos métodos por clase es demasiado, puede escribir una función que tome un objeto y construya reflexivamente
LambdaProxy
buscando un nombre de propiedad específico:Esto falla abismalmente cuando se le dan objetos de tipo incorrecto; La reflexión introduce inherentemente la posibilidad de fallas que el sistema tipo C # no puede evitar. Afortunadamente, puede evitar la reflexión hasta que la carga de mantenimiento de los ayudantes sea demasiado grande porque no es necesario que modifique la interfaz IProxy o la implementación de LambdaProxy para agregar el azúcar reflectante.
Parte de la razón por la que esto funciona es que
LambdaProxy
es "máximamente genérico"; puede adaptar cualquier valor que implemente el "espíritu" del contrato IProxy porque la implementación de LambdaProxy está completamente definida por las funciones getter y setter dadas. Incluso funciona si las clases tienen diferentes nombres para la propiedad, o diferentes tipos que son representables de manera sensata y segura comoint
s, o si hay alguna forma de mapear el concepto queProperty
se supone que representa a otras características de la clase. La indirección proporcionada por las funciones le brinda la máxima flexibilidad.fuente
ReflectiveProxier
, ¿podría construir un proxy usando ladynamic
palabra clave? Me parece que tendría los mismos problemas fundamentales (es decir, errores que solo se detectan en tiempo de ejecución), pero la sintaxis y la facilidad de mantenimiento serían mucho más simples.Aquí hay un resumen de cómo usar adaptadores sin heredar de A y / o B, con la posibilidad de usarlos para objetos A y B existentes:
Por lo general, preferiría este tipo de adaptador de objetos en lugar de proxies de clase, evitan problemas feos con los que puede encontrarse con la herencia. Por ejemplo, esta solución funcionará incluso si A y B son clases selladas.
fuente
new int Property
? No estás sombreando nada.Podrías adaptarte
ClassA
y aClassB
través de una interfaz común. De esta manera, su códigoLogicAToBeApplied
permanece igual. Sin embargo, no es muy diferente de lo que tienes.fuente
A
,B
tipos de una interfaz común. La gran ventaja es que no tenemos que duplicar la lógica común. La desventaja es que la lógica ahora instancia el contenedor / proxy en lugar del tipo real.LogicToBeApplied
tiene una cierta complejidad y no debe repetirse en dos lugares en la base del código bajo ninguna circunstancia. Entonces, el código adicional repetitivo suele ser insignificante.La versión C ++ solo funciona porque sus plantillas usan "tipeo de pato estático", todo se compila siempre que el tipo proporcione los nombres correctos. Es más como un sistema macro. El sistema genérico de C # y otros lenguajes funciona de manera muy diferente.
Las respuestas de devnull y Doc Brown muestran cómo se puede usar el patrón del adaptador para mantener su algoritmo en general, y aún operar en tipos arbitrarios ... con un par de restricciones. En particular, ahora está creando un tipo diferente del que realmente desea.
Con un poco de truco, es posible usar exactamente el tipo deseado sin ningún cambio. Sin embargo, ahora necesitamos extraer todas las interacciones con el tipo de destino en una interfaz separada. Aquí, estas interacciones son construcción y asignación de propiedades:
En una interpretación de OOP, este sería un ejemplo del patrón de estrategia , aunque mezclado con genéricos.
Luego podemos reescribir su lógica para usar estas interacciones:
Las definiciones de interacción se verían así:
La gran desventaja de este enfoque es que el programador necesita escribir y pasar una instancia de interacción al llamar a la lógica. Esto es bastante similar a las soluciones basadas en patrones de adaptador, pero es un poco más general.
En mi experiencia, esto es lo más cerca que puede llegar a las funciones de plantilla en otros idiomas. Se utilizan técnicas similares en Haskell, Scala, Go y Rust para implementar interfaces fuera de una definición de tipo. Sin embargo, en estos idiomas el compilador interviene y selecciona la instancia de interacción correcta implícitamente para que no vea el argumento adicional. Esto también es similar a los métodos de extensión de C #, pero no se limita a los métodos estáticos.
fuente
Si realmente quieres arrojar precaución al viento, puedes usar "dinámico" para hacer que el compilador se encargue de toda la maldad del reflejo por ti. Esto generará un error de tiempo de ejecución si pasa un objeto a SetSomeProperty que no tiene una propiedad llamada SomeProperty.
fuente
Las otras respuestas identifican correctamente el problema y proporcionan soluciones viables. C # no admite (en general) el "tipeo de pato" ("Si camina como un pato ..."), por lo que no hay forma de forzarlo
ClassA
yClassB
ser intercambiable si no se diseñaron de esa manera.Sin embargo, si ya está dispuesto a aceptar el riesgo de una falla de tiempo de ejecución, entonces hay una respuesta más fácil que usar Reflection.
C # tiene la
dynamic
palabra clave que es perfecta para situaciones como esta. Le dice al compilador "No sabré de qué tipo es hasta el tiempo de ejecución (y tal vez ni siquiera en ese momento), así que permíteme hacer algo ".Con eso, puede construir exactamente la función que desea:
Tenga en cuenta el uso de la
static
palabra clave también. Eso te permite usar esto como:No hay implicaciones de rendimiento de imagen general del uso
dynamic
, la forma en que existe el éxito único (y la complejidad adicional) de usar Reflection. La primera vez que su código llegue a la llamada dinámica con un tipo específico tendrá una pequeña cantidad de sobrecarga , pero las llamadas repetidas serán tan rápidas como el código estándar. Sin embargo, usted va a obtener unRuntimeBinderException
si intenta pasar en algo que no tiene esa propiedad, y no hay buena manera de comprobar que antes de tiempo. Es posible que desee manejar específicamente ese error de una manera útil.fuente
Puede usar la reflexión para extraer la propiedad por su nombre.
Obviamente corre el riesgo de un error de tiempo de ejecución con este método. Que es lo que C # está tratando de evitar que hagas.
Leí en alguna parte que una versión futura de C # le permitirá pasar objetos como una interfaz que no heredan pero coinciden. Lo que también resolvería tu problema.
(Intentaré desenterrar el artículo)
Otro método, aunque no estoy seguro de que te guarde ningún código, sería subclasificar tanto A como B y también heredar una interfaz con IntProperty.
fuente
Solo quería usar
implicit operator
conversiones junto con el enfoque delegado / lambda de la respuesta de Jack.A
yB
son como se supone:Entonces es fácil obtener una buena sintaxis con conversiones implícitas definidas por el usuario (no se necesitan métodos de extensión o similares):
Ilustración de uso:
El
Initialize
método muestra cómo puede trabajarAdapter
sin preocuparse por si es unaA
o unaB
o alguna otra cosa. Las invocaciones delInitialize
método muestran que no necesitamos ningún molde (visible).AsProxy()
o similar para tratar el concretoA
oB
como unAdapter
.Considere si desea incluir un
ArgumentNullException
en las conversiones definidas por el usuario si el argumento pasado es una referencia nula o no.fuente