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

问题描述

我目前正在重做 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”定义都将被设计为利用最初提供给它的对象的定义,以便计算贯穿所有层。

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

,

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

  1. 构造函数不能等待用户输入。任何输入/输出(诊断消息除外)都应尽可能在调用链中处理,即在主程序或它直接调用的函数之一中。构造函数必须从其参数中获取所有必要的输入。
  2. 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;
    }
}

最后,这里是我们的 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();

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