Programación para uso futuro de interfaces

42

Tengo un colega sentado a mi lado que diseñó una interfaz como esta:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

El problema es que, en este momento, no estamos usando este parámetro de "fin" en ninguna parte de nuestro código, solo está ahí porque podríamos tener que usarlo en algún momento en el futuro.

Estamos tratando de convencerlo de que es una mala idea poner parámetros en interfaces que no son útiles en este momento, pero él sigue insistiendo en que habrá que hacer mucho trabajo si implementamos el uso de la fecha de "finalización" en algún momento luego y tengo que adaptar todo el código entonces.

Ahora, mi pregunta es, ¿hay alguna fuente que esté manejando un tema como este de gurús de codificación "respetados" con los que podamos vincularlo?

sveri
fuente
29
"La predicción es muy difícil, especialmente sobre el futuro".
Jörg W Mittag
10
Parte del problema es que no es la mejor interfaz para empezar. Obtener Foos y filtrarlos son dos problemas separados. Esa interfaz te obliga a filtrar la lista de una manera particular; un enfoque mucho más general es pasar una función que decida si se debe incluir al foo. Sin embargo, esto solo es elegante si tiene acceso a Java 8.
Doval
55
Contrarrestar es el punto. Simplemente haga que ese método acepte tipos personalizados que por ahora solo contienen lo que necesita y eventualmente podría agregar el endparámetro a ese objeto e incluso predeterminarlo para no romper el código
Rémi
3
Con los métodos predeterminados de Java 8 no hay razón para agregar argumentos innecesarios. Un método puede ser añadido más tarde con una aplicación predeterminada que simples llamadas definen con la con, por ejemplo, null. La implementación de clases puede anularse según sea necesario.
Boris the Spider
1
@Doval No sé sobre Java, pero en .NET a veces verá cosas como evitar exponer las peculiaridades de IQueryable(solo puede tomar ciertas expresiones) al código fuera del DAL
Ben Aaronson

Respuestas:

62

Invítalo a aprender sobre YAGNI . La parte de Justificación de la página de Wikipedia puede ser particularmente interesante aquí:

Según quienes abogan por el enfoque YAGNI, la tentación de escribir código que no es necesario en este momento, pero podría serlo en el futuro, tiene las siguientes desventajas:

  • El tiempo empleado se toma agregando, probando o mejorando la funcionalidad necesaria.
  • Las nuevas funciones se deben depurar, documentar y admitir.
  • Cualquier característica nueva impone restricciones sobre lo que se puede hacer en el futuro, por lo que una característica innecesaria puede impedir que se agreguen características necesarias en el futuro.
  • Hasta que la característica sea realmente necesaria, es difícil definir completamente lo que debe hacer y probarla. Si la nueva función no se define y prueba correctamente, es posible que no funcione correctamente, incluso si finalmente se necesita.
  • Conduce a la hinchazón de código; El software se hace más grande y más complicado.
  • A menos que haya especificaciones y algún tipo de control de revisión, la función puede no ser conocida por los programadores que podrían utilizarla.
  • Agregar la nueva función puede sugerir otras funciones nuevas. Si estas nuevas características también se implementan, esto podría dar como resultado un efecto de bola de nieve hacia el arrastre de características.

Otros posibles argumentos:

  • "El 80% del costo de por vida de un software se destina al mantenimiento" . Escribir código justo a tiempo reduce el costo del mantenimiento: uno tiene que mantener menos código y puede concentrarse en el código realmente necesario.

  • El código fuente se escribe una vez, pero se lee docenas de veces. Un argumento adicional, que no se usa en ningún lado, llevaría a perder el tiempo entendiendo por qué hay un argumento que no es necesario. Dado que esta es una interfaz con varias implementaciones posibles, las cosas solo son más difíciles.

  • Se espera que el código fuente sea autodocumentado. La firma real es engañosa, ya que un lector pensaría que endafecta el resultado o la ejecución del método.

  • Las personas que escriben implementaciones concretas de esta interfaz pueden no entender que el último argumento no debe usarse, lo que conduciría a diferentes enfoques:

    1. No necesito end, así que simplemente ignoraré su valor,

    2. No necesito end, así que lanzaré una excepción si no es así null,

    3. No lo necesito end, pero intentaré usarlo de alguna manera,

    4. Escribiré un montón de código que podría usarse más tarde cuando endsea ​​necesario.

Pero tenga en cuenta que su colega puede tener razón.

Todos los puntos anteriores se basan en el hecho de que la refactorización es fácil, por lo que agregar un argumento más adelante no requerirá mucho esfuerzo. Pero esta es una interfaz y, como interfaz, puede ser utilizada por varios equipos que contribuyen a otras partes de su producto. Esto significa que cambiar una interfaz puede ser particularmente doloroso, en cuyo caso, YAGNI realmente no se aplica aquí.

La respuesta de hjk ofrece una buena solución: agregar un método a una interfaz ya utilizada no es particularmente difícil, pero en algunos casos, también tiene un costo considerable:

  • Algunos marcos no admiten sobrecargas. Por ejemplo, y si recuerdo bien (corrígeme si me equivoco), WCF de .NET no admite sobrecargas.

  • Si la interfaz tiene muchas implementaciones concretas, agregar un método a la interfaz requeriría revisar todas las implementaciones y agregar el método allí también.

Arseni Mourzenko
fuente
44
Creo que también es importante tener en cuenta la probabilidad de que aparezca esta característica en el futuro. Si sabe con certeza que se agregará, pero no en este momento por cualquier razón, entonces diría que es un buen argumento a tener en cuenta, ya que no es solo una función "por si acaso".
Jeroen Vannevel
3
@JeroenVannevel: ciertamente, pero eso es muy especulativo. Según mi experiencia, la mayoría de las funciones que creía que se implementarían se cancelaron o retrasaron para siempre. Esto incluye características que originalmente fueron destacadas como muy importantes por las partes interesadas.
Arseni Mourzenko
26

pero sigue insistiendo en que habrá que hacer mucho trabajo si implementamos el uso de la fecha de "finalización" algún tiempo después y luego tenemos que adaptar todo el código.

(algún tiempo después)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

Eso es todo lo que necesitas, sobrecarga de métodos. El método adicional en el futuro se puede introducir de forma transparente sin afectar las llamadas al método existente.

hjk
fuente
66
Excepto que su cambio significa que ya no es una interfaz, y que no será posible usarla en los casos en que un objeto ya se derive de un objeto e implemente la interfaz. Es un problema trivial si todas las clases que implementan la interfaz están en el mismo proyecto (errores de compilación de persecución). Si se trata de una biblioteca utilizada por múltiples proyectos, la adición del campo causa confusión y trabajo para otras personas en momentos esporádicos durante los próximos x meses / años.
Kieveli
1
Si el equipo de OP (salve al colega) realmente cree que el endparámetro es innecesario ahora y ha utilizado el endmétodo sin uso en todo el proyecto, entonces la actualización de la interfaz es la menor de sus preocupaciones porque se convierte en una necesidad de actualizar el clases de implementación Mi respuesta está dirigida específicamente a la parte de "adaptar todo el código", porque parece que el colega estaba pensando en la línea de tener que actualizar a los llamantes del método original en todo el proyecto para introducir un tercer parámetro predeterminado / ficticio .
hjk
18

[¿Hay] gurús de codificación "respetados" con los que podamos vincularlo [para persuadirlo]?

Las apelaciones a la autoridad no son particularmente convincentes; mejor presentar un argumento que sea válido sin importar quién lo haya dicho.

Aquí hay una reducción ad absurdum que debería persuadir o demostrar que su compañero de trabajo está atascado en ser "correcto" independientemente de su sensibilidad:


Lo que realmente necesitas es

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

Nunca se sabe cuándo tendrá que internacionalizarse para Klingon, por lo que es mejor que se encargue de esto ahora porque requerirá mucho trabajo para su adaptación y los Klingon no son conocidos por su paciencia.

msw
fuente
22
You never know when you'll have to internationalize for Klingon. Es por eso que solo debe pasar un Calendar, y dejar que el código del cliente decida si quiere enviar un GregorianCalendaro un KlingonCalendar(apuesto a que algunos ya lo hicieron). Duh :-p
SJuan76
3
¿Qué hay de StarDate sdStarty StarDate sdEnd? No podemos olvidarnos de la Federación después de todo ...
WernerCD
¡ Obviamente, todas esas son subclases o convertibles en Date!
Darkhogg
Si tan solo fuera así de simple .
msw
@msw, no estoy seguro de que estaba pidiendo una "apelación a la autoridad" cuando solicitó enlaces a "respetados gurús de la codificación". Como usted dice, necesita "un argumento que sea válido sin importar quién lo haya dicho". Las personas del tipo "gurú" tienden a conocer muchas de ellas. También te olvidaste HebrewDate starty HebrewDate end.
trysis
12

Desde una perspectiva de ingeniería de software, creo que la solución adecuada para este tipo de problemas está en el patrón del constructor. Este es definitivamente un enlace de autores 'gurú' para su colega http://en.wikipedia.org/wiki/Builder_pattern .

En el patrón de construcción, el usuario crea un objeto que contiene los parámetros. Este contenedor de parámetros se pasará al método. Esto se encargará de cualquier extensión y sobrecarga de parámetros que su colega pueda necesitar en el futuro, al tiempo que hará que todo sea muy estable cuando sea necesario realizar cambios.

Tu ejemplo se convertirá en:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}
InformadoA
fuente
3
Este es un buen enfoque general, pero en este caso parece empujar el problema de IEventGettera ISearchFootRequest. En cambio, sugeriría algo como public IFooFilterwith member public bool include(FooType item)que devuelve si se incluye o no el elemento. Luego, las implementaciones de interfaz individual pueden decidir cómo harán ese filtrado
Ben Aaronson
@Ben Aaronson Acabo de editar el código para mostrar cómo se crea el objeto de parámetro Solicitud
InformadoA
El constructor es innecesario aquí (y, francamente, un patrón en exceso / recomendado en general); no existen reglas complejas sobre cómo deben configurarse los parámetros, por lo que un objeto de parámetro está perfectamente bien si existe una preocupación legítima sobre si los parámetros del método son complejos o se cambian con frecuencia. Y estoy de acuerdo con @BenAaronson, aunque el objetivo de usar un objeto de parámetro no es incluir los parámetros innecesarios desde el principio, sino hacerlos muy fáciles de agregar más tarde (incluso si hay implementaciones de interfaz múltiple).
Aaronaught
7

No lo necesita ahora, así que no lo agregue. Si lo necesita más tarde, extienda la interfaz:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}
ToddR
fuente
+1 por no cambiar la interfaz existente (y probablemente ya probada / enviada).
Sebastian Godelet
4

Ningún principio de diseño es absoluto, por lo que, si bien estoy de acuerdo con las otras respuestas, pensé en jugar al abogado del diablo y discutir algunas condiciones bajo las cuales consideraría aceptar la solución de su colega:

  • Si se trata de una API pública, y anticipa que la característica será útil para desarrolladores de terceros, incluso si no la usa internamente.
  • Si tiene considerables beneficios inmediatos, no solo los futuros. Si la fecha de finalización implícita es Now(), agregar un parámetro elimina un efecto secundario, que tiene beneficios para el almacenamiento en caché y las pruebas unitarias. Tal vez permite una implementación más simple. O tal vez sea más consistente con otras API en su código.
  • Si su cultura de desarrollo tiene una historia problemática. Si los procesos o la territorialidad hacen que sea demasiado difícil cambiar algo central como una interfaz, entonces lo que he visto suceder antes es que las personas implementen soluciones alternativas del lado del cliente en lugar de cambiar la interfaz, entonces está tratando de mantener una docena de fecha de finalización ad hoc filtros en lugar de uno. Si ese tipo de cosas suceden mucho en su empresa, tiene sentido esforzarse un poco más en las pruebas futuras. No me malinterpreten, es mejor cambiar sus procesos de desarrollo, pero esto suele ser más fácil decirlo que hacerlo.

Dicho esto, en mi experiencia, el corolario más grande para YAGNI es YDKWFYN: No sabes qué forma necesitarás (sí, acabo de hacer ese acrónimo). Incluso si la necesidad de algún parámetro de límite puede ser relativamente predecible, puede tomar la forma de un límite de página, o un número de días, o un valor booleano que especifica el uso de la fecha de finalización de una tabla de preferencias del usuario, o cualquier cantidad de cosas.

Como todavía no tiene el requisito, no tiene forma de saber qué tipo de parámetro debería ser. A menudo terminas teniendo una interfaz incómoda que no es la que mejor se ajusta a tus necesidades, o teniendo que cambiarla de todos modos.

Karl Bielefeldt
fuente
2

No hay suficiente información para responder esta pregunta. Depende de lo que getFooListrealmente hace y cómo lo hace.

Aquí hay un ejemplo obvio de un método que debería admitir un parámetro adicional, usado o no.

void CapitalizeSubstring (String capitalize_me, int start_index);

Implementar un método que opera en una colección donde puede especificar el inicio pero no el final de la colección es a menudo tonto.

Realmente tiene que mirar el problema en sí y preguntar si el parámetro no tiene sentido en el contexto de toda la interfaz, y realmente cuánta carga impone el parámetro adicional.

Pregunta C
fuente
1

Me temo que su colega puede tener un punto muy válido. Aunque su solución en realidad no es la mejor.

De su interfaz propuesta está claro que

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

está devolviendo instancias encontradas dentro de un intervalo de tiempo. Si los clientes actualmente no usan el parámetro final, eso no cambia el hecho de que esperan instancias encontradas dentro de un intervalo de tiempo. Simplemente significa que actualmente todos los clientes usan intervalos abiertos (desde el comienzo hasta la eternidad)

Entonces una mejor interfaz sería:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

Si proporciona un intervalo con un método de fábrica estático:

public static Interval startingAt(Date start) { return new Interval(start, null); }

entonces los clientes ni siquiera sentirán la necesidad de especificar una hora de finalización.

Al mismo tiempo, su interfaz transmite más correctamente lo que hace, ya getFooList(String, Date)que no comunica que se trata de un intervalo.

Tenga en cuenta que mi sugerencia se deriva de lo que el método hace actualmente, no de lo que debería o podría hacer en el futuro, y como tal, el principio YAGNI (que es realmente válido) no se aplica aquí.

Bowmore
fuente
0

Agregar un parámetro no utilizado es confuso. La gente podría llamar a ese método suponiendo que esta característica funcione.

No lo agregaría. Es trivial agregarlo luego usando una refactorización y arreglando mecánicamente los sitios de llamadas. En un lenguaje escrito estáticamente, esto es fácil de hacer. No es necesario agregarlo ansiosamente.

Si no es una razón para tener ese parámetro puede evitar la confusión: Añadir una aserción en la aplicación de esa interfaz para hacer cumplir que este parámetro se pasa como el valor predeterminado. Si alguien usa accidentalmente ese parámetro al menos, notará inmediatamente durante la prueba que esta característica no está implementada. Esto elimina el riesgo de deslizar un error en la producción.

usr
fuente