这还算继承吗?这是一个好的编码实践吗?
Is this still considered as inheritance? Is this an ok coding practice?
我目前正在重做 Tim Buchalka 的 OOP Master Challenge 练习,任务是制作一个 Hamburger
class,其中包含名称、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 的定义,以便计算贯穿所有层。
这对您的作用是减轻结构需要对汉堡的含义做出的任何假设。无需修改任何其他内容即可添加或修改成分和价格。
您在这里违反了面向对象编程的几个原则:
- 构造函数无法等待用户输入。任何 input/output(诊断消息除外)都应在调用链中尽可能向上处理,即在主程序或它直接调用的函数之一中处理。构造函数必须从其参数中获取所有必要的输入。
- 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;
}
}
最后,这是我们的 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();
向用户询问汉堡类型,并为该类型创建构建器。然后在获得卷类型、肉类型和添加物的输入时使用构建器创建对象。
我目前正在重做 Tim Buchalka 的 OOP Master Challenge 练习,任务是制作一个 Hamburger
class,其中包含名称、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 的定义,以便计算贯穿所有层。
这对您的作用是减轻结构需要对汉堡的含义做出的任何假设。无需修改任何其他内容即可添加或修改成分和价格。
您在这里违反了面向对象编程的几个原则:
- 构造函数无法等待用户输入。任何 input/output(诊断消息除外)都应在调用链中尽可能向上处理,即在主程序或它直接调用的函数之一中处理。构造函数必须从其参数中获取所有必要的输入。
- 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;
}
}
最后,这是我们的 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();
向用户询问汉堡类型,并为该类型创建构建器。然后在获得卷类型、肉类型和添加物的输入时使用构建器创建对象。