Cuándo usar genéricos en el diseño de la interfaz

11

Tengo algunas interfaces que pretendo que terceros implementen en el futuro, y yo mismo proporciono una implementación básica. Solo usaré un par para mostrar el ejemplo.

Actualmente, se definen como

Articulo:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

Ahora, Itemy ItemStackFactorypuedo prever absolutamente que un tercero necesitará extenderlo en el futuro. ItemStackContainerTambién podría extenderse en el futuro, pero no de manera que pueda prever, fuera de mi implementación predeterminada proporcionada.

Ahora, estoy tratando de hacer esta biblioteca lo más robusta posible; esto todavía está en las primeras etapas (pre-pre-alfa), por lo que puede ser un acto de ingeniería excesiva (YAGNI). ¿Es este un lugar apropiado para usar genéricos?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

Y

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

Me temo que esto puede hacer que las implementaciones y el uso sean más difíciles de leer y comprender; Creo que es la recomendación evitar anidar genéricos siempre que sea posible.

Zymus
fuente
1
De alguna manera, se siente como si estuvieras exponiendo los detalles de implementación. ¿Por qué la persona que llama necesita trabajar y conocer / preocuparse por las pilas en lugar de las cantidades simples?
cHao
Eso es algo en lo que no había pensado. Eso proporciona una buena idea de este problema específico. Me aseguraré de hacer los cambios en mi código.
Zymus

Respuestas:

9

Utiliza genéricos en su interfaz cuando es probable que su implementación también sea genérica.

Por ejemplo, cualquier estructura de datos que pueda aceptar objetos arbitrarios es un buen candidato para una interfaz genérica. Ejemplos: List<T>y Dictionary<K,V>.

Cualquier situación en la que desee mejorar la seguridad de tipos de forma generalizada es un buen candidato para los genéricos. A List<String>es una lista que solo opera en cadenas.

Cualquier situación en la que desee aplicar SRP de forma generalizada y evitar métodos específicos de tipo es un buen candidato para los genéricos. Por ejemplo, un PersonDocument, PlaceDocument y ThingDocument se pueden reemplazar con a Document<T>.

El patrón Abstract Factory es un buen caso de uso para un genérico, mientras que un Método Factory común simplemente crearía objetos que heredarían de una interfaz común y concreta.

Robert Harvey
fuente
Realmente no estoy de acuerdo con la idea de que las interfaces genéricas deben combinarse con implementaciones genéricas. Declarar un parámetro de tipo genérico en una interfaz y luego refinarlo o instanciarlo en varias implementaciones es una técnica poderosa. Es especialmente común en Scala. Interfaces de cosas abstractas; Las clases las especializamos.
Benjamin Hodgson
@BenjaminHodgson: eso solo significa que estás produciendo implementaciones en una interfaz genérica para tipos específicos. El enfoque general sigue siendo genérico, aunque algo menos.
Robert Harvey
Estoy de acuerdo. Tal vez no entendí bien su respuesta: parecía estar desaconsejando ese tipo de diseño.
Benjamin Hodgson
@BenjaminHodgson: ¿No se escribiría un Método de Fábrica contra una interfaz concreta , en lugar de una genérica? (aunque una Fábrica abstracta sería genérica)
Robert Harvey
Creo que estamos de acuerdo :) Probablemente escribiría el ejemplo del OP como algo así class StackFactory { Stack<T> Create<T>(T item, int count); }. Puedo escribir un ejemplo de lo que quise decir con "refinar un parámetro de tipo en una subclase" en una respuesta si desea más aclaraciones, aunque la respuesta solo estaría relacionada tangencialmente con la pregunta original.
Benjamin Hodgson
8

Su plan de cómo introducir generalidad a su interfaz me parece correcto. Sin embargo, su pregunta de si sería una buena idea o no requiere una respuesta algo más complicada.

A lo largo de los años, entrevisté a varios candidatos para puestos de Ingeniería de Software en nombre de las empresas para las que trabajé, y me di cuenta de que la mayoría de los solicitantes de empleo se sienten cómodos con el uso de clases genéricas (por ejemplo, clases de colección estándar), pero no con escribir clases genéricas. La mayoría nunca ha escrito una sola clase genérica en su carrera, y parece que se perderían por completo si se les pidiera que escribieran una. Por lo tanto, si fuerza el uso de genéricos sobre cualquier persona que desee implementar su interfaz o extender sus implementaciones base, puede estar posponiendo a las personas.

Sin embargo, lo que puede intentar es proporcionar versiones genéricas y no genéricas de sus interfaces y sus implementaciones básicas, de modo que las personas que no hacen genéricos puedan vivir felices en su mundo estrecho y pesado, mientras que las personas que Los genéricos pueden cosechar los beneficios de su comprensión más amplia del idioma.

Mike Nakis
fuente
3

Ahora, Itemy ItemStackFactorypuedo prever absolutamente que un tercero necesitará extenderlo en el futuro.

Ahí está tu decisión tomada por ti. Si no proporciona genéricos, deberán actualizar su implementación Itemcada vez que usen:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

Al menos debe hacer ItemStackgenérico en la forma que sugiere. El beneficio más profundo de los genéricos es que casi nunca necesitas lanzar nada, y ofrecer código que obliga a los usuarios a lanzar es en mi humilde opinión un delito penal.

OldCurmudgeon
fuente
2

Wow ... Tengo una opinión completamente diferente sobre esto.

Puedo prever absolutamente alguna necesidad de terceros

Esto es un error. No deberías escribir código "para el futuro".

Además, las interfaces están escritas de arriba hacia abajo. No miras una clase y dices "Oye, esta clase podría implementar totalmente una interfaz y luego puedo usar esa interfaz en otro lugar también". Ese es un enfoque incorrecto, porque solo la clase que usa una interfaz realmente sabe cuál debería ser la interfaz. En su lugar, debería pensar: "En este lugar, mi clase necesita una dependencia. Permítanme definir una interfaz para esa dependencia. Cuando termine de escribir esta clase, escribiré una implementación de esa dependencia". Es irrelevante si alguna vez alguien reutilizará su interfaz y sustituirá su implementación predeterminada por la suya. Puede que nunca lo hagan. Esto no significa que la interfaz fuera inútil. Hace que su código siga el principio de Inversión de control, lo que hace que su código sea muy extensible en muchos lugares "

anton1980
fuente
0

Creo que usar genéricos en su situación es demasiado complejo.

Si elige la versión genérica de las interfaces, el diseño indica que

  1. ItemStackContainer <A> contiene un solo tipo de pila, A. (es decir, ItemStackContainer no puede contener varios tipos de pila).
  2. ItemStack está dedicado a un tipo específico de artículo. Por lo tanto, no hay ningún tipo de abstracción de todo tipo de pilas.
  3. Debe definir un ItemStackFactory específico para cada tipo de elementos. Se considera demasiado fino como Factory Pattern.
  4. Escriba código más difícil como ItemStack <? extiende Item>, disminuyendo la mantenibilidad.

Estas suposiciones y restricciones implícitas son cosas muy importantes para decidir cuidadosamente. Entonces, especialmente en la etapa inicial, estos diseños prematuros no deben ser introducidos. Simple, fácil de entender, se desea un diseño común.

Akio Takahashi
fuente
0

Diría que depende principalmente de esta pregunta: si alguien necesita ampliar la funcionalidad de Itemy ItemStackFactory,

  • ¿simplemente sobrescribirán los métodos y, por lo tanto, las instancias de sus subclases se usarán al igual que las clases base, y se comportarán de manera diferente?
  • ¿o agregarán miembros y usarán las subclases de manera diferente?

En el primer caso, no hay necesidad de genéricos, en el segundo, probablemente sí.

Michael Borgwardt
fuente
0

Tal vez, puede tener una jerarquía de interfaz, donde Foo es una interfaz, Bar es otra. Siempre que un tipo debe ser ambos, es un FooAndBar.

Para evitar un diseño excesivo, solo haga el tipo FooAndBar cuando sea necesario y mantenga una lista de interfaces que nunca extiendan nada.

Esto es mucho más cómodo para la mayoría de los programadores (a la mayoría les gusta su verificación de tipos o les gusta nunca tratar con la escritura estática de ningún tipo). Muy pocas personas han escrito su propia clase de genéricos.

También como nota: las interfaces tratan sobre las posibles acciones que se pueden ejecutar. En realidad deberían ser verbos, no sustantivos.

Akash Patel
fuente
Esta podría ser una alternativa al uso de genéricos, pero la pregunta original se refería a cuándo usar genéricos sobre lo que sugirió. ¿Puede mejorar su respuesta para sugerir que los genéricos nunca son necesarios, pero el uso de múltiples interfaces es la respuesta para todos?
Jay Elston