java - simple - ¿Cómo reducir el código utilizando una superclase?
super en java (10)
Aquí hay una solución, bastante parecida a la de Slartidan, pero que utiliza el setter
con el estilo del constructor, evitando las variables dog
y cat
public class Dog extends Animal
{
// stuff
Dog setD(...)
{
//...
return this;
}
Dog setE(...)
{
//...
return this;
}
}
public class Cat extends Animal
{
// stuff
Cat setF(...)
{
//...
return this;
}
Cat setG(...)
{
//...
return this;
}
}
Animal animal = condition ?
new Dog().setD(..).setE(..) :
new Cat().setF(..).setG(..);
animal.setA(..);
animal.setB(..);
animal.setC(..);
listAnimal.add(animal);
Me gustaría refactorizar un código que actualmente consiste en una superclase y dos subclases.
Estas son mis clases:
public class Animal {
int a;
int b;
int c;
}
public class Dog extends Animal {
int d;
int e;
}
public class Cat extends Animal {
int f;
int g;
}
Este es mi código actual:
ArrayList<Animal> listAnimal = new ArrayList<>();
if (condition) {
Dog dog = new Dog();
dog.setA(..);
dog.setB(..);
dog.setC(..);
dog.setD(..);
dog.setE(..);
listAnimal.add(dog);
} else {
Cat cat = new Cat();
cat.setA(..);
cat.setB(..);
cat.setC(..);
cat.setF(..);
cat.setG(..);
listAnimal.add(cat);
}
¿Cómo puedo refactorizar el código con respecto a los atributos comunes?
Me gustaría algo así:
Animal animal = new Animal();
animal.setA(..);
animal.setB(..);
animal.setC(..);
if (condition) {
Dog anim = (Dog) animal; //I know it doesn''t work
anim.setD(..);
anim.setE(..);
} else {
Cat anim = (Cat) animal; //I know it doesn''t work
anim.setF(..);
anim.setG(..);
}
listAnimal.add(anim);
Como alternativa, puede hacer que las partes "Animales" de su perro y gato sean una entidad independiente que esté disponible a través de la interfaz "Animalian". Al hacer esto, creará primero el estado común y luego lo proporcionará al constructor específico de la especie en el momento en que sea necesario.
public class Animal {
int a;
int b;
int c;
}
public interface Animalian {
Animal getAnimal();
}
public class Dog implements Animalian {
int d;
int e;
Animal animal;
public Dog(Animal animal, int d, int e) {
this.animal = animal;
this.d = d;
this.e = e;
}
public Animal getAnimal() {return animal};
}
public class Cat implements Animalian {
int f;
int g;
Animal animal;
public Cat(Animal animal, int f, int g) {
this.animal = animal;
this.f = f;
this.g = g;
}
public Animal getAnimal() {return animal};
}
Ahora para crear animales:
Animal animal = new Animal();
animal.setA(..);
animal.setB(..);
animal.setC(..);
if (condition) {
listAnimalian.add(new Dog(animal, d, e));
} else {
listAnimalian.add(new Cat(animal, f, g));
}
La razón para hacer esto es " favorecer la composición sobre la herencia ". Quiero expresar que esta es simplemente una forma diferente de resolver el problema planteado. No significa que la composición deba favorecerse sobre la herencia en todo momento. Depende del ingeniero determinar la solución correcta para el contexto en el que surge el problema.
Hay mucha lectura sobre este tema .
Consideraría una búsqueda / registro dinámico de capacidades / características: vuelo / natación.
La pregunta es si esto se ajusta a tu uso: en lugar de Flying & Swimming, toma Bird and Fish.
Depende de si las propiedades agregadas son exclusivas (Dog / Cat) o aditivas (Flying / Swimming / Mammal / Insect / EggLaying / ...). Este último es más para una búsqueda utilizando un mapa.
interface Fish { boolean inSaltyWater(); }
interface Bird { int wingSpan(); setWingSpan(int span); }
Animal animal = ...
Optional<Fish> fish = animal.as(Fish.class);
fish.ifPresent(f -> System.out.println(f.inSaltyWater()? "seafish" : "riverfish"));
Optional<Bird> bird = animal.as(Bird.class);
bird.ifPresent(b-> b.setWingSpan(6));
Animal
no necesita implementar ninguna interfaz, pero puedes buscar (buscar o quizás) las capacidades. Esto es extensible en el futuro, dinámico: puede cambiar en tiempo de ejecución.
Implementación como
private Map<Class<?>, ?> map = new HashMap<>();
public <T> Optional<T> as(Class<T> type) {
return Optional.ofNullable(type.cast(map.get(type)));
}
<S> void register(Class<S> type, S instance) {
map.put(type, instance);
}
La implementación realiza una conversión dinámica segura, ya que el registro garantiza el llenado seguro de las entradas (clave, valor).
Animal flipper = new Animal();
flipper.register(new Fish() {
@Override
public boolean inSaltyWater() { return true; }
});
Considere hacer que sus clases sean inmutables (Artículo 3 de la tercera edición de Java vigente). Si todos los parámetros son necesarios, utilice un constructor o un método de fábrica estático (Artículo 1 de la 3ª edición de Java vigente). Si hay parámetros obligatorios y opcionales, use el patrón de creación (Artículo 2 de la 3ª edición de Java vigente).
El proceso de construir un gato o un perro es complejo, ya que hay muchos campos involucrados. Ese es un buen caso para el patrón de constructor .
Mi idea es escribir un constructor para cada tipo y organizar las relaciones entre ellos. Podría ser composición o herencia.
-
AnimalBuilder
construye un objetoAnimal
general y administra los campos a,b
,c
-
CatBuilder
toma unAnimalBuilder
(o lo extiende) y continúa construyendo un objetoCat
que administra los camposf
,g
-
DogBuilder
toma unAnimalBuilder
(o lo extiende) y continúa construyendo un objetoDog
que administra los camposd
,e
Si no desea crear constructores, considere la posibilidad de introducir un método de fábrica estático con un nombre significativo para cada subclase:
Animal animal = condition ? Dog.withDE(4, 5) : Cat.withFG(6, 7);
// populate animal''s a, b, c
listAnimal.add(animal);
Simplificaría la construcción y la haría menos verbosa y más legible.
Esto es lo que me gustaría proponer:
import java.util.ArrayList;
import java.util.List;
class Animal {
int a;
int b;
int c;
public Animal setA(int a) {
this.a = a;
return this;
}
public Animal setB(int b) {
this.b = b;
return this;
}
public Animal setC(int c) {
this.c = c;
return this;
}
}
class Dog extends Animal {
int d;
int e;
public Dog setD(int d) {
this.d = d;
return this;
}
public Dog setE(int e) {
this.e = e;
return this;
}
}
class Cat extends Animal {
int f;
int g;
public Cat setF(int f) {
this.f = f;
return this;
}
public Cat setG(int g) {
this.g = g;
return this;
}
}
class Scratch {
public static void main(String[] args) {
List<Animal> listAnimal = new ArrayList();
boolean condition = true;
Animal animal;
if (condition) {
animal = new Dog()
.setD(4)
.setE(5);
} else {
animal = new Cat()
.setF(14)
.setG(15);
}
animal.setA(1)
.setB(2)
.setC(3);
listAnimal.add(animal);
System.out.println(listAnimal);
}
}
Algunos puntos notables:
- Uso de la interfaz de la Lista en la declaración
List<Animal> listAnimal
- Uso de la interfaz animal mientras que la creación del objeto
Animal animal;
- clase
abstract
animal - Setters devolviendo
this
para hacer el código más limpio. O tendrías que usar código comoanimal.setD(4);
animal.setE(5);
De esta manera podemos utilizar la interfaz Animal y establecer los atributos comunes una vez. Espero que esto ayude.
Muchas grandes sugerencias aquí. Usaría mi patrón de constructor favorito personal (pero con un sabor de herencia añadido):
public class Animal {
int a;
int b;
int c;
public Animal() {
}
private <T> Animal(Builder<T> builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
public static class Builder<T> {
Class<T> builderClass;
int a;
int b;
int c;
public Builder(Class<T> builderClass) {
this.builderClass = builderClass;
}
public T a(int a) {
this.a = a;
return builderClass.cast(this);
}
public T b(int b) {
this.b = b;
return builderClass.cast(this);
}
public T c(int c) {
this.c = c;
return builderClass.cast(this);
}
public Animal build() {
return new Animal(this);
}
}
// getters and setters goes here
}
public class Dog extends Animal {
int d;
int e;
private Dog(DogBuilder builder) {
this.d = builder.d;
this.e = builder.e;
}
public static class DogBuilder extends Builder<DogBuilder> {
int d;
int e;
public DogBuilder() {
super(DogBuilder.class);
}
public DogBuilder d(int d) {
this.d = d;
return this;
}
public DogBuilder e(int e) {
this.e = e;
return this;
}
public Dog build() {
return new Dog(this);
}
}
// getters and setters goes here
}
public class Cat extends Animal {
int f;
int g;
private Cat(CatBuilder builder) {
this.f = builder.f;
this.g = builder.g;
}
public static class CatBuilder extends Builder<CatBuilder> {
int f;
int g;
public CatBuilder() {
super(CatBuilder.class);
}
public CatBuilder f(int f) {
this.f = f;
return this;
}
public CatBuilder g(int g) {
this.g = g;
return this;
}
public Cat build() {
return new Cat(this);
}
}
// getters and setters goes here
}
public class TestDrive {
public static void main(String[] args) {
Boolean condition = true;
ArrayList<Animal> listAnimal = new ArrayList<>();
if (condition) {
Dog dogA = new Dog.DogBuilder().a(1).b(2).c(3).d(4).e(5).build();
Dog dogB = new Dog.DogBuilder().d(4).build();
listAnimal.add(dogA);
listAnimal.add(dogB);
} else {
Cat catA = new Cat.CatBuilder().b(2).f(6).g(7).build();
Cat catB = new Cat.CatBuilder().g(7).build();
listAnimal.add(catA);
listAnimal.add(catB);
}
Dog doggo = (Dog) listAnimal.get(0);
System.out.println(doggo.d);
}
}
Nota: el constructor Animal.Builder
toma Class builderClass
como argumento genérico. Convierte la instancia actual del objeto a esta clase cuando regrese.
Tu idea de tener una variable de tipo Animal
es buena. Pero también debes asegurarte de usar el constructor correcto:
Animal animal; // define a variable for whatever animal we will create
if (condition) {
Dog dog = new Dog(); // create a new Dog using the Dog constructor
dog.setD(..);
dog.setE(..);
animal = dog; // let both variables, animal and dog point to the new dog
} else {
Cat cat = new Cat();
cat.setF(..);
cat.setG(..);
animal = cat;
}
animal.setA(..); // modify either cat or dog using the animal methods
animal.setB(..);
animal.setC(..);
listAnimal.add(animal);
Consejo: si un animal es siempre un gato o un perro, considera hacer un animal abstract
. Luego, el compilador se quejará automáticamente cada vez que intentes hacer un new Animal()
.
Refactoriza tu código a:
ArrayList<Animal> listAnimal = new ArrayList();
//Other code...
if (animalIsDog) {
addDogTo(listAnimal, commonAttribute, dogSpecificAttribute);
} else {
addCatTo(listAnimal, commonAttribute, catSpecificAttribute);
}
Beneficios con el nuevo código:
- Complejidad oculta : ha ocultado la complejidad y ahora tendrá que buscar un código más pequeño, escrito casi en inglés, cuando vuelva a visitar el código más tarde.
Pero ahora, los métodos addDogTo
y addCatTo
deberían escribirse. Así es como se verían:
private void addDogTo(ArrayList<Animal> listAnimal,
AnimalAttribute generalAttribute,
DogAttribute specificAttribute) {
var dog = createDog(commonAttribute, specificAttribute);
listAnimal.add(dog);
}
private void addCatTo(ArrayList<Animal> listAnimal,
AnimalAttribute generalAttribute,
CatAttribute specificAttribute) {
var cat = createCat(commonAttribute, specificAttribute);
listAnimal.add(cat);
}
Beneficios:
- Complejidad oculta ;
- Ambos métodos son privados : Esto significa que solo se pueden llamar desde la clase. Por lo tanto, puede dejar de revisar la entrada en busca de valores nulos, ya que la persona que llama (que está dentro de la clase) debe tener cuidado de no pasar datos falsos a sus propios miembros.
Esto significa que ahora necesitamos tener los métodos createDog
y createCat
en su lugar. Así es como escribiría esos métodos:
private Dog createDog(AnimalAttribute generalAttribute,
DogAttribute specificAttribute) {
var dog = new Dog(generalAttribute, specificAttribute);
return dog;
}
private Cat createCat(AnimalAttribute generalAttribute,
CatAttribute specificAttribute) {
var cat = new Cat(generalAttribute, specificAttribute);
return cat;
}
Beneficios:
- Complejidad oculta ;
- Ambos métodos son privados .
Ahora, para el código escrito anteriormente, tendrá que escribir constructores para Cat
y Dog
que tomen los atributos comunes y los atributos específicos para la construcción del objeto. Eso puede parecer:
public Dog(AnimalAttribute generalAttribute,
DogAttribute specificAttribute)
: base (generalAttribute) {
this.d = specificAttribute.getD();
this.e = specificAttribute.getE();
}
y,
public Cat(AnimalAttribute generalAttribute,
CatAttribute specificAttribute)
: base (generalAttribute) {
this.f = specificAttribute.getF();
this.g = specificAttribute.getG();
}
Beneficios:
- El código es DRY: ambos constructores llaman al método de superclase con
generalAttributes
y se ocupan de los atributos comunes de ambos objetos de subclase; - Se conserva todo el objeto: en lugar de llamar a un constructor y pasarle 20 mil parámetros, simplemente se le pasa 2, a saber. Objeto de atributo animal general y objeto de atributo animal específico. Estos dos parámetros contienen el resto de los atributos y se desajustan dentro del constructor cuando es necesario.
Finalmente, el constructor de tu Animal
se verá como:
public Animal(AnimalAttribute attribute) {
this.a = attribute.getA();
this.b = attribute.getB();
this.c = attribute.getC();
}
Beneficios:
- Todo el objeto se conserva ;
Por el bien de la finalización:
-
AnimalAttribute
/DogAttribute
/CatAttribute
solo tienen algunos campos y losDogAttribute
yCatAttribute
para esos campos; - Estos campos son los datos necesarios para construir el objeto
Animal
/Dog
/Cat
.
Responder
Una forma de hacerlo es agregar los constructores adecuados a sus clases. Mira abajo:
public class Animal {
int a, b, c;
public Animal(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}
}
public class Dog extends Animal {
int d, e;
public Dog(int a, int b, int c, int d, int e) {
super(a, b, c);
this.d = d;
this.e = e;
}
}
public class Cat extends Animal {
int f, g;
public Cat(int a, int b, int c, int f, int g) {
super(a, b, c);
this.f = f;
this.g = g;
}
}
Ahora, para crear una instancia de los objetos, puede hacer lo siguiente:
ArrayList<Animal> listAnimal = new ArrayList();
//sample values
int a = 10;
int b = 5;
int c = 20;
if(condition) {
listAnimal.add(new Dog(a, b, c, 9, 11));
//created and added a dog with e = 9 and f = 11
}
else {
listAnimal.add(new Cat(a, b, c, 2, 6));
//created and added a cat with f = 2 and g = 6
}
Este es el método que usaría en este caso. Mantiene el código más limpio y más legible al evitar toneladas de métodos "establecidos". Tenga en cuenta que super()
es una llamada al constructor de superclase ( Animal
en este caso).
Prima
Si no planea crear instancias de la clase Animal
, debe declararlo como abstract
. Las clases abstractas no se pueden crear instancias, pero se pueden clasificar en subclases y pueden contener métodos abstractos . Esos métodos se declaran sin un cuerpo, lo que significa que todas las clases de niños deben proporcionar su propia implementación. Aquí hay un ejemplo:
public abstract class Animal {
//...
//all animals must eat, but each animal has its own eating behaviour
public abstract void eat();
}
public class Dog {
//...
@Override
public void eat() {
//describe the eating behaviour for dogs
}
}
¡Ahora puedes llamar eat()
para cualquier animal! En el ejemplo anterior, con la lista de animales, podría hacer lo siguiente:
for(Animal animal: listAnimal) {
animal.eat();
}