第二章:快速跟踪OOP - 类和接口(java9 cookbook)

在本章中,我们将介绍以下内容:

  • 使用类实现面向对象的设计
  • 使用内部类
  • 使用继承和组合来制作
  • 设计可扩展
  • 编码到接口
  • 使用默认和静态方法创建接口
  • 使用私有方法创建接口
  • 使用枚举来表示常量实体
  • 使用@Deprecated批注来弃用API
  • 在Javadocs中使用HTML5

介绍

本章简要介绍了OOP的组件,并介绍了Java 8和Java 9中这些组件的新增强功能。我们还将尝试介绍一些
在适用的情况下,良好的面向对象设计(OOD)实践。

在整个配方中,我们将使用新的(在Java 8和Java 9中引入)增强功能,使用特定代码示例定义和演示OOD的概念,并提供新功能以获得更好的代码文档。

人们可以花很多时间在书籍和互联网上阅读有关OOD的文章和实用建议。 有些活动对某些人有益。 但是,根据我们的经验,掌握OOD的最快方法是在您自己的代码中尽早尝试其原则。 这正是本章的目标:让您有机会查看和使用OOD原则,以便正式定义立即有意义。

编写良好的代码的主要标准之一是表达其意图的清晰度。 一个充满活力和清晰的设计有助于实现这一目标。 代码由计算机运行,但由人类维护和扩展。 牢记这一点将确保您编写的代码的长久性,甚至可能还有一些感谢和赞赏提及。

在本章中,您将学习如何使用五个基本的OOD概念:

对象/类 - 将数据和过程保持在一起

封装 - 隐藏数据和/或程序

继承 - 扩展另一个类数据和/或过程

接口 - 推迟类型的实现和编码

多态性 - 当父类引用用于引用子类对象时,对其所有扩展使用基类类型

这些概念将在本章介绍的代码片段中定义和演示。 如果您在互联网上搜索,您可能会注意到它们的许多其他概念和附加内容可以从我们刚才讨论过的五点中得出。

虽然以下文本不需要事先了解OOD,但在Java中编写代码的一些经验将是有益的。 代码示例功能齐全,与Java 9兼容。为了更好地理解,我们建议您尝试运行提供的示例。

我们还鼓励您根据团队体验中的需求调整本章中的提示和建议。 考虑与您的同事分享您的新发现的知识,并讨论如何将所述原则应用于您的域,当前项目。

使用类实现面向对象的设计

在本文中,您将了解前两个OOD概念:对象/类和封装。

做好准备

术语对象通常指的是可以应用于这些数据的数据和过程的耦合。 既不需要数据也不需要程序,但其中一个是 - 并且通常两者都是 - 始终存在。 数据称为对象属性,而过程称为方法。 属性捕获对象的状态。 方法描述了对象的行为。 每个对象都有一个类型,由其类定义(参见下文)。 一个对象也被认为是一个类的实例。

术语class是将在其每个实例中存在的属性和方法的定义的集合 - 基于此类创建的对象。
封装是隐藏其他对象无法访问的对象属性和方法。
封装是通过Java关键字private或在属性和方法的声明中受到保护来实现的。

怎么做…

1.使用horsePower属性创建Engine类,设置此属性值的setHorsePower()方法,以及根据车辆开始移动后的时间计算车辆速度的getSpeedMph()方法,车辆重量, 和发动机功率:

public class Engine {
private int horsePower;
public void setHorsePower(int horsePower) {
this.horsePower = horsePower;
}
public double getSpeedMph(double timeSec,
int weightPounds) {
double v = 2.0*this.horsePower*746;
v = v*timeSec*32.17/weightPounds;
return Math.round(Math.sqrt(v)*0.68);
}
}

2.创建Vehicle类:

public class Vehicle {
private int weightPounds;
private Engine engine;
public Vehicle(int weightPounds, Engine engine) {
this.weightPounds = weightPounds;
this.engine = engine;
}
public double getSpeedMph(double timeSec){
return this.engine.getSpeedMph(timeSec, weightPounds);
}
}

3.创建将使用这些类的应用程序:

public static void main(String... arg) {
double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Engine engine = new Engine();
engine.setHorsePower(horsePower);
Vehicle vehicle = new Vehicle(vehicleWeight, engine);
System.out.println("Vehicle speed (" + timeSec + " sec)=" +
vehicle.getSpeedMph(timeSec) + " mph");
}

怎么运行的…

上述应用程序生成以下输出:

Vehicle speed (10.0 sec)=117.0 mph

如您所见,引擎对象是通过调用不带参数的Engine类的默认构造函数和Java关键字new来创建的,该关键字为堆上新创建的对象分配了内存。

第二个对象,即vehicle,是使用带有两个参数的Vehicle类的显式定义构造函数创建的。 构造函数的第二个参数是一个引擎对象,它使用setHorsePower()方法携带值为246的horsePower属性。

它还包含getSpeedMph()方法,该方法可由任何其他有权访问引擎对象的对象调用,因为它在Vehicle类的getSpeedMph()方法中完成。

值得注意的是,Vehicle类的getSpeedMph()方法依赖于分配给引擎属性的值的存在。 Vehicle类的对象将速度计算委托给Engine类的对象。 如果没有设置后者(例如,在Vehicle()构造函数中传递null),我们将在运行时获得NullPointerException。 为避免这种情况,我们可以检查Vehicle()构造函数中是否存在此值:

if(engine == null){
throw new RuntimeException("Engine" + " is required parameter.");
}

或者,我们可以检查Vehicle类的getSpeedMph()方法:

if(getEngine() == null){
throw new RuntimeException("Engine value is required.");
}

这样,我们避免了NullPointerException的歧义,并告诉用户确切的问题来源是什么。

您可能已经注意到,getSpeedMph()方法可以从Engine类中删除,并在Vehicle:class中完全实现:

public double getSpeedMph(double timeSec){
double v = 2.0 * this.engine.getHorsePower() * 746;
v = v * timeSec * 32.174 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}

为此,我们需要将公共方法getHorsePower()添加到Engine类,以使其可供Vehicle类中的getSpeedMph()方法使用。 现在,我们将getSpeedMph()方法保留在 Engine. class类中。

这是您需要做出的设计决策之一。 如果您认为Engine类的对象将被传递给不同类的对象(不仅是Vehicle),那么您需要在Engine类中保留getSpeedMph()方法。 否则,如果您认为Vehicle类将负责速度计算(这是有意义的,因为它是车辆的速度,而不是引擎的速度),那么您应该在Vehicle类中实现该方法。

还有更多…

Java提供了扩展类的功能,并允许子类访问基类的所有功能。 例如,您可以决定可以询问其速度的每个对象是否属于从Vehicle类派生的子类。 在这种情况下,Car可能如下所示:

public class Car extends Vehicle {
private int passengersCount;
public Car(int passengersCount, int weightPounds,
Engine engine){
super(weightPounds, engine);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount;
}
}

现在我们可以通过将Car类替换为Car类来更改我们的测试代码:

public static void main(String... arg) {
double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Engine engine = new Engine();
engine.setHorsePower(horsePower);
Vehicle vehicle = new Car(4, vehicleWeight, engine);
System.out.println("Car speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

运行时,它产生的值与Vehicle类的对象相同:

Car speed (10.0 sec)=117.0 mph

由于多态性,可以将对Car类对象的引用分配给基类的引用,即Vehicle。 Car类的对象有两种类型:它自己的类型,即Car和基类的类型,即Vehicle。

在Java中,类也可以实现多个接口,并且这样的类的对象也将具有每个实现的接口的类型。 我们将在随后的文章中讨论这个问题。

通常有几种方法可以为相同的功能设计应用程序。 这完全取决于项目的需求和开发团队采用的风格。 但在任何情况下,设计的清晰度将帮助您传达意图。 良好的设计有助于提高代码的质量和寿命。

也可以看看

在本文中,您将学习三种类型的内部类:

  • 内部类:这是在另一个(封闭)类中定义的类。 它来自封闭类之外的可访问性受public,protected和 private 关键字的约束。 它可以访问封闭类的私有成员,封闭类可以访问其内部类的私有成员。
  • 方法本地内部类:这是在方法内定义的类。 其范围仅限于该方法。
  • 匿名内部类:这是在对象实例化期间定义的匿名类。

准备

当一个类被一个类,或其他类单独使用时,设计者可能会认为不需要公开这样的类。 例如,假设Engine类仅由Vehicle类使用。

怎么做…

1.将Engine类创建为Vehicle类的内部类:

public class Vehicle {
private int weightPounds;
private Engine engine;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.engine = new Engine(horsePower);
}
public double getSpeedMph(double timeSec){
return this.engine.getSpeedMph(timeSec);
}
private int getWeightPounds(){ return weightPounds; }
private class Engine {
private int horsePower;
private Engine(int horsePower) {
this.horsePower = horsePower;
}
private double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / getWeightPounds();
return Math.round(Math.sqrt(v) * 0.68);
}
}
}

2.请注意,Vehicle类的getSpeedMph()方法可以访问Engine类(尽管它被声明为private),甚至可以访问Engine类的私有getSpeedMph()方法。 内部类也可以访问封闭类的所有私有元素。 这就是Engine类的getSpeedMph()可以访问封闭Vehicle类的私有getWeightPounds()方法的原因。注封闭类理解为外部类,外部类可以访问内部类的所有属性与方未能,内部类也可以访问外部类的所有属性与方法

3.仔细研究内部类Engine的用法。 只有getSpeedMph()方法使用它。 如果设计师认为将来也会如此,那么将它作为方法本地内部类是合理的,这是内部类的第二种类型:

public class Vehicle {
private int weightPounds;
private int horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
private int getWeightPounds() { return weightPounds; }
public double getSpeedMph(double timeSec){
class Engine {
private int horsePower;
private Engine(int horsePower) {
this.horsePower = horsePower;
}

private double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / getWeightPounds();
return Math.round(Math.sqrt(v) * 0.68);
}
}
Engine engine = new Engine(this.horsePower);
return engine.getSpeedMph(timeSec);
}
}

封装 - 隐藏对象的状态和行为 - 有助于避免因意外更改或覆盖而导致的意外副作用。 它使行为更易于理解和更容易理解。 这就是为什么好的设计只暴露必须从外部访问的功能。 通常,这是首先激发课堂创作的主要功能。

怎么运行的…

无论Engine类是作为内部类还是方法本地内部类实现,测试代码看起来都是一样的:

public static void main(String arg[]) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Vehicle(vehicleWeightPounds, engineHorsePower);
System.out.println("Vehicle speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

如果我们运行这个程序,我们得到相同的输出:

Vehicle speed (10.0 sec) = 117.0 mph

现在,让我们假设我们需要测试getSpeedMph()方法的不同实现:

public double getSpeedMph(double timeSec){ return -1.0d; }

如果这个速度计算公式对你没有意义,那你就是对的。 它不是。 我们这样做是为了使结果可预测并且与先前实现的结果不同。

有很多方法可以引入这个新实现。 例如,我们可以在Engine类中更改getSpeedMph()方法的实现。 或者,我们可以在Vehicle类中更改相同方法的实现。

在这个配方中,我们将使用称为匿名内部类的第三类内部类来完成此操作。 当您希望尽可能少地编写新代码或想要通过暂时覆盖旧代码来快速测试新行为时,此方法尤其方便。 代码将如下所示:

public static void main(String... arg) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Vehicle(vehicleWeightPounds, engineHorsePower) {#覆盖旧代码
public double getSpeedMph(double timeSec){
return -1.0d;
}
};
System.out.println("Vehicle speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

如果我们运行此程序,结果如下:

Vehicle speed (10.0 sec) = -1.0 mph

我们通过只留下一个方法覆盖了Vehicle类的实现 - 返回硬编码值的getSpeedMph()方法。 我们可以覆盖其他方法或添加新方法,但为了演示目的,我们会保持简单。

根据定义,匿名内部类必须是一个表达式,它是以分号结束(作为任何语句)的语句的一部分。 表达式由以下内容组成:

  • new 操作符
  • 实现接口的名称(后跟表示默认构造函数的括号())或扩展类的构造函数(后者是我们的例子,扩展类是Vehicle)
  • 具有方法声明的类主体(匿名内部类的主体中不允许使用语句)

与任何内部类一样,匿名内部类可以访问封闭类的任何成员,并可以捕获其变量的值。 为了能够做到这一点,必须将这些变量声明为final。 否则,它们会隐式变为最终,这意味着它们的值无法更改(如果您尝试更改此值,则一个好的现代IDE会警告您违反此约束)。

使用这些功能,我们可以修改我们的示例代码,并为新实现的getSpeedMph()方法提供更多输入数据,而不将它们作为方法参数传递:

public static void main(String... arg) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Vehicle(vehicleWeightPounds, engineHorsePower){
public double getSpeedMph(double timeSec){
double v = 2.0 * engineHorsePower * 746;
v = v * timeSec * 32.174 / vehicleWeightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
};
System.out.println("Vehicle speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

如果接口只有一个抽象方法(称为功能接口),一种特殊类型的匿名内部类,称为lambda表达式,它允许您使用较短的表示法,但提供接口实现。 我们将在下一章讨论功能接口和lambda表达式。

还有更多…

内部类是非静态嵌套类。 Java还允许您创建一个静态嵌套类,当内部类不需要访问封闭类的非公共属性和方法时,可以使用该类。 这是一个示例(关键字static添加到Engine类):

public class Vehicle {
private Engine engine;
public Vehicle(int weightPounds, int horsePower) {
this.engine = new Engine(horsePower, weightPounds);
}
public double getSpeedMph(double timeSec){
return this.engine.getSpeedMph(timeSec);
}
private static class Engine {
private int horsePower;
private int weightPounds;
private Engine(int horsePower, int weightPounds) {
this.horsePower = horsePower;
}
private double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}
}

也可以看看

请参阅本章中的以下内容:
功能齐全

使用继承和组合使设计可扩展

在本文中,您将了解两个重要的OOD概念,即继承和多态,这些概念已经在前面的配方示例中提到过。

做好准备

继承是一个类扩展(并且可选地,覆盖)另一个类的属性和/或方法的能力。 扩展类称为基类,超类或父类。 类的新扩展称为子类或子类。

多态性是将基类用作对其子类的对象的引用的类型的能力。

为了展示这两个概念的力量,让我们创建代表汽车和卡车的类,每个类都具有重量,发动机功率和最大负载可达到的速度(作为时间的函数)。 此外,在这种情况下,汽车将以乘客数量为特征,而卡车的重要特征将是其有效载荷。

怎么做…

1.看看Vehicle类:

public class Vehicle {
private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}

2.创建一个名为Car的子类:

public class Car extends Vehicle {
private int passengersCount;
public Car(int passengersCount, int weightPounds,
int horsepower){
super(weightPounds, horsePower);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount;
}
}

3.创建另一个名为Truck的子类:

public class Truck extends Vehicle {
private int payload;
public Truck(int payloadPounds, int weightPounds,
int horsePower){
super(weightPounds, horsePower);
this.payload = payloadPounds;
}
public int getPayload() {
return this.payload;
}
}

由于基类Vehicle既没有隐式或显式构造函数也没有参数(因为我们选择使用只带参数的显式构造函数),我们必须调用基类构造函数super()作为每个子类构造函数的第一行;

怎么运行的…

我们来写一个测试程序:

public static void main(String... arg) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Passengers count=" +
((Car)vehicle).getPassengersCount());
System.out.println("Car speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
vehicle = new Truck(3300, vehicleWeightPounds, engineHorsePower);
System.out.println("Payload=" +
((Truck)vehicle).getPayload() + " pounds");
System.out.println("Truck speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

请注意,基类Vehicle的引用载体指向子类Car的对象。 这可以通过多态来实现,根据该多态,对象在其继承行中具有每个类的类型(包括所有接口,稍后我们将讨论)。

如前面的示例所示,需要对子类类型强制转换此类引用,以便调用仅存在于子类中的方法。

如果我们运行前面的示例,结果将如下所示:

Passengers count=4
Car speed (10.0 sec) = 117.0 mph
Payload=3300 pounds
Truck speed (10.0 sec) = 117.0 mph

我们不应该惊讶地看到同样的速度,即117.0英里每小时,计算汽车和卡车 - 因为相同的重量和发动机功率用于计算每个的速度。 但是,直观地说,我们认为重载卡车不应该达到与汽车相同的速度。 为了验证这一点,我们需要在getSpeedMph()方法中包括汽车(乘客及其行李)和卡车(带有效载荷)的总重量。 一种方法是在每个子类中覆盖基类Vehicle的getSpeedMph()方法

现在,将carPower和weightPounds属性以及以下方法添加到Car类(我们假设行李箱的乘客平均重量为250磅):

public double getSpeedMph(double timeSec) {
int weight = this.weightPounds + this.passengersCount * 250;
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / weight;
return Math.round(Math.sqrt(v) * 0.68);
}

另外,将horsePower和weightPounds属性以及以下方法添加到theTruck类:

public double getSpeedMph(double timeSec) {
int weight = this.weightPounds + this.payload;
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / weight;
return Math.round(Math.sqrt(v) * 0.68);
}

这两个添加的结果(如果我们运行相同的测试类)将如下:

Passengers count=4
Car speed (10.0 sec) = 105.0 mph
Payload=3300 pounds
Truck speed (10.0 sec) = 86.0 mph

注:修改时应把Vehicle 中的int weightPounds,horsePower;改为public

这些结果证实了我们的直觉:满载的汽车或卡车没有达到与空载相同的速度。

子类中的新方法覆盖基类Vehicle的getSpeedMph(),尽管我们通过基类引用访问它:

Vehicle vehicle = new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Car speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");

重写的方法是动态绑定的,这意味着方法调用的上下文由所引用的实际对象的类型决定。 因为在我们的示例中,引用载体指向子类Car的对象,所以vehicle.getSpeedMph()构造调用子类的方法,而不是基类的方法。

两个新方法中存在明显的代码冗余,我们可以通过在基类中创建方法来重构,即Vehicle:

protected double getSpeedMph(double timeSec, int weightPounds) {
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}

以下是Car子类的getSpeedMph()方法现在的样子:

public double getSpeedMph(double timeSec) {
int weightPounds = this.weightPounds + this.passengersCount * 250;
return getSpeedMph(timeSec, weightPounds);
}

这是它在Truck子类中的显示方式:

public double getSpeedMph(double timeSec) {
int weightPounds = this.weightPounds + this.payload;
return getSpeedMph(timeSec, weightPounds);
}

现在我们需要通过添加强制转换来修改测试类。 否则,将出现运行时错误,因为基类Vehicle中不存在getSpeedMph(int timeSec)方法:

public static void main(String... arg) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Passengers count=" +
((Car)vehicle).getPassengersCount());
System.out.println("Car speed (" + timeSec + " sec) = " +
((Car)vehicle).getSpeedMph(timeSec) + " mph");
vehicle = new Truck(3300, vehicleWeightPounds, engineHorsePower);
System.out.println("Payload=" +
((Truck)vehicle).getPayload() + " pounds");
System.out.println("Truck speed (" + timeSec + " sec) = " +
((Truck)vehicle).getSpeedMph(timeSec) + " mph");
}
}

正如您所料,测试类产生相同的值:

Passengers count=4
Car speed (10.0 sec) = 105.0 mph
Payload=3300 pounds
Truck speed (10.0 sec) = 86.0 mph

为简化测试代码,我们可以放弃转换并编写以下代码:

public static void main(String... arg) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Car car = new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Passengers count=" + car.getPassengersCount());
System.out.println("Car speed (" + timeSec + " sec) = " +
car.getSpeedMph(timeSec) + " mph");
Truck truck = new Truck(3300, vehicleWeightPounds, engineHorsePower);
System.out.println("Payload=" + truck.getPayload() + " pounds");
System.out.println("Truck speed (" + timeSec + " sec) = " +
truck.getSpeedMph(timeSec) + " mph");
}

此代码生成的速度值保持不变。

然而,有一种更简单的方法可以达到同样的效果。 我们可以将getMaxWeightPounds()方法添加到基类和每个子类中。 Car类现在看起来如下:

public class Car extends Vehicle {
private int passengersCount, weightPounds;
public Car(int passengersCount, int weightPounds, int horsePower){
super(weightPounds, horsePower);
this.passengersCount = passengersCount;
this.weightPounds = weightPounds;
}
public int getPassengersCount() {
return this.passengersCount;
}
public int getMaxWeightPounds() {
return this.weightPounds + this.passengersCount * 250;
}
}

以下是Truck类的新版本的外观:

public class Truck extends Vehicle {
private int payload, weightPounds;
public Truck(int payloadPounds, int weightPounds, int horsePower) {
super(weightPounds, horsePower);
this.payload = payloadPounds;
this.weightPounds = weightPounds;
}
public int getPayload() { return this.payload; }
public int getMaxWeightPounds() {
return this.weightPounds + this.payload;
}
}

我们还需要将getMaxWeightPounds()方法添加到基类中,以便它可以用于速度计算:

public abstract class Vehicle {
private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public abstract int getMaxWeightPounds();
public double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / getMaxWeightPounds();
return Math.round(Math.sqrt(v) * 0.68);
}
}

将一个抽象方法getMaxWeightPounds()添加到Vehicle类使该类成为抽象类。 这有一个积极的副作用:它强制执行每个子类中的getMaxWeightPounds()方法。
注:副作用即子类继承抽象类,必须覆盖其所有抽象方法

测试类保持不变并产生相同的结果:

Passengers count=4
Car speed (10.0 sec) = 105.0 mph
Payload=3300 pounds
Truck speed (10.0 sec) = 86.0 mph

对于相同的效果,更简单的代码更改 - 在基类的速度计算中使用最大权重。 如果我们回到类的原始版本,我们需要做的就是将最大权重传递给基类Vehicle的构造函数。 结果类将如下所示:

public class Car extends Vehicle {
private int passengersCount;
public Car(int passengersCount, int weightPounds, int horsepower){
super(weightPounds + passengersCount * 250, horsePower);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount; }
}

我们将乘客的重量加到我们传递给超类构造函数的值上; 这是这个子类中唯一的变化。 Truck子类中有类似的变化:

public class Truck extends Vehicle {
private int payload;
public Truck(int payloadPounds, int weightPounds, int horsePower) {
super(weightPounds + payloadPounds, horsePower);
this.payload = payloadPounds;
}
public int getPayload() { return this.payload; }
}

基类Vehicle保持不变:

public class Vehicle {
private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}

测试类不会更改并产生相同的结果:

Passengers count=4
Car speed (10.0 sec) = 105.0 mph
Payload=3300 pounds
Truck speed (10.0 sec) = 86.0 mph

最后一个版本 - 将最大权重传递给基类的构造函数 - 现在将成为进一步演示代码开发的起点。

组合使设计更具可扩展性

在前面的示例中,速度模型在Vehicle类的getSpeedMph()方法中实现。 如果我们需要使用不同的速度模型(例如,包括更多的输入参数并且更多地调整到某些驾驶条件),我们需要更改Vehicle类或创建新的子类来覆盖该方法。 在我们需要试验数十甚至数百种不同模型的情况下,这种方法变得站不住脚。

此外,在现实生活中,基于机器学习和其他先进技术的建模变得如此复杂和专业化,汽车加速建模由不同的团队而不是制造车辆的团队完成是很常见的。

为了避免车辆构建者和速度模型开发人员之间的子类和代码合并冲突的激增,我们可以使用组合创建更具可扩展性的设计。

组合是一种OOD原则,用于使用不属于继承层次结构的类的行为来实现必要的功能

我们可以在getSpeedMph()方法中将SpeedModel类中的速度计算封装起来:

private Properties conditions;
public SpeedModel(Properties drivingConditions){
this.drivingConditions = drivingConditions;
}
public double getSpeedMph(double timeSec, int weightPounds,
int horsePower){
String road = drivingConditions.getProperty("roadCondition","Dry");
String tire = drivingConditions.getProperty("tireCondition","New");
double v = 2.0 * horsePower * 746;
v = v * timeSec * 32.174 / weightPounds;
return Math.round(Math.sqrt(v) * 0.68)
- (road.equals("Dry") ? 2 : 5)
- (tire.equals("New") ? 0 : 5);
}
}

可以创建此类的对象,然后在Vehicle类上设置:

public class Vehicle {
private SpeedModel speedModel;
private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public void setSpeedModel(SpeedModel speedModel){
this.speedModel = speedModel;
}
public double getSpeedMph(double timeSec){
return this.speedModel.getSpeedMph(timeSec,
this.weightPounds, this.horsePower);
}
}

因此,测试类可能如下所示:

public static void main(String... arg) {
double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Properties drivingConditions = new Properties();
drivingConditions.put("roadCondition", "Wet");
drivingConditions.put("tireCondition", "New");
SpeedModel speedModel = new SpeedModel(drivingConditions);
Car car = new Car(4, vehicleWeight, horsePower);
car.setSpeedModel(speedModel);
System.out.println("Car speed (" + timeSec + " sec) = " +
car.getSpeedMph(timeSec) + " mph");
}

结果如下:

Car speed (10.0 sec) = 100.0 mph

我们在单独的类中隔离了速度计算功能,现在可以修改或扩展它,而无需更改Vehicle层次结构的任何类。 这就是组合设计原则允许您在不改变其实现的情况下更改Vehicle类及其子类的行为的方式。

在下一个方案中,我们将展示界面的OOD概念如何释放组合和多态的更多力量,使设计更简单,更具表现力。

也可以看看

请参阅本章中的以下配方:

  • 编码到接口
  • 使用枚举来表示常量实体

编码到接口

在本文中,您将了解最后一个OOD概念,称为Interface,并进一步练习组合和多态的使用以及内部类和继承。

做好准备

在这种情况下,接口是一种引用类型,它定义了在实现接口的类中可以看到的方法的签名。 它是客户端可访问的功能的公共面,因此通常称为应用程序接口(API)。 它支持多态性和组合,从而促进更灵活和可扩展的设计。

接口是隐式抽象的,这意味着它无法实例化(不能仅基于接口创建对象)。 它仅用于包含抽象方法(无主体)。 现在,从Java 8开始,可以向接口添加默认和私有方法 - 我们将在以下方法中讨论这个功能。

每个接口都可以扩展多个其他接口,与类继承类似,继承扩展接口的所有方法。

怎么做…

1.创建将描述API的接口:

public interface SpeedModel {
double getSpeedMph(double timeSec, int weightPounds,
int horsePower);
}
public interface Vehicle {
void setSpeedModel(SpeedModel speedModel);
double getSpeedMph(double timeSec);
}
public interface Car extends Vehicle {
int getPassengersCount();
}
public interface Truck extends Vehicle {
int getPayloadPounds();
}

2.使用工厂,它是生成实现某些接口的对象的类。 工厂是用于创建对象的模式的实现,而无需指定所创建对象的确切类 - 仅指定接口,而不是调用构造函数。 当对象创建实例需要复杂的过程和/或重要的代码重复时,它尤其有用

在我们的例子中,有一个FactoryVehicle类可以为Vehicle,Car和Truck接口创建对象,FactorySpeedModel类将为SpeedModel接口生成对象。 这样的API将允许您编写以下代码:

public static void main(String... arg) {
double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Properties drivingConditions = new Properties();
drivingConditions.put("roadCondition", "Wet");
drivingConditions.put("tireCondition", "New");
SpeedModel speedModel = FactorySpeedModel
.generateSpeedModel(drivingConditions);
Car car = FactoryVehicle.buildCar(4, vehicleWeight,
horsePower);
car.setSpeedModel(speedModel);
System.out.println("Car speed (" + timeSec + " sec) = " +
car.getSpeedMph(timeSec) + " mph");
}

3.观察代码行为是否相同:

Car speed (10.0 sec) = 100.0 mph

但是,设计更具可扩展性。

怎么运行的…

我们已经看到了SpeedModel类的一种可能实现。 这是在FactorySpeedModel类中执行此操作的另一种方法:

public class FactorySpeedModel {
public static SpeedModel generateSpeedModel(
Properties drivingConditions){
//if drivingConditions includes "roadCondition"="Wet"
return new SpeedModelWet(...);
//if drivingConditions includes "roadCondition"="Dry"
return new SpeedModelDry(...);
}
private class SpeedModelWet implements SpeedModel{
public double getSpeedMph(double timeSec, int weightPounds,
int horsePower){...}
}
private class SpeedModelDry implements SpeedModel{
public double getSpeedMph(double timeSec, int weightPounds,
int horsePower){...}
}
}

为简洁起见,我们将注释(作为伪代码)和…符号而不是代码。 如您所见,工厂类可能隐藏许多不同的私有和静态嵌套类,每个类都包含特定驱动条件的专用模型。 每种型号都带来不同的结果

FactoryVehicle类的实现可能如下所示:

public class FactoryVehicle {
public static Car buildCar(int passengersCount, int weightPounds,
int horsePower){
return new CarImpl(passengersCount, weightPounds, horsePower);
}
public static Truck buildTruck(int payloadPounds, int weightPounds,
int horsePower){
return new TruckImpl(payloadPounds, weightPounds, horsePower);
}
class CarImpl extends VehicleImpl implements Car {
private int passengersCount;
private CarImpl(int passengersCount, int weightPounds,
int horsePower){
super(weightPounds + passengersCount * 250, horsePower);
this.passengersCount = passengersCount;
}

public int getPassengersCount() {
return this.passengersCount;
}
}
class TruckImpl extends VehicleImpl implements Truck {
private int payloadPounds;
private TruckImpl(int payloadPounds, int weightPounds,
int horsePower){
super(weightPounds+payloadPounds, horsePower);
this.payloadPounds = payloadPounds;
}
public int getPayloadPounds(){ return payloadPounds; }
}
abstract class VehicleImpl implements Vehicle {
private SpeedModel speedModel;
private int weightPounds, horsePower;
private VehicleImpl(int weightPounds, int horsePower){
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public void setSpeedModel(SpeedModel speedModel){
this.speedModel = speedModel;
}
public double getSpeedMph(double timeSec){
return this.speedModel.getSpeedMph(timeSec, weightPounds,
horsePower);
}
}
}

如您所见,接口描述了如何调用对象行为; 它还允许您为不同的请求(和提供的值)生成不同的实现,而无需更改主应用程序的代码。

这里写图片描述

也可以看看

请参阅本章中的以下配方:

  • 使用默认和静态方法创建接口
  • 使用私有方法创建接口

使用默认和静态方法创建接口

在本文中,您将了解Java 8中首次引入的两个新功能:接口中的默认和静态方法。

做好准备

默认方法允许您向接口添加新功能,而无需更改已实现此接口的类。 该方法称为默认方法,因为它在类未实现方法时提供功能。 但是,如果类实现它,那么接口的默认实现将被类实现忽略并覆盖

在接口中使用静态方法可以提供与类中的静态方法相同的功能。 与类静态方法(可以在没有类实例化时调用)一样,也可以通过在其前面添加接口的名称来调用接口静态方法。

静态接口方法不能被任何类覆盖,包括实现此接口的类,并且它不能隐藏任何类的任何静态方法,包括实现此接口的类。

到目前为止,我们已经创建了一个惊人的软件来计算车辆的速度。 如果该程序变得流行(应该如此),那么它可以被喜欢重量单位公制系统的读者使用。 为了满足这种需求 - 在我们的速度计算软件变得流行之后 - 我们决定为Truck接口添加更多方法; 但是,我们不想打破由其他公司创建的FactoryVehicle的现有实现。

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/82354567
今日推荐