patterns - ¿Parámetro de método multitipo de Java?
singleton design pattern in java (8)
Me pregunto si es posible requerir que un parámetro del método Java sea de cualquier tipo de un conjunto finito de tipos. Por ejemplo, estoy usando una biblioteca donde dos (o más) tipos tienen métodos comunes, pero su antepasado común más bajo en la jerarquía de tipos es Objeto. Lo que quiero decir aquí:
public interface A {
void myMethod();
}
public interface B {
void myMethod();
}
...
public void useMyMethod(A a) {
// code duplication
}
public void useMyMethod(B b) {
// code duplication
}
Quiero evitar la duplicación de código. Lo que pienso es algo como esto:
public void useMyMethod(A|B obj){
obj.myMethod();
}
Ya hay un tipo similar de sintaxis en java. Por ejemplo:
try{
//fail
} catch (IllegalArgumentException | IllegalStateException e){
// use e safely here
}
Obviamente esto no es posible. ¿Cómo puedo lograr un código bien diseñado con este tipo de jerarquía de tipos no editable?
¿Qué hay de pasar la función como un parámetro a su función useMyMethod?
Si está utilizando Java <8:
public interface A {
void myMethod();
}
public interface B {
void myMethod();
}
public void useMyMethod(Callable<Void> myMethod) {
try {
myMethod.call();
} catch(Exception e) {
// handle exception of callable interface
}
}
//Use
public void test() {
interfaceA a = new ClassImplementingA();
useMyMethod(new Callable<Void>() {
public call() {
a.myMethod();
return null;
}
});
interfaceB b = new ClassImplementingB();
useMyMethod(new Callable<Void>() {
public call() {
b.myMethod();
return null;
}
});
}
Para Java> = 8, puedes usar Lambda Expressions :
public interface IMyMethod {
void myMethod();
}
public void useMyMethod(IMyMethod theMethod) {
theMethod.myMethod();
}
//Use
public void test() {
interfaceA a = new ClassImplementingA();
useMyMethod(() -> a.myMethod());
interfaceB b = new ClassImplementingB();
useMyMethod(() -> b.myMethod());
}
Bueno, la forma correcta de modelar su requisito sería tener myMethod () declarado en una interfaz de supertipo C que se extienden tanto A como B; su método acepta entonces el tipo C como su parámetro. El hecho de que tenga problemas para hacer esto en la situación que describe indica que no está modelando la jerarquía de clases de una manera que realmente refleje cómo se comportan.
Por supuesto, si no puede cambiar la estructura de la interfaz, siempre puede hacerlo con reflexiones.
public static void useMyMethod(Object classAorB) throws Exception {
classAorB.getClass().getMethod("myMethod").invoke(classAorB);
}
Esto me parece mucho al patrón de la plantilla:
public interface A {
void myMethod();
}
public interface B {
void myMethod();
}
public class C {
private abstract class AorBCaller {
abstract void myMethod();
}
public void useMyMethod(A a) {
commonAndUseMyMethod(new AorBCaller() {
@Override
void myMethod() {
a.myMethod();
}
});
}
public void useMyMethod(B b) {
commonAndUseMyMethod(new AorBCaller() {
@Override
void myMethod() {
b.myMethod();
}
});
}
private void commonAndUseMyMethod(AorBCaller aOrB) {
// ... Loads of stuff.
aOrB.myMethod();
// ... Loads more stuff
}
}
En Java 8 es mucho más sucinto:
public class C {
// Expose an "A" form of the method.
public void useMyMethod(A a) {
commonAndUseMyMethod(() -> a.myMethod());
}
// And a "B" form.
public void useMyMethod(B b) {
commonAndUseMyMethod(() -> b.myMethod());
}
private void commonAndUseMyMethod(Runnable aOrB) {
// ... Loads of stuff -- no longer duplicated.
aOrB.run();
// ... Loads more stuff
}
}
Esto podría no constituir una buena práctica, pero podría crear una nueva clase (llámela C), que contenga las partes de A y B que están duplicadas, y crear un nuevo método que tome C, tener sus métodos que tomen A y ¿B hace una instancia de C y llama al nuevo método?
Para que tengas
class C {
// Stuff from both A and B
}
public void useMyMethod(A a) {
// Make a C
useMyMethod(c);
}
public void useMyMethod(B b) {
// Make a C
useMyMethod(c);
}
public void useMyMethod(C c) {
// previously duplicated code
}
Eso también le permitiría mantener cualquier código no duplicado en los métodos para A y B (si hay alguno).
La forma correcta es usar los genéricos de Java.
Consulte http://docs.oracle.com/javase/tutorial/java/generics/bounded.html
Podría escribir una interface
MyInterface
con un solo método myMethod
. Luego, para cada tipo que desee considerar como parte del conjunto finito, escriba una clase contenedora, como esta:
class Wrapper1 implements MyInterface {
private final Type1 type1;
Wrapper1(Type1 type1) {
this.type1 = type1;
}
@Override
public void myMethod() {
type1.method1();
}
}
Entonces solo necesita usar un MyInterface
lugar de uno de los conjuntos de tipos finitos, y siempre se llamará al método apropiado del tipo apropiado.
Tenga en cuenta que para utilizar realmente estas clases de envoltura para llamar al método myMethod
, tendría que escribir
myMethod(new Wrapper1(type1));
Esto se pondrá un poco feo, ya que tendrá que recordar el nombre de la clase contenedora para cada tipo en el conjunto. Por este motivo, es posible que prefiera reemplazar MyInterface
por una clase abstracta con varias fábricas estáticas que producen los tipos de envoltura. Me gusta esto:
abstract class MyWrapper {
static MyWrapper of(Type1 type1) {
return new Wrapper1(type1);
}
static MyWrapper of(Type2 type2) {
return new Wrapper2(type2);
}
abstract void myMethod();
}
entonces puedes llamar al método usando el código
myMethod(MyWrapper.of(type1));
La ventaja de este enfoque es que el código es el mismo, independientemente del tipo que utilice. Si utiliza este enfoque, debe reemplazar los implements MyInterface
en la declaración de Wrapper1
con extends MyWrapper
.
Se puede usar un proxy dinámico para crear un puente entre una interfaz común que defina y los objetos que implementan las otras interfaces que se ajustan a la nueva interfaz. Luego, puede hacer que su useMyMethod
s convierta el parámetro a la nueva interfaz (como un proxy dinámico) y useMyMethod
que su código común se escriba solo en términos de la nueva interfaz.
Esta sería la nueva interfaz:
interface Common {
void myMethod();
}
Entonces, con este controlador de invocación:
class ForwardInvocationHandler implements InvocationHandler {
private final Object wrapped;
public ForwardInvocationHandler(Object wrapped) {
this.wrapped = wrapped;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Method match = wrapped.getClass().getMethod(method.getName(), method.getParameterTypes());
return match.invoke(wrapped, args);
}
}
Puedes tener tus métodos así:
public void useMyMethod(A a) {
useMyMethod(toCommon(a));
}
public void useMyMethod(B b) {
useMyMethod(toCommon(b));
}
public void useMyMethod(Common common) {
// ...
}
private Common toCommon(Object o) {
return (Common)Proxy.newProxyInstance(
Common.class.getClassLoader(),
new Class[] { Common.class },
new ForwardInvocationHandler(o));
}
Tenga en cuenta que para simplificar los asuntos, incluso podría elegir que una de sus interfaces existentes ( A
o B
) se utilice como interfaz común.
(Vea otro ejemplo here , y también otras ideas sobre este tema)
Trate de usar el patrón de diseño del Adapter .
O, si es posible, agrega alguna interfaz base:
public interface Base {
void myMethod();
}
public interface A extends Base {}
public interface B extends Base {}
...
public void useMyMethod(Base b) {
b.myMethod()
}
Además, puedes usar algo similar a this