¿Está bien tener objetos que se emitan, incluso si contamina la API de sus subclases?

33

Tengo una clase base, Base. Tiene dos subclases Sub1y Sub2. Cada subclase tiene algunos métodos adicionales. Por ejemplo, Sub1tiene Sandwich makeASandwich(Ingredients... ingredients)y Sub2tiene boolean contactAliens(Frequency onFrequency).

Dado que estos métodos toman parámetros diferentes y hacen cosas completamente diferentes, son completamente incompatibles, y no puedo usar el polimorfismo para resolver este problema.

Baseproporciona la mayor parte de la funcionalidad, y tengo una gran colección de Baseobjetos. Sin embargo, todos los Baseobjetos son a Sub1o a Sub2, y a veces necesito saber cuáles son.

Parece una mala idea hacer lo siguiente:

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

Así que se me ocurrió una estrategia para evitar esto sin lanzar. Baseahora tiene estos métodos:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

Y, por supuesto, Sub1implementa estos métodos como

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

Y los Sub2implementa de la manera opuesta.

Lamentablemente, ahora Sub1y Sub2tienen estos métodos en su propia API. Entonces puedo hacer esto, por ejemplo, en Sub1.

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

De esta manera, si se sabe que el objeto es solo a Base, estos métodos no están en desuso y se pueden usar para "lanzarse" a un tipo diferente para que yo pueda invocar los métodos de la subclase. Esto me parece elegante de alguna manera, pero por otro lado, estoy abusando de las anotaciones obsoletas como una forma de "eliminar" métodos de una clase.

Dado que una Sub1instancia realmente es una Base, tiene sentido usar la herencia en lugar de la encapsulación. ¿Lo que estoy haciendo bien? ¿Hay una mejor manera de resolver este problema?

descifrador de códigos
fuente
12
Las 3 clases ahora tienen que saber el uno del otro. Agregar Sub3 implicaría muchos cambios de código, y agregar Sub10 sería francamente doloroso
Dan Pichelman
15
Sería de gran ayuda si nos proporcionara un código real. Hay situaciones en las que es apropiado tomar decisiones basadas en la clase particular de algo, pero es imposible saber si estás justificado en lo que estás haciendo con tales ejemplos artificiales. Para lo que sea que valga, lo que quieres es un visitante o una unión etiquetada .
Doval
1
Lo siento, pensé que facilitaría las cosas dar ejemplos simplificados. Tal vez publique una nueva pregunta con un alcance más amplio de lo que quiero hacer.
codebreaker
12
Simplemente está reimplementando el casting y instanceof, de una manera que requiere mucho tipeo, es propenso a errores y dificulta agregar más subclases.
user253751
55
Si Sub1y Sub2no se pueden usar indistintamente, ¿por qué los trata como tal? ¿Por qué no realizar un seguimiento de sus 'fabricantes de sándwiches' y 'contactores alienígenas' por separado?
Pieter Witvoet

Respuestas:

27

No siempre tiene sentido agregar funciones a la clase base, como se sugiere en algunas de las otras respuestas. Agregar demasiadas funciones de casos especiales puede dar como resultado la unión de componentes que de otro modo no estarían relacionados.

Por ejemplo, podría tener una Animalclase, con Caty Dogcomponentes. Si quiero ser capaz de imprimir, o los mostrará en la interfaz gráfica de usuario que puede ser excesiva para que añada renderToGUI(...)y sendToPrinter(...)de la clase base.

El enfoque que está utilizando, utilizando verificaciones de tipo y modelos, es frágil, pero al menos mantiene las preocupaciones separadas.

Sin embargo, si te encuentras haciendo este tipo de verificaciones / lanzamientos con frecuencia, entonces una opción es implementar el patrón de visitante / doble despacho para ello. Se parece a esto:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Ahora tu código se convierte

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

El principal beneficio es que si agrega una subclase, obtendrá errores de compilación en lugar de casos que faltan silenciosamente. La nueva clase de visitante también se convierte en un buen objetivo para incorporar funciones. Por ejemplo, podría tener sentido para moverse getRandomIngredients()en ActOnBase.

También puede extraer la lógica de bucle: por ejemplo, el fragmento anterior podría convertirse

BaseVisitor.applyToArray(bases, new ActOnBase() );

Un poco más de masajes y el uso de lambdas y transmisión de Java 8 le permitirán llegar a

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Qué IMO tiene el aspecto más prolijo y conciso posible.

Aquí hay un ejemplo más completo de Java 8:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));
Michael Anderson
fuente
+1: Este tipo de cosas se usan comúnmente en lenguajes que tienen soporte de primera clase para tipos de suma y coincidencia de patrones, y es un diseño válido en Java, a pesar de que es sintácticamente muy feo.
Mankarse
@Mankarse Un poco de azúcar sintáctica puede hacer que esté muy lejos de ser feo; lo actualicé con un ejemplo.
Michael Anderson
Digamos hipotéticamente que el OP cambia de opinión y decide agregar a Sub3. ¿No tiene el visitante exactamente el mismo problema que instanceof? Ahora debe agregar onSub3al visitante y se rompe si lo olvida.
Radiodef
1
@Radiodef Una ventaja de usar el patrón de visitante en lugar de activar el tipo directamente es que una vez que ha agregado onSub3a la interfaz de visitante, obtiene un error de compilación en cada ubicación donde crea un nuevo visitante que aún no se ha actualizado. Por el contrario, la activación del tipo generará, en el mejor de los casos, un error de tiempo de ejecución, que puede ser más difícil de activar.
Michael Anderson
1
@FiveNine, si está contento de agregar ese ejemplo completo al final de la respuesta que podría ayudar a otros.
Michael Anderson
83

Desde mi perspectiva: tu diseño está equivocado .

Traducido al lenguaje natural, estás diciendo lo siguiente:

Dado que tenemos animals, hay catsy fish. animalstener propiedades, que son comunes a catsy fish. Pero eso no es suficiente: hay algunas propiedades que hacen diferentes cata partir fish, por lo tanto, que necesita subclase.

Ahora tiene el problema, que olvidó modelar el movimiento . Bueno. Eso es relativamente fácil:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Pero ese es un diseño incorrecto. La forma correcta sería:

for(Animal a : animals){
    animal.move()
}

Donde movese compartiría el comportamiento implementado de manera diferente por cada animal.

Dado que estos métodos toman parámetros diferentes y hacen cosas completamente diferentes, son completamente incompatibles, y no puedo usar el polimorfismo para resolver este problema.

Esto significa: su diseño está roto.

Mi recomendación: refactor Base, Sub1y Sub2.

Thomas Junk
fuente
8
Si ofreciera un código real, podría hacer recomendaciones.
Thomas Junk
9
@codebreaker Si acepta que su ejemplo no era bueno, le recomiendo aceptar esta respuesta y luego escribir una nueva pregunta. No revise la pregunta para tener un ejemplo diferente o las respuestas existentes no tendrán sentido.
Moby Disk
16
@codebreaker Creo que esto "resuelve" el problema respondiendo la pregunta real . La verdadera pregunta es: ¿Cómo resuelvo mi problema? Refactorice su código correctamente. Refactor para que .move()o .attack()y debidamente abstracta thatparte - en lugar de tener .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home(), etc. ¿Cómo se refactoriza correctamente? Desconocido sin mejores ejemplos ... pero en general sigue siendo la respuesta correcta, a menos que el código de ejemplo y más detalles sugieran lo contrario.
WernerCD
11
@codebreaker Si su jerarquía de clases lo lleva a situaciones como esta, entonces, por definición , no tiene sentido
Patrick Collins
77
@codebreaker Relacionado con el último comentario de Crisfole, hay otra forma de verlo que podría ayudar. Por ejemplo, con el movevs fly/ run/ etc, eso se puede explicar en una oración como: Debería decirle al objeto qué hacer, no cómo hacerlo. En términos de accesores, como usted menciona es más como el caso real en un comentario aquí, debe hacer preguntas al objeto, no inspeccionar su estado.
Izkata
9

Es un poco difícil imaginar una circunstancia en la que tienes un grupo de cosas y quieres que hagan un sándwich o contacten a extraterrestres. En la mayoría de los casos en los que encuentre este tipo de conversión, operará con un tipo; por ejemplo, en clang, filtrará un conjunto de nodos para las declaraciones donde getAsFunction devuelve no nulo, en lugar de hacer algo diferente para cada nodo en la lista.

Puede ser que necesite realizar una secuencia de acción y en realidad no es relevante que los objetos que realizan la acción estén relacionados.

Entonces, en lugar de una lista de Base, trabaje en la lista de acciones

for (RandomAction action : actions)
   action.act(context);

dónde

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

Puede, si corresponde, hacer que Base implemente un método para devolver la acción, o cualquier otro medio que necesite para seleccionar la acción de las instancias en su lista base (ya que dice que no puede usar el polimorfismo, entonces, presumiblemente, la acción a tomar no es una función de la clase, sino alguna otra propiedad de las bases; de lo contrario, solo le daría a Base el método act (Context)

Pete Kirkham
fuente
2
Entiendes que mi tipo de métodos absurdos están diseñados para mostrar las diferencias en lo que las clases pueden hacer una vez que se conoce su subclase (y que no tienen nada que ver entre sí), pero creo que tener un Contextobjeto solo empeora las cosas. También podría estar pasando Objects a los métodos, lanzándolos y esperando que sean del tipo correcto.
codebreaker
1
@codebreaker realmente necesita aclarar su pregunta entonces: en la mayoría de los sistemas, habría una razón válida para llamar a un montón de funciones independientes de lo que hacen; por ejemplo para procesar reglas en un evento o realizar un paso en una simulación. Por lo general, dichos sistemas tienen un contexto en el que ocurren las acciones. Si 'getRandomIngredients' y 'getFrequency' no están relacionados, entonces no deberían estar en el mismo objeto, y aún necesita un enfoque diferente, como hacer que las acciones capturen la fuente de ingredientes o frecuencia.
Pete Kirkham
1
Tienes razón, y no creo que pueda salvar esta pregunta. Terminé eligiendo un mal ejemplo, así que probablemente solo publique otro. GetFrequency () y getIngredients () eran solo marcadores de posición. Probablemente debería haber puesto "..." como argumentos para hacer más evidente que no sabría cuáles son.
Codebreaker
Esta es la OMI la respuesta correcta. Comprenda que Contextno es necesario que sea una nueva clase, o incluso una interfaz. Por lo general, el contexto es solo la persona que llama, por lo que las llamadas son en forma action.act(this). Si getFrequency()y getRandomIngredients()son métodos estáticos, es posible que ni siquiera necesite un contexto. Así es como implementaría, digamos, una cola de trabajo, a la que se parece mucho su problema.
QuestionC
Además, esto es casi idéntico a la solución de patrón de visitante. La diferencia es que está implementando los métodos Visitor :: OnSub1 () / Visitor :: OnSub2 () como Sub1 :: act () / Sub2 :: act ()
QuestionC
4

¿Qué tal si sus subclases implementan una o más interfaces que definen lo que pueden hacer? Algo como esto:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Entonces tus clases se verán así:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

Y su bucle for se convertirá en:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Dos ventajas principales de este enfoque:

  • sin casting involucrado
  • puede hacer que cada subclase implemente tantas interfaces como desee, lo que proporciona cierta flexibilidad para futuros cambios.

PD: Perdón por los nombres de las interfaces, no se me ocurrió nada más genial en ese momento en particular: D.

Radu Murzea
fuente
3
Esto en realidad no resuelve el problema. Todavía necesita el reparto en el bucle for. (Si no lo hace, tampoco necesita el elenco en el ejemplo de OP).
Taemyr
2

El enfoque puede ser una buena en los casos en que casi cualquier tipo dentro de una familia , ya sea directamente utilizable como una implementación de alguna de las interfaces que cumple algún criterio o se puede utilizar para crear una implementación de la interfaz. Los tipos de colección integrados en mi humilde opinión se habrían beneficiado de este patrón, pero como no lo hacen, por ejemplo, inventaré una interfaz de colección BunchOfThings<T>.

Algunas implementaciones de BunchOfThingsson mutables; algunos no lo son. En muchos casos, un objeto que Fred podría querer contener algo que pueda usar como BunchOfThingsy sepa que nada más que Fred podrá modificarlo. Este requisito puede cumplirse de dos maneras:

  1. Fred sabe que tiene las únicas referencias a eso BunchOfThings, y no existe ninguna referencia externa a eso en BunchOfThingssus partes internas en ningún lugar del universo. Si nadie más tiene una referencia a BunchOfThingssus componentes internos, nadie más podrá modificarlo, por lo que se cumplirá la restricción.

  2. Ni el BunchOfThings, ni ninguno de sus componentes internos a los que existen referencias externas, pueden modificarse por cualquier medio. Si absolutamente nadie puede modificar a BunchOfThings, entonces la restricción será satisfecha.

Una forma de satisfacer la restricción sería copiar incondicionalmente cualquier objeto recibido (procesando recursivamente cualquier componente anidado). Otra sería probar si un objeto recibido promete inmutabilidad y, si no, hacer una copia del mismo, y hacer lo mismo con cualquier componente anidado. Una alternativa, que es más limpia que la segunda y más rápida que la primera, es ofrecer un AsImmutablemétodo que le pida a un objeto que haga una copia inmutable de sí mismo (utilizando AsImmutablecualquier componente anidado que lo admita).

También se pueden proporcionar métodos relacionados asDetached(para usar cuando el código recibe un objeto y no sabe si querrá mutarlo, en cuyo caso un objeto mutable debe reemplazarse por un nuevo objeto mutable, pero un objeto inmutable puede mantenerse como está), asMutable(en los casos en que un objeto sabe que contendrá un objeto devuelto anteriormente asDetached, es decir, una referencia no compartida a un objeto mutable o una referencia que se puede compartir a un objeto mutable), y asNewMutable(en los casos en que el código recibe un exterior referencia y sabe que va a querer mutar una copia de los datos allí; si los datos entrantes son mutables, no hay razón para comenzar haciendo una copia inmutable que se usará inmediatamente para crear una copia mutable y luego se abandonará).

Tenga en cuenta que, si bien los asXXmétodos pueden devolver tipos ligeramente diferentes, su función real es garantizar que los objetos devueltos satisfagan las necesidades del programa.

Super gato
fuente
0

Ignorando la cuestión de si tiene un buen diseño o no, y suponiendo que sea bueno o al menos aceptable, me gustaría considerar las capacidades de las subclases, no el tipo.

Por lo tanto, ya sea:


Transfiera algún conocimiento de la existencia de sándwiches y extraterrestres a la clase base, aunque sepa que algunas instancias de la clase base no pueden hacerlo. Impleméntelo en la clase base para generar excepciones y cambie el código a:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Luego, una o ambas subclases anulan canMakeASandwich(), y solo una implementa cada una de makeASandwich()y contactAliens().


Use interfaces, no subclases concretas, para detectar qué capacidades tiene un tipo. Deje la clase base sola y cambie el código a:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

o posiblemente (y siéntase libre de ignorar esta opción si no se adapta a su estilo, o para cualquier estilo Java que considere razonable):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Personalmente, generalmente no me gusta el último estilo de capturar una excepción medio esperada, debido al riesgo de detectar inapropiadamente una ClassCastExceptionsalida de getRandomIngredientso makeASandwich, pero YMMV.

Steve Jessop
fuente
2
Capturar ClassCastException es simplemente nuevo. Ese es todo el propósito de la instancia de.
Radiodef
@Radiodef: cierto, pero uno de los propósitos <es evitar la captura ArrayIndexOutOfBoundsException, y algunas personas deciden hacer eso también. Sin tener en cuenta el gusto ;-) Básicamente mi "problema" aquí es que trabajo en Python, donde el estilo preferido generalmente es que atrapar cualquier excepción es preferible a probar por adelantado si ocurrirá la excepción, no importa cuán simple sea la prueba avanzada. . Tal vez no valga la pena mencionar la posibilidad en Java.
Steve Jessop
@SteveJessop »Es más fácil pedir perdón que permiso« es el eslogan;)
Thomas Junk
0

Aquí tenemos un caso interesante de una clase base que se reduce a sus propias clases derivadas. Sabemos muy bien que esto normalmente es malo, pero si queremos decir que encontramos una buena razón, veamos y veamos cuáles pueden ser las limitaciones:

  1. Podemos cubrir todos los casos en la clase base.
  2. Ninguna clase derivada extranjera tendría que agregar un nuevo caso, nunca.
  3. Podemos vivir con la política para la cual llamar a estar bajo el control de la clase base.
  4. Pero sabemos por nuestra ciencia que la política de qué hacer en una clase derivada para un método que no está en la clase base es la de la clase derivada y no la clase base.

Si 4 entonces tenemos: 5. La política de la clase derivada siempre está bajo el mismo control político que la política de la clase base.

2 y 5 implican directamente que podemos enumerar todas las clases derivadas, lo que significa que no se supone que haya ninguna clase derivada externa.

Pero aquí está la cosa. Si son todos suyos, puede reemplazar el if con una abstracción que es una llamada a un método virtual (incluso si es una tontería) y deshacerse del if y self-cast. Por lo tanto, no lo hagas. Un mejor diseño está disponible.

Joshua
fuente
-1

Coloque un método abstracto en la clase base que haga una cosa en Sub1 y la otra en Sub2, y llame a ese método abstracto en sus bucles mixtos.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

Tenga en cuenta que esto puede requerir otros cambios dependiendo de cómo se definan getRandomIngredients y getFrequency. Pero, sinceramente, dentro de la clase que contacta a extraterrestres es probablemente un mejor lugar para definir getFrequency de todos modos.

Por cierto, sus métodos asSub1 y asSub2 son definitivamente una mala práctica. Si vas a hacer esto, hazlo lanzando. No hay nada que estos métodos te den que el casting no tenga.

Aleatorio832
fuente
2
Su respuesta sugiere que no leyó la pregunta por completo: el polimorfismo simplemente no lo cortará aquí. Hay varios métodos en cada uno de Sub1 y Sub2, estos son solo ejemplos, y "doAThing" realmente no significa nada. No corresponde a nada de lo que estaría haciendo. Además, en cuanto a su última oración: ¿qué tal la seguridad de tipo garantizada?
Codebreaker
@codebreaker: corresponde exactamente a lo que está haciendo en el bucle en el ejemplo. Si eso no tiene sentido, no escriba el bucle. Si no tiene sentido como método, no tiene sentido como un cuerpo de bucle.
Random832
1
Y sus métodos "garantizan" la seguridad del tipo de la misma manera que lo hace el lanzamiento: lanzando una excepción si el objeto es del tipo incorrecto.
Random832
Me doy cuenta de que elegí un mal ejemplo. El problema es que la mayoría de los métodos en las subclases se parecen más a getters que a métodos mutativos. Estas clases no hacen las cosas por sí mismas, solo proporcionan datos, principalmente. El polimorfismo no me ayudará allí. Estoy tratando con productores, no con consumidores. Además: lanzar una excepción es una garantía de tiempo de ejecución, que no es tan buena como el tiempo de compilación.
codebreaker
1
Mi punto es que su código solo proporciona una garantía de tiempo de ejecución también. No hay nada que impida que alguien no compruebe isSub2 antes de llamar a asSub2.
Random832
-2

Puede que haya empujado tanto makeASandwichy contactAliensen la clase base y luego ponerlas en práctica con implementaciones ficticias. Dado que los tipos / argumentos de retorno no pueden resolverse en una clase base común.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

Hay desventajas obvias para este tipo de cosas, como lo que eso significa para el contrato del método y lo que puede y no puede inferir sobre el contexto del resultado. No se puede pensar, ya que no pude hacer un sándwich, los ingredientes se agotaron en el intento, etc.

Firmar
fuente
1
Pensé en hacer esto, pero viola el Principio de sustitución de Liskov y no es tan agradable trabajar con él, por ejemplo, cuando escribe "sub1". y aparece el autocompletado, ves makeASandwich pero no contactas aAliens.
codebreaker
-5

Lo que estás haciendo es perfectamente legítimo. No presten atención a los detractores que simplemente reiteran el dogma porque lo leen en algunos libros. Dogma no tiene lugar en la ingeniería.

He empleado el mismo mecanismo un par de veces, y puedo decir con confianza que el tiempo de ejecución de Java también podría haber hecho lo mismo en al menos un lugar en el que puedo pensar, mejorando así el rendimiento, la usabilidad y la legibilidad del código que lo usa

Tomemos, por ejemplo java.lang.reflect.Member, cuál es la base de java.lang.reflect.Fieldy java.lang.reflect.Method. (La jerarquía real es un poco más complicada que eso, pero eso es irrelevante). Los campos y los métodos son animales muy diferentes: uno tiene un valor que puede obtener o establecer, mientras que el otro no tiene tal cosa, pero puede invocarse con una serie de parámetros y puede devolver un valor. Por lo tanto, los campos y los métodos son miembros, pero las cosas que puede hacer con ellos son tan diferentes entre sí como hacer sándwiches frente a contactar con extraterrestres.

Ahora, cuando escribimos código que usa la reflexión, a menudo tenemos un Memberen nuestras manos, y sabemos que es un a Methodo un Field(o, raramente, algo más) y, sin embargo, tenemos que hacer todo lo tedioso instanceofpara descubrir con precisión qué es y luego tenemos que lanzarlo para obtener una referencia adecuada. (Y esto no solo es tedioso, sino que tampoco funciona muy bien). La Methodclase podría haber implementado muy fácilmente el patrón que está describiendo, facilitando así la vida de miles de programadores.

Por supuesto, esta técnica solo es viable en jerarquías pequeñas y bien definidas de clases estrechamente acopladas de las que usted tiene (y siempre tendrá) control de nivel de origen: no desea hacer tal cosa si su jerarquía de clases es Es probable que sea extendido por personas que no están en libertad de refactorizar la clase base.

Así es como lo que he hecho difiere de lo que has hecho:

  • La clase base proporciona una implementación predeterminada para toda la asDerivedClass()familia de métodos, haciendo que cada uno de ellos regrese null.

  • Cada clase derivada solo anula uno de los asDerivedClass()métodos, regresando en thislugar de null. No anula nada del resto, ni quiere saber nada sobre ellos. Por lo tanto, no IllegalStateExceptionse arrojan s.

  • La clase base también proporciona finalimplementaciones para toda la isDerivedClass()familia de métodos, codificados de la siguiente manera: De return asDerivedClass() != null; esta manera, se minimiza el número de métodos que deben ser anulados por las clases derivadas.

  • No he estado usando @Deprecatedeste mecanismo porque no lo pensé. Ahora que me diste la idea, la pondré en práctica, ¡gracias!

C # tiene un mecanismo relacionado incorporado mediante el uso de la aspalabra clave. En C # puede decir DerivedClass derivedInstance = baseInstance as DerivedClassy obtendrá una referencia a DerivedClasssi su baseInstancepertenecía a esa clase o nullno. Esto (teóricamente) funciona mejor que lo isseguido por la conversión ( ises la palabra clave de C # mejor nombrada para instanceof), pero el mecanismo personalizado para el que hemos estado trabajando a mano funciona aún mejor: el par de instanceofoperaciones de lanzamiento y lanzamiento de Java también ya que el asoperador de C # no funciona tan rápido como la llamada de método virtual único de nuestro enfoque personalizado.

Por la presente, propongo que esta técnica debe declararse como un patrón y que se debe encontrar un buen nombre para ello.

Gee, gracias por los votos negativos!

Un resumen de la controversia, para salvarlo de la molestia de leer los comentarios:

La objeción de la gente parece ser que el diseño original era incorrecto, lo que significa que nunca debería haber clases muy diferentes que se derivan de una clase base común, o que incluso si lo hace, el código que utiliza dicha jerarquía nunca debería estar en la posición de tener una referencia base y la necesidad de descubrir la clase derivada. Por lo tanto, dicen, el mecanismo de autoelaboración propuesto por esta pregunta y por mi respuesta, que mejora el uso del diseño original, nunca debería haber sido necesario en primer lugar. (Realmente no dicen nada sobre el mecanismo de auto-fundición en sí, solo se quejan de la naturaleza de los diseños a los que se debe aplicar el mecanismo).

Sin embargo, en el ejemplo anterior que han ya demostrado que los creadores de la ejecución de Java, en efecto, elegir el diseño precisamente tal para el java.lang.reflect.Member, Field, Methodjerarquía, y en los comentarios a continuación que también muestran que los creadores de la C # tiempo de ejecución llegado independientemente a una equivalente diseño para el System.Reflection.MemberInfo, FieldInfo, MethodInfojerarquía. Por lo tanto, estos son dos escenarios diferentes del mundo real que se encuentran justo debajo de la nariz de todos y que tienen soluciones demostrablemente viables utilizando precisamente tales diseños.

A eso se reducen todos los siguientes comentarios. El mecanismo de auto-fundición apenas se menciona.

Mike Nakis
fuente
13
Es legítimo si te diseñaste en una caja, claro. Ese es el problema con muchos de estos tipos de preguntas. Las personas toman decisiones en otras áreas del diseño que obligan a este tipo de soluciones hackeadas cuando la solución real es descubrir por qué terminaron en esta situación en primer lugar. Un diseño decente no te llevará aquí. Actualmente estoy viendo una aplicación SLOC 200K y no veo en ningún lado que necesitáramos hacer esto. Entonces no es solo algo que la gente lee en un libro. No entrar en este tipo de situaciones es simplemente el resultado final de seguir buenas prácticas.
Dunk
3
@Dunk Acabo de demostrar que existe la necesidad de tal mecanismo, por ejemplo, en la java.lang.reflection.Memberjerarquía. Los creadores del tiempo de ejecución de Java se metieron en este tipo de situación, ¿no supongo que dirían que se diseñaron en una caja, o los culpan por no seguir algún tipo de mejores prácticas? Entonces, lo que quiero decir aquí es que una vez que tienes este tipo de situación, este mecanismo es una buena manera de mejorar las cosas.
Mike Nakis
66
@MikeNakis Los creadores de Java han tomado muchas malas decisiones, por lo que solo eso no dice mucho. En cualquier caso, usar un visitante sería mejor que hacer una prueba ad-hoc-luego-emitir.
Doval
3
Además, el ejemplo es defectuoso. Hay una Memberinterfaz, pero solo sirve para verificar los calificadores de acceso. Por lo general, obtienes una lista de métodos o propiedades y la usas directamente (sin mezclarlos, por lo que no List<Member>aparece), por lo que no necesitas lanzar. Tenga en cuenta que Classno proporciona un getMembers()método. Por supuesto, un programador despistado podría crear el List<Member>, pero eso sería hacer el menor sentido de que es una List<Object>y la adición de algunos Integero Stringde ella.
SJuan76
55
@MikeNakis "Mucha gente lo hace" y "Persona famosa lo hace" no son argumentos reales. Los antipatrones son malas ideas y ampliamente utilizados por definición, sin embargo, esas excusas justificarían su uso. Dé razones reales para usar un diseño u otro. Mi problema con la conversión ad-hoc es que cada usuario de su API tiene que hacer la conversión correcta cada vez que lo hace. Un visitante solo necesita ser correcto una vez. Ocultar el casting detrás de algunos métodos y regresar en nulllugar de una excepción no lo hace mucho mejor. En igualdad de condiciones, el visitante está más seguro mientras hace el mismo trabajo.
Doval