studio programacion móviles ejemplo desarrollo curso aplicaciones java generics fluent-interface

java - móviles - manual de programacion android pdf



API fluida con herencia y genéricos. (5)

El compilador le advierte de esta operación insegura, ya que no puede verificar la exactitud de su código. Esto lo hace, de hecho, inseguro y no hay nada que pueda hacer para evitar esta advertencia. A pesar de que una operación no segura no está verificada por compilación, aún puede ser legítima en tiempo de ejecución. Si elude la verificación del compilador, es su trabajo validar su propio código para el uso de los tipos correctos, que es para lo que sirve la @SupressWarning("unchecked") .

Para aplicar esto a tu ejemplo:

public abstract class Message<T extends Message<T>> { // ... @SupressWarning("unchecked") public T withID(String id) { return (T) this; } }

está bien, porque de hecho puede decir con certeza que esta instancia de Message siempre es del tipo representado por T Pero el compilador de Java no puede (todavía). Al igual que con otras advertencias de supresión, ¡la clave para utilizar la anotación es minimizar su alcance! De lo contrario, puede conservar fácilmente la supresión de anotaciones por accidente después de realizar cambios en el código que hacen que su verificación manual anterior de seguridad de tipo no sea válida.

Como solo devuelve this instancia, sin embargo, puede externalizar fácilmente la tarea a un método específico como se recomienda en otra respuesta. Definir un método protected como

@SupressWarning("unchecked") public T self() { (T) this; }

y siempre puedes llamar al mutador como aquí:

public T withID(String id) { return self(); }

Como otra opción, y si es posible que usted implemente, considere un constructor inmutable que solo expone su API mediante interfaces pero implementa un constructor completo. Así es como normalmente construyo interfaces fluidas en estos días:

interface Two<T> { T make() } interface One { <S> Two<S> do(S value) } class MyBuilder<T> implements One, Two<T> { public static One newInstance() { return new MyBuilder<Object>(null); } private T value; // private constructors omitted public <S> Two<S> do(S value) { return new MyBuilder<S>(value); } public T make() { return value; } }

Por supuesto, puede crear construcciones más inteligentes donde evite los campos no utilizados. Si desea ver ejemplos de cómo utilizo este enfoque, vea mis dos proyectos que usan interfaces fluidas con bastante fuerza:

  1. Byte Buddy : API para definir una clase Java en tiempo de ejecución.
  2. Conversor de PDF : un software de conversión para convertir archivos utilizando MS Word de Java.

Estoy escribiendo una API fluida para configurar e instanciar una serie de objetos "mensaje". Tengo una jerarquía de tipos de mensajes.

Para poder acceder al método de las subclases al usar la API fluida, usé genéricos para parametrizar las subclases y hacer que todos los métodos fluidos (que comienzan con "con") devuelvan el tipo genérico. Tenga en cuenta que omití la mayor parte del cuerpo del método fluido; Mucha configuración va en ellos.

public abstract class Message<T extends Message<T>> { protected Message() { } public T withID(String id) { return (T) this; } }

Las subclases concretas redefinen el tipo genérico de manera similar.

public class CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>> { protected CommandMessage() { super(); } public static CommandMessage newMessage() { return new CommandMessage(); } public T withCommand(String command) { return (T) this; } } public class CommandWithParamsMessage extends CommandMessage<CommandWithParamsMessage> { public static CommandWithParamsMessage newMessage() { return new CommandWithParamsMessage(); } public CommandWithParamsMessage withParameter(String paramName, String paramValue) { contents.put(paramName, paramValue); return this; } }

Este código funciona, es decir, puedo crear una instancia de cualquiera de las clases y usar todos los métodos fluidos:

CommandWithParamsMessage msg = CommandWithParamsMessage.newMessage() .withID("do") .withCommand("doAction") .withParameter("arg", "value");

Llamar a los métodos fluidos en cualquier orden es un objetivo importante aquí.

Sin embargo, el compilador advierte que todo return (T) this no es seguro.

Tipo de seguridad: Unchecked cast from Message to T

No estoy seguro de cómo reorganizar la jerarquía para hacer que este código sea realmente seguro. Aunque funciona, el uso de genéricos de esta manera se siente realmente complicado. Especialmente, no puedo prever situaciones en las que se produzcan excepciones en el tiempo de ejecución si simplemente ignoro las advertencias. Habrá nuevos tipos de mensajes, así que necesito mantener el código extensible. Si la solución es evitar la herencia por completo, también me gustaría obtener sugerencias de alternativas.

Hay other questions aquí en SO que tratan un problema similar. Señalan una solución en la que todas las clases intermedias son abstractas y declaran un método como protected abstract self() . Aún así, al final no es seguro.


Esto no es una solución para su problema original. Es solo un intento de capturar su intención real y esbozar un enfoque donde el problema original no aparece. (Me gustan los genéricos, pero los nombres de clase como CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>> me hace temblar ...)

Sé que esto es estructuralmente bastante diferente de lo que pidió originalmente, y es posible que haya omitido algunos detalles en la pregunta que limitan el rango de posibles respuestas para que la siguiente información ya no sea aplicable.

Pero si entendí tu intención correctamente, podrías considerar dejar que los subtipos sean manejados por las llamadas fluidas.

La idea aquí es que inicialmente solo puedes crear un Message simple:

Message m0 = Message.newMessage(); Message m1 = m0.withID("id");

En este mensaje, puede llamar al método withID , que es el único método que todos los mensajes tienen en común. El método withID devuelve un Message en este caso.

Hasta ahora, el mensaje no es un CommandMessage ni ninguna otra forma especializada. Sin embargo, cuando llama al método withCommand , obviamente desea construir un CommandMessage , por lo que ahora simplemente devuelve un CommandMessage :

CommandMessage m2 = m1.withCommand("command");

De manera similar, cuando llama al método withParameter , recibe un CommandWithParamsMessage :

CommandWithParamsMessage m3 = m2.withParameter("name", "value");

Esta idea está más o menos (!) Inspirada en una entrada de blog , que está en alemán, pero el código muestra muy bien cómo se puede usar este concepto para construir consultas de tipo "Seleccionar desde donde" de tipo seguro.

Aquí, el enfoque es esbozado, aproximadamente adaptado para su caso de uso. Por supuesto, hay algunos detalles en los que la implementación dependerá de cómo se vaya a utilizar, pero espero que la idea quede clara.

import java.util.HashMap; import java.util.Map; public class FluentTest { public static void main(String[] args) { CommandWithParamsMessage msg = Message.newMessage(). withID("do"). withCommand("doAction"). withParameter("arg", "value"); Message m0 = Message.newMessage(); Message m1 = m0.withID("id"); CommandMessage m2 = m1.withCommand("command"); CommandWithParamsMessage m3 = m2.withParameter("name", "value"); CommandWithParamsMessage m4 = m3.withCommand("otherCommand"); CommandWithParamsMessage m5 = m4.withID("otherID"); } } class Message { protected String id; protected Map<String, String> contents; static Message newMessage() { return new Message(); } private Message() { contents = new HashMap<>(); } protected Message(Map<String, String> contents) { this.contents = contents; } public Message withID(String id) { this.id = id; return this; } public CommandMessage withCommand(String command) { Map<String, String> newContents = new HashMap<String, String>(contents); newContents.put("command", command); return new CommandMessage(newContents); } } class CommandMessage extends Message { protected CommandMessage(Map<String, String> contents) { super(contents); } @Override public CommandMessage withID(String id) { this.id = id; return this; } public CommandWithParamsMessage withParameter(String paramName, String paramValue) { Map<String, String> newContents = new HashMap<String, String>(contents); newContents.put(paramName, paramValue); return new CommandWithParamsMessage(newContents); } } class CommandWithParamsMessage extends CommandMessage { protected CommandWithParamsMessage(Map<String, String> contents) { super(contents); } @Override public CommandWithParamsMessage withID(String id) { this.id = id; return this; } @Override public CommandWithParamsMessage withCommand(String command) { this.contents.put("command", command); return this; } }


He hecho algo como esto antes. Se puede poner feo. De hecho, lo he intentado más veces de lo que lo he usado; Por lo general, se borra y trato de encontrar un mejor diseño. Dicho esto, para ayudarte a avanzar un poco más en el camino, prueba esto:

Haga que sus clases abstractas declaren un método como:

protected abstract T self();

Esto puede reemplazar this en sus declaraciones de devolución. Las subclases deberán devolver algo que coincida con el límite de T , pero no garantiza que devuelvan el mismo objeto.


Si cambia las firmas de esta manera, no debería recibir ninguna advertencia ni necesita ningún molde:

abstract class Message<T extends Message<T>> { public T withID(String id) { return self(); } protected abstract T self(); } abstract class CommandMessage<T extends CommandMessage<T>> extends Message<T> { public T withCommand(String command) { // do some work ... return self(); } } class CommandWithParamsMessage extends CommandMessage<CommandWithParamsMessage> { public static CommandWithParamsMessage newMessage() { return new CommandWithParamsMessage(); } public CommandWithParamsMessage withParameter(String paramName, String paramValue) { // do some work ... return this; } @Override protected CommandWithParamsMessage self() { return this; } }


Su código es fundamentalmente un uso inseguro de Genéricos. Por ejemplo, si escribo una nueva clase que extiende el mensaje, diga Amenaza, y tiene un nuevo método doSomething (), y luego creo un mensaje parametrizado por esta nueva clase y crea una instancia de Mensaje, y luego intento lanzarlo. a su subclase. Sin embargo, dado que es una instancia de Mensaje, y no de Amenaza, un intento de llamar a este mensaje causará una Excepción. Dado que no es posible que Mensaje haga doSOmething ().

Además, también es innecesario usar genéricos aquí. La herencia antigua sencilla funcionará bien. Dado que los subtipos pueden anular los métodos al hacer que sus tipos de retorno sean más específicos, puede tener:

public abstract class Message { protected Message() { } public Message withID(String id) { return this; } }

Y entonces

public class CommandMessage extends Message { protected CommandMessage() { super(); } public static CommandMessage newMessage() { return new CommandMessage(); } public CommandMessage withCommand(String command) { return this; } }

Esto funcionará bien, en el entendimiento de que convoca sus argumentos en el orden correcto:

CommandWithParamsMessage.newMessage() .withID("do") .withCommand("doAction") .withParameter("arg", "value");

fallará, pero

CommandWithParamsMessage.newMessage().withParameter("arg", "value") .withCommand("doAction").withID("do")

Tendrá éxito, ya que solo "tipos arriba", finalmente devuelve una clase de "mensaje". Si no desea que se "suba", simplemente sobrescriba los comandos heredados y ahora puede llamar a los métodos en cualquier orden, ya que todos devuelven el tipo original.

P.ej

public class CommandWithParamsMessage extends CommandMessage { public static CommandWithParamsMessage newMessage() { return new CommandWithParamsMessage(); } public CommandWithParamsMessage withParameter(String paramName, String paramValue) { contents.put(paramName, paramValue); return this; } @Override public CommandWithParamsMessage withCommand(String command){ super.withCommand(command); return this; } @Override public CommandWithParamsMessage withID(String s){ super.withID(s); return this; } }

Ahora devolverá con fluidez un CommandWithParamsMessage con cualquiera de las dos llamadas anteriores.

¿Resuelve esto tu problema o he malinterpretado tu intención?