问题描述
我目前正在重做 Tim Buchalka 的 OOP 大师挑战练习,其中的任务是制作一个 Hamburger
类,其中包含名称、面包卷、价格、肉类等基本字段;并创建一个名为 Healthy Burger
的继承类,其中它将拥有自己的特定字段来区分它(具有不同的面包卷,更多的添加)。
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};
我有一个想法,即要创建 Hamburger 类的用户必须具有约束输入和更简单的方法来创建 Hamburger 对象,而没有无效输入。
所以我制作了这样的构造函数:
public Hamburger(String name) {
this.name=name;
breadrollSelection();
baseMeatSelection();
this.totalPrice = basePrice;
}
breadrollSelection();
和 baseMeatSelection();
都包含循环,用于打印出用于面包卷和肉类类型的 String[],用户将使用扫描仪输入:
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
类中,我只这样做了:|
public class HealthyBurger extends Hamburger {
private String breadroll="Bread Roll Diet";
public HealthyBurger() {
super("Healthy Hamburger");
}
所以当我创建一个 HealthyHamburger ham = new HealthyHamburger();
对象时,在访问 breadrollSelection()
方法时,它已经知道一个子对象正在调用它。我会在子类中覆盖它,但我无权访问 String[] 面包卷,除非我让它们受到保护(如果在这里这样做可以)。
使用 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;
}
}
}
}
我的问题是,如果可以这样使用父类,它会知道哪个孩子正在访问它,如果我这样使用构造函数可以吗?
解决方法
你的直觉很好,就感觉这里有些不对劲。这有点主观,但我可以提供一些指导方针和设计模式描述来帮助您解决问题。
首先,这是一个反模式的危险信号:
-
if
块太多 - 嵌套过多,即 ifs 内的循环
- 是的,一个知道其孩子的父类。
其中一些可以通过将较大的函数分解为较小的函数来解决,但父类本质上应该是抽象的。一旦您发现有必要为其提供更多的上下文感知,这表明您使用了错误的模式。
现在,该怎么办。
我相信您正在寻找一种与传统继承相反的“组合”模式。具体来说,您可能正在寻找的是 Decorator Pattern。事实上,装饰者模式的经典示例用例是咖啡馆中各种类型咖啡的构建和定价,从概念上讲,与您的汉堡包示例相去甚远。
它的要点是,您尝试制作的东西的每个组件都是它自己独立的类。而不是互相继承。所以你从一个基本的“汉堡包”类或一个基本的“VegiBurger”类开始。 (并且没有理由他们仍然不能从抽象的“Burger”类继承。)然后,使用 Dependency Injection,将该基础对象赋予一个成分实例,然后将该成分对象赋予下一个,然后以此类推。
每个组件负责自己的定价和属性,最终得到的是一系列对象包装器,看起来有点像你可能会放在汉堡上的洋葱层。现在的诀窍是,它们中的每一个都实现了相同的接口。并且该接口保证存在诸如“getTotal”方法之类的东西。每个单独的“getTotal”定义都将被设计为利用最初提供给它的对象的定义,以便计算贯穿所有层。
这对您的作用是减轻结构需要对汉堡包的含义做出的任何假设。无需修改任何其他内容即可添加或修改成分和价格。
,您在这里违反了面向对象编程的几个原则:
- 构造函数不能等待用户输入。任何输入/输出(诊断消息除外)都应尽可能在调用链中处理,即在主程序或它直接调用的函数之一中。构造函数必须从其参数中获取所有必要的输入。
-
The Open/Closed Principle:每次添加子类时不必修改基类。此处,
breadRollSelection
取决于是否知道HealtyBurger
的存在。
您可能考虑做的一件事是从 Hamburger
类中获取您要求用户输入的部分。您可以将所有这些转移到 OrderCounter
类。
构建器模式似乎非常适合创建 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;
}
}
最后,这里是我们的 Addition
和 BreadRoll
枚举:
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();
向用户询问汉堡包类型,并为该类型创建构建器。然后使用构建器在您获得卷类型、肉类类型和添加物的输入时创建对象。