这还算继承吗?这是一个好的编码实践吗?

Is this still considered as inheritance? Is this an ok coding practice?

我目前正在重做 Tim Buchalka 的 OOP Master Challenge 练习,任务是制作一个 Hamburgerclass,其中包含名称、breadRoll、价格、肉类等基本字段;并创建一个名为 Healthy Burger 的继承 class ,它将拥有自己的特定字段来区分它(有不同的面包卷,更多添加)。

Tim 首先将所有添加项和每次添加项的价格都放在一个单独的字段中(并且还为每个添加项创建了设置它们的方法)。像这样:

private String addition1;
private String additionPrice1;
private String addition2; etc...

因为我更喜欢创建一个 String[] 并将每个添加预定义到一个 String 数组中。像这样:

    private String[] additions = {"lettuce","tomato","carrot","dressing","onion","ginger"};
    private double[] additionPrice = {0.3,0.4,0.5,0.6,0.7,0.8};

我想到了一个想法,即要创建汉堡包的用户 class 必须具有受限输入和创建汉堡包对象的更简单方法,并且没有无效输入。

所以我的构造函数是这样的:

 public Hamburger(String name) {

    this.name=name;
    breadRollSelection();
    baseMeatSelection();
    this.totalPrice = basePrice;
}

breadRollSelection();baseMeatSelection(); 都包含打印出面包卷和肉类类型的 String[] 的循环,用户将在其中使用 Scanner 输入:

public void breadRollSelection() {
    System.out.println("Pick a bread roll: ");
    if(this.name.equalsIgnoreCase("Healthy Hamburger")) {
        this.breadRoll=breadRolls[2];//healthy bread roll
        System.out.println(breadRoll + " has been chosen with the price of "+breadRollsPrice[2]);
        this.basePrice += breadRollsPrice[2];
    } else {
        for (int i = 0; i < 2; i++) {
            System.out.println(i + " " + breadRolls[i] + " with the price of: " + breadRollsPrice[i]);
        }
        Scanner scan = new Scanner(System.in);
        int choice = scan.nextInt();
        this.breadRoll = breadRolls[choice];
        this.basePrice += breadRollsPrice[choice];
    }
}

Healthy Burger class 中,我只这样做了:|

public class HealthyBurger extends Hamburger {

private String breadRoll="Bread Roll Diet";
public HealthyBurger() {
    super("Healthy Hamburger");
}

所以当我创建一个HealthyHamburger ham = new HealthyHamburger();对象时,在访问breadRollSelection()方法时,它已经知道有一个子对象在调用它。我会在子 class 中覆盖它,但我无权访问 String[] breadRoll,除非我让它们受到保护(如果可以的话)。

并且使用更大的 addAddition(); 方法,我使用汉堡的名称作为应该添加的条件:

  public void addAddition(){
        int additionLimit=0;
        boolean q=true;
        if(this.name=="Basic") {
            while (q) {
                for(int i=0;i<4;i++){
                    System.out.println(i+" "+additions[i]+ " with the price of: "+additionPrice[i]);
                }
                Scanner scan = new Scanner(System.in);
                int choice = scan.nextInt();
                choice -= 1;
                System.out.println(additions[choice] + " chosen!");
                this.totalPrice += additionPrice[choice];
                System.out.println(this.totalPrice + " is the price after this addition!");
                additionLimit += 1;

                if (additionLimit == 4) {
                    System.out.println("No more additions");
                    q = false;
                    break;
                }
                System.out.println("Press 0 to exit or 1 if you want more additions!");
                int choice1 = scan.nextInt();
                if (choice1 == 0) {
                    q = false;
                }
            }
        }
        if(this.name=="Healthy Hamburger"){
            while (q) {
                for(int i=0;i<additions.length;i++){
                    System.out.println(i+" "+additions[i]+ " with the price of: "+additionPrice[i]);
                }
                Scanner scan = new Scanner(System.in);
                int choice = scan.nextInt();
                choice -= 1;
                System.out.println(additions[choice] + " chosen!");
                this.totalPrice += additionPrice[choice];
                System.out.println(this.totalPrice + " is the price after this addition!");
                additionLimit += 1;

                if (additionLimit == 6) {
                    System.out.println("No more additions");
                    q = false;
                    break;
                }
                System.out.println("Press 0 to exit or 1 if you want more additions!");
                int choice1 = scan.nextInt();
                if (choice1 == 0) {
                    q = false;
                }
            }
        }
    } 

我的问题是,如果可以像这样使用父 class,它会知道哪个子正在访问它,如果我这样使用构造函数是否可以?

你的直觉很好,感觉这里有些不对劲。这有点主观,但我可以提供一些指南和设计模式描述来帮助您解决问题。

首先,这是一个红旗 anti-pattern:

  • 块太多 if
  • 嵌套过多,即在 ifs 内部循环
  • 是的,parent class 意识到了它的 child。

其中一些可以通过将较大的函数分解为较小的函数来解决,但是 parent class 本质上应该是抽象的。一旦您发现有必要赋予它更多的上下文意识,那就表明您使用了错误的模式。

现在,该怎么办。

我相信您正在寻找与传统继承相反的 'compositional' 模式。具体来说,您可能正在寻找的是 Decorator Pattern。事实上,Decorator 模式的 classic 示例 use-case 是咖啡馆中各种咖啡的构造和定价,在概念上与您的汉堡包示例相差无几。

要点是,您要制作的东西的每个组件都是独立的 class。而不是相互继承。所以你从一个基本的“汉堡包”class 或一个基本的“VegiBurger”开始。 (而且没有理由他们仍然不能继承抽象的“汉堡”class。)然后,使用 Dependency Injection,将 base-object 赋予一个成分实例,然后将该成分 object到下一个,以此类推。

每个组件都负责自己的定价和属性,您最终得到的是一系列 object 包装纸,看起来有点像您可能要放的洋葱层你的汉堡现在的诀窍是,它们中的每一个都实现了相同的接口。该接口保证了诸如“getTotal”方法之类的东西的存在。每个单独的“getTotal”定义都将设计为利用最初给它的 object 的定义,以便计算贯穿所有层。

这对您的作用是减轻结构需要对汉堡的含义做出的任何假设。无需修改任何其他内容即可添加或修改成分和价格。

您在这里违反了面向对象编程的几个原则:

  1. 构造函数无法等待用户输入。任何 input/output(诊断消息除外)都应在调用链中尽可能向上处理,即在主程序或它直接调用的函数之一中处理。构造函数必须从其参数中获取所有必要的输入。
  2. The Open/Closed Principle:每次添加子class时,不必修改基础class。这里,breadRollSelection 取决于对 HealtyBurger 的存在的了解。

您可能会考虑做的一件事是从 Hamburger class 中获取您要求用户输入的位。您可以将所有这些转移到 OrderCounter class.

构建器模式似乎很适合创建 Hamburger 对象,这些对象有很多(可选的)“添加项”。在 Effective Java, 3rd ed. 的第 2 项中,Joshua Bloch 建议使用相同的方法来创建 Pizza 对象,这些对象同样有很多浇头。如果我们采用他的方法,

public abstract class Hamburger {

    final Set<Addition> additions;
    final BreadRoll breadRoll;

    abstract static class Builder<T extends Hamburger.Builder<T>> {
        protected EnumSet<Addition> additions = EnumSet.noneOf(Addition.class);
        protected BreadRoll breadRoll;

        public T addAddition(Addition addition) {
            additions.add(Objects.requireNonNull(addition));
            return self();
        }

        public T breadRoll(BreadRoll breadRoll) {
            this.breadRoll = breadRoll;
            return self();
        }

        abstract Hamburger build();

        protected abstract T self();
    }

    abstract double getPrice();

    Hamburger(Hamburger.Builder<?> builder) {
        additions = builder.additions.clone();
        breadRoll = builder.breadRoll;
    }
}

public class HealthyBurger extends Hamburger {

    public static class Builder extends Hamburger.Builder<HealthyBurger.Builder> {

        @Override
        public HealthyBurger build() {
            return new HealthyBurger(this);
        }

        @Override
        protected HealthyBurger.Builder self() {
            return this;
        }
    }

    HealthyBurger(Builder builder) {
        super(builder);
    }

    
    @Override
    double getPrice() {
        double priceOfAdditions = additions.stream().mapToDouble(Addition::getPrice).sum();
        double priceOfBreadRoll = breadRoll.getPrice();
        return priceOfAdditions + priceOfBreadRoll;
    }
}

最后,这是我们的 AdditionBreadRoll 枚举:

public enum Addition {
    LETTUCE(0.3), TOMATO(0.4), CARROT(0.5), ONION(0.6);

    private final double price;

    Addition(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

public enum BreadRoll {
    KAISER(1.0), PRETZEL(1.1), SLICED(1.2), DIET(1.3);

    private final double price;

    BreadRoll(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

创建 HealthyBurger 如下所示:

HealthyBurger healthy = new HealthyBurger.Builder()
                .breadRoll(BreadRoll.DIET)
                .addAddition(Addition.TOMATO)
                .addAddition(Addition.ONION)
                .build();

向用户询问汉堡类型,并为该类型创建构建器。然后在获得卷类型、肉类型和添加物的输入时使用构建器创建对象。