如何通过使用超类来减少代码量?

39
我希望优化一些代码,目前这些代码由一个超类和两个子类组成。
以下是我的类:
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;
}

这是我的当前代码:
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);
}

如何重构与通用属性相关的代码?

我希望得到类似以下的代码:

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);
10个回答

37

你将一个Animal类型的变量作为设想是很好的。但同时,你也必须确保使用正确的构造函数:

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

提示:如果 Animal 总是猫或狗,请考虑将 Animal 声明为 abstract。这样,编译器会在你尝试执行 new Animal() 时自动发出警告。


15
构建猫或狗的过程很复杂,因为涉及到许多领域。这是建造者模式的一个很好的例子。
我的想法是为每种类型编写一个建造者,并组织它们之间的关系。这可以是组合或继承。
  • AnimalBuilder 构建一个通用的 Animal 对象并管理 abc 字段
  • CatBuilder 接受一个 AnimalBuilder(或扩展它)并继续构建一个 Cat 对象,管理 fg 字段
  • DogBuilder 接受一个 AnimalBuilder(或扩展它)并继续构建一个 Dog 对象,管理 de 字段

如果您不想创建构造器,请考虑为每个子类引入一个具有有意义名称的静态工厂方法:

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

这将简化构建过程,使其更加简洁易读。


11

答案

一种方法是向您的类添加适当的构造函数。请看下面:

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

现在,要实例化对象,您可以按照以下步骤操作:

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
} 

在这种情况下,我会使用这种方法。它通过避免大量的"set"方法来使代码更清晰易读。请注意,super()是对超类(在本例中为Animal)构造函数的调用。




Bonus

如果您不打算创建Animal类的实例,则应将其声明为abstract。抽象类不能被实例化,但可以被子类化并且可以包含抽象方法。这些方法声明时没有方法体,意味着所有子类都必须提供自己的实现。以下是一个示例:

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

现在您可以为任何动物调用eat()!在上面的示例中,对于动物列表,您可以像下面这样做:

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

3
类似于 Dog(int a, int b, int c, int d, int e) 这样的构造函数让我感到害怕。 - Andrew Tobilko
1
哈哈,是的,如果你不小心处理,长构造函数确实会变得混乱。然而,在某些情况下它们可以很有用。顺便说一句,我喜欢你个人资料图片中的猫。 - Talendar
1
@AndrewTobilko: #MeToo. - displayName

4

这里有一个解决方案,与slartidan的解决方案非常相似,但使用了建造者风格的setter,避免了dogcat变量。

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

这是一个非常奇怪的解决方案:setter 应该设置值并返回空。此外,您的子类将具有不同的 setter 类型:标准类型(用于 a、b、c)和返回 this 的类型。 - Andrew Tobilko
A、b 和 c 的设置器也可以返回这个。 - ToYonos

4
这是我的建议:
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);
    }
}

一些值得注意的要点:

  1. 在声明时使用List接口,例如List<Animal> listAnimal
  2. 在对象创建时使用动物接口Animal animal;
  3. 抽象类 Animal
  4. Setter方法返回this以使代码更简洁。否则,您将不得不像这样使用代码:animal.setD(4); animal.setE(5);

这样我们就可以利用接口Animal并设置共同属性一次。希望这有所帮助。


4
我会考虑动态查找/注册功能:飞行/游泳。
问题在于这是否适用于您的使用情况:取代飞行和游泳,使用鸟和鱼。
这取决于添加的属性是互斥的(狗/猫)还是可加的(飞行/游泳/哺乳动物/昆虫/产卵/...)。后者更适合使用地图进行查找。
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不需要实现任何接口,但您可以查找(lookup或者说作为)能力。这在未来是可扩展的,动态的:可以在运行时更改。

实现方式为

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

实现进行了安全的动态转换,因为寄存器确保了(键,值)条目的安全填充。
Animal flipper = new Animal();
flipper.register(new Fish() {
    @Override
    public boolean inSaltyWater() { return true; }
});

1
你的 as 方法可以简化为 return Optional.ofNullable(map.get(type)).map(type.cast(instance)),除此之外还有可爱的名称和示例! - Eugene
@Eugene 谢谢,在那个时候我不确定,但是 type.cast(null) 是可以的。我会改成安全的代码行。 - Joop Eggen

4
作为替代方案,您可以将狗和猫的“动物”部分作为单独的实体通过“Animalian”接口提供。这样做,您首先创建了公共状态,然后在需要时将其提供给特定于物种的构造函数。
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};
}

现在创建动物:
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));
}

这样做的原因是为了“优先选择组合而非继承”。我想表达的是,这只是解决问题的另一种不同方式。这并不意味着在任何时候都应该优先选择组合而非继承。在问题出现的上下文中,由工程师确定正确的解决方案。
关于这个主题有很多阅读材料

4

考虑将你的类设置为不可变的(《Effective Java》第三版第17条),如果所有参数都是必需的,可以使用构造函数或静态工厂方法(《Effective Java》第三版第1条)。如果有必选和可选的参数,则使用建造者模式(《Effective Java》第三版第2条)。


1

重构你的代码为:

ArrayList<Animal> listAnimal = new ArrayList();

//Other code...

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

新代码的好处:

  1. 隐藏复杂性:您已经隐藏了代码中的复杂性,当以后回顾代码时,您将会看到一个更小的、几乎以普通英语编写的代码。

但是现在,必须编写addDogToaddCatTo方法。它们的代码如下:

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

好处:

  1. 隐藏复杂性
  2. 两种方法都是私有的:这意味着它们只能在类内部调用。因此,您可以放心地省去对输入进行空值检查等操作,因为调用者(即在类内部)必须确保不向自己的成员传递虚假数据。

这意味着现在我们需要有createDogcreateCat方法。以下是我编写这些方法的方式:
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;
}

好处:

  1. 隐藏复杂性
  2. 两种方法都是私有的

现在,针对上述代码,您需要编写CatDog的构造函数,以便传入对象构造的公共属性和特定属性。可以像这样实现:
public Dog(AnimalAttribute generalAttribute,
    DogAttribute specificAttribute)
        : base (generalAttribute) {
    this.d = specificAttribute.getD();
    this.e = specificAttribute.getE();
}

和,

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

好处:

  1. DRY 代码:两个构造函数都使用 generalAttributes 调用超类方法,这样就可以处理两个子类对象的共同属性;
  2. 整个对象被保留:不需要调用一个构造函数并传递 20,000 个参数,只需传递 2 个参数,即通用动物属性对象和特定动物属性对象。这两个参数包含其余的属性,并在需要时在构造函数中取消装箱。

最后,你的 Animal 构造函数应该是这样的:

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

好处:

  1. 整个对象被保留

为了完整起见:
  • AnimalAttribute/DogAttribute/CatAttribute 类仅具有一些字段和这些字段的 getter 和 setter;
  • 这些字段是构造 Animal/Dog/Cat 对象所需的数据。

0

这里有许多很好的建议。我会使用我个人最喜欢的构建器模式(但加入了继承的特色):

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

注意: Animal.Builder 构造方法以 Class builderClass 作为泛型参数。当返回当前对象实例时,请将其强制转换为此类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接