wake-up-neo.com

Wie kann man Code durch Verwendung einer Superklasse reduzieren?

Ich möchte etwas Code umgestalten, der derzeit aus einer Oberklasse und zwei Unterklassen besteht.

Dies sind meine Klassen:

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;
}

Dies ist mein aktueller Code:

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);
}

Wie kann ich den Code bezüglich der allgemeinen Attribute umgestalten? 

Ich hätte so etwas gern:

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);
38
Jax Teller

Ihre Idee, eine Variable vom Typ Animal zu haben, ist gut. Sie müssen jedoch auch den richtigen Konstruktor verwenden:

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);

Hinweis: Wenn ein Tier immer entweder Katze oder Hund ist, sollten Sie Animal abstract erstellen. Der Compiler beschwert sich dann automatisch, wenn Sie versuchen, new Animal() auszuführen.

37
slartidan

Die Konstruktion einer Katze oder eines Hundes ist komplex, da viele Felder betroffen sind. Das ist ein guter Fall für das Builder-Muster .

Meine Idee ist es, für jeden Typ einen Builder zu schreiben und die Beziehungen zwischen ihnen zu organisieren. Es könnte sich um Komposition oder Erbschaft handeln.

  • AnimalBuilder erstellt ein allgemeines Animal-Objekt und verwaltet die Felder a, b, c
  • CatBuilder nimmt eine AnimalBuilder (oder erweitert diese) und erstellt ein Cat-Objekt, das die f, g-Felder verwaltet
  • DogBuilder nimmt eine AnimalBuilder (oder erweitert diese) und erstellt ein Dog-Objekt, das die Felder d, e verwaltet

Wenn Sie keine Builder erstellen möchten, sollten Sie eine statische Factory-Methode mit einem aussagekräftigen Namen für jede Unterklasse einführen:

Animal animal = condition ? Dog.withDE(4, 5) : Cat.withFG(6, 7);
// populate animal's a, b, c
listAnimal.add(animal);

Dies würde die Konstruktion vereinfachen und weniger ausführlich und lesbarer machen.

15
Andrew Tobilko

Antworten

Eine Möglichkeit besteht darin, den Klassen die richtigen Konstruktoren hinzuzufügen. Siehe unten:

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;
   } 
}

Jetzt können Sie die Objekte wie folgt instanziieren:

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
} 

Dies ist die Methode, die ich in diesem Fall verwenden würde. Es hält den Code sauberer und lesbarer, indem er jede Menge "festgelegter" Methoden vermeidet. Beachten Sie, dass super() ein Aufruf an den Konstruktor der Oberklasse (in diesem Fall Animal) ist.




Bonus

Wenn Sie keine Instanzen der Klasse Animal erstellen möchten, sollten Sie sie als abstract deklarieren. Abstrakte Klassen kann nicht instanziiert werden, kann jedoch Unterklassen enthalten und abstrakte Methoden enthalten. Diese Methoden werden ohne Körper deklariert, was bedeutet, dass alle Kinderklassen ihre eigene Implementierung bereitstellen müssen. Hier ist ein Beispiel:

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
   } 
}

Jetzt können Sie eat() für jedes Tier aufrufen! Im vorherigen Beispiel können Sie mit der Liste der Tiere Folgendes tun:

for(Animal animal: listAnimal) {
   animal.eat();
} 
11
Talendar

Erwägen Sie, Ihre Klassen unveränderlich zu machen (Effektives Java 3rd Edition-Element 17). Wenn alle Parameter erforderlich sind, verwenden Sie einen Konstruktor oder eine statische Factory-Methode (Effektives Java 3rd Edition-Element 1). Wenn erforderliche und optionale Parameter vorhanden sind, verwenden Sie das Builder-Muster (Effektives Java 3rd Edition-Element 2).

4
Diego Marin

Folgendes möchte ich vorschlagen:

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);
    }
}

Einige bemerkenswerte Punkte:

  1. Verwendung der List-Schnittstelle in der Deklaration List<Animal> listAnimal
  2. Verwendung des Schnittstellentiers bei der Objekterstellung Animal animal;
  3. abstract Klasse Tier
  4. Setter, die this zurückgeben, um den Code sauberer zu machen. Oder Sie müssten Code wie animal.setD(4);animal.setE(5); verwenden.

Auf diese Weise können wir die Schnittstelle Animal verwenden und die allgemeinen Attribute einmal festlegen. Hoffe das hilft.

4
mayurmadnani

Alternativ können Sie die "Animal" -Teile Ihres Hundes und Ihrer Katze zu einer separaten Einheit machen, die über die "Animalian" -Schnittstelle verfügbar ist. Auf diese Weise erstellen Sie zuerst den allgemeinen Zustand und stellen ihn dann an dem Punkt, an dem er benötigt wird, dem artspezifischen Konstruktor zur Verfügung.

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};
}

Jetzt Tiere erstellen:

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));
}

Der Grund dafür ist, " die Zusammensetzung der Vererbung vorzuziehen ". Ich möchte sagen, dass dies nur eine andere Art ist, das gestellte Problem zu lösen. Das bedeutet nicht, dass die Komposition zu jeder Zeit der Vererbung vorgezogen werden sollte. Es liegt am Ingenieur, die richtige Lösung für den Kontext zu ermitteln, in dem das Problem auftritt.

Es gibt viel von Lesen zu diesem Thema .

4
justin.hughey

Ich würde eine dynamische Suche/Registrierung von Fähigkeiten/Funktionen in Betracht ziehen: Fliegen/Schwimmen.

Es ist die Frage, ob dies zu Ihrer Verwendung passt: Anstatt Flying & Swimming nehmen Sie Bird and Fish.

Es hängt davon ab, ob die hinzugefügten Eigenschaften ausschließlich (Hund/Katze) oder Zusatzstoff (Fliegen/Schwimmen/Säugetier/Insekt/Eiablage/...) sind. Letzteres ist eher eine Suche mit Hilfe einer Karte.

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 muss keine Schnittstelle implementieren, Sie können jedoch nachschlagen (nachschlagen oder möglicherweise als). Dies ist in der Zukunft erweiterbar, dynamisch: kann sich zur Laufzeit ändern.

Umsetzung als

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);
}

Die Implementierung führt eine sichere dynamische Umwandlung durch, da register das sichere Ausfüllen von (Schlüssel-, Wert-) Einträgen gewährleistet.

Animal flipper = new Animal();
flipper.register(new Fish() {
    @Override
    public boolean inSaltyWater() { return true; }
});
4
Joop Eggen

Hier ist eine Lösung, die ziemlich nahe an Slartidans Lösung liegt, aber setter mit dem Builder-Stil verwendet und die Variablen dog und cat vermeidet

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);
4
ToYonos

Refaktorieren Sie Ihren Code in:

ArrayList<Animal> listAnimal = new ArrayList();

//Other code...

if (animalIsDog) {
    addDogTo(listAnimal, commonAttribute, dogSpecificAttribute); 
} else {
    addCatTo(listAnimal, commonAttribute, catSpecificAttribute);
}

Vorteile mit dem neuen Code:

  1. Versteckte Komplexität : Sie haben die Komplexität verborgen und Sie müssen sich jetzt einen etwas kleineren Code ansehen, der fast in reinem Englisch geschrieben ist, wenn Sie Ihren Code später erneut aufrufen.

Nun müssten aber die Methoden addDogTo und addCatTo geschrieben werden. So würden sie aussehen:

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);
}

Leistungen:

  1. Versteckte Komplexität ;
  2. Beide Methoden sind privat : Dies bedeutet, dass sie nur innerhalb der Klasse aufgerufen werden können. Sie können also sicher auf die Überprüfung der Eingabe auf Null usw. verzichten, da der Aufrufer (der sich innerhalb der Klasse befindet) darauf geachtet haben muss, dass keine falschen Daten an seine eigenen Mitglieder weitergegeben werden.

Dies bedeutet, dass jetzt createDog- und createCat-Methoden vorhanden sein müssen. So würde ich diese Methoden schreiben:

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;
}

Leistungen:

  1. Versteckte Komplexität ;
  2. Beide Methoden sind privat .

Für den oben geschriebenen Code müssen Sie nun Konstruktoren für Cat und Dog schreiben, die die allgemeinen Attribute und die spezifischen Attribute für die Objektkonstruktion aufnehmen. Das kann so aussehen:

public Dog(AnimalAttribute generalAttribute,
    DogAttribute specificAttribute)
        : base (generalAttribute) {
    this.d = specificAttribute.getD();
    this.e = specificAttribute.getE();
}

und,

public Cat(AnimalAttribute generalAttribute,
    CatAttribute specificAttribute)
        : base (generalAttribute) {
    this.f = specificAttribute.getF();
    this.g = specificAttribute.getG();
}

Leistungen:

  1. Code ist DRY: Beide Konstruktoren rufen die Superklassenmethode mit generalAttributes auf. Dabei werden die gemeinsamen Attribute beider Unterklassenobjekte berücksichtigt.
  2. Das gesamte Objekt bleibt erhalten: Anstatt einen Konstruktor aufzurufen und 20.000 Parameter zu übergeben, übergeben Sie es einfach 2, nämlich. allgemeines Tierattributobjekt und spezifisches Tierattributobjekt. Diese beiden Parameter enthalten den Rest der Attribute und werden bei Bedarf innerhalb des Konstruktors eingebettet.

Schließlich sieht der Konstruktor Ihres Animal so aus:

public Animal(AnimalAttribute attribute) {
    this.a = attribute.getA();
    this.b = attribute.getB();
    this.c = attribute.getC();
}

Leistungen:

  1. Das gesamte Objekt bleibt erhalten ;

Der Vollendung halber:

  • AnimalAttribute/DogAttribute/CatAttribute Klassen haben nur einige Felder und die Getter und Setter für diese Felder;
  • Diese Felder sind die Daten, die zum Erstellen des Objekts Animal/Dog/Cat benötigt werden.
1
displayName

Viele tolle Vorschläge hier. Ich würde mein persönliches Lieblings-Builder-Muster verwenden (aber mit zusätzlichem Vererbungsgeschmack):

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); 
    }
}

Hinweis: Der Animal.Builder-Konstruktor verwendet Class builderClass als generisches Argument. Die aktuelle Instanz des Objekts in diese Klasse umwandeln, wenn sie zurückgegeben wird.

0
nabster