1. 真实应用中的抽象:为什么需要它
在之前的讲座中,我们已经认识了抽象的概念,并看过了一些简单示例。现在看看这种方法在更真实的任务中如何发挥作用。 在真实项目中,几乎总要处理不同但相似的对象。例如,不同的支付方式、不同类型的交通工具、图形编辑器中的不同图形。如果不应用抽象,代码很快就会变成一堆“if-else”和复制粘贴。使用抽象——一切更规范、优雅,并且最重要的是,便于演进和维护。
抽象可以:
- 隐藏实现细节:通过统一接口与对象交互,而无需关心其内部实现。
- 避免代码重复:将共同行为与字段提取到基类。
- 便于扩展系统:添加新类型的对象无需重写旧代码。
- 让代码更灵活:可以在不改动其余系统的情况下替换一种实现为另一种实现。
让我们从不同领域看几个示例。
2. 示例 1:支付系统
任务说明
假设你在为一个网店编写模块。你的任务是实现多种支付方式的处理:银行卡、PayPal、加密货币。它们都需要“处理支付”,但细节各不相同。
抽象:类 Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// 抽象方法:具体如何处理支付——由子类决定
public abstract void process();
// 所有支付的通用方法
public void printAmount() {
System.out.println("Payment amount: " + amount + " CNY");
}
}
具体实现
public class CreditCardPayment extends Payment {
private String cardNumber;
public CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
public void process() {
System.out.println("处理银行卡支付: " + cardNumber);
// 这里可以是与银行的集成 :)
}
}
public class PaypalPayment extends Payment {
private String email;
public PaypalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public void process() {
System.out.println("处理 PayPal 支付,账户: " + email);
// 这里——调用 PayPal API
}
}
public class CryptoPayment extends Payment {
private String walletAddress;
public CryptoPayment(double amount, String walletAddress) {
super(amount);
this.walletAddress = walletAddress;
}
@Override
public void process() {
System.out.println("处理加密货币支付,钱包地址: " + walletAddress);
// 这里可以是区块链魔法
}
}
抽象的使用
import java.util.*;
public class PaymentDemo {
public static void main(String[] args) {
List<Payment> payments = new ArrayList<>();
payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
payments.add(new PaypalPayment(500.0, "user@example.com"));
payments.add(new CryptoPayment(0.05, "0xABCD..."));
for (Payment payment : payments) {
payment.printAmount();
payment.process();
System.out.println("---");
}
}
}
运行结果:
Payment amount: 1500.0 CNY
处理银行卡支付: 1234 5678 9012 3456
---
Payment amount: 500.0 CNY
处理 PayPal 支付,账户: user@example.com
---
Payment amount: 0.05 CNY
处理加密货币支付,钱包地址: 0xABCD...
---
优点:
- 可以在不修改旧代码的情况下添加新的支付方式(例如 Apple Pay)。
- 处理支付的代码不依赖于具体类型。
- 通用逻辑(例如通过 printAmount() 打印金额)集中在一个地方实现。
3. 示例 2:交通工具
任务说明
在游戏或模拟器中,你有多种交通工具:汽车、自行车、火车。它们都可以“移动”,但方式不同。有些需要加油,另一些则不需要。
抽象:类 Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// 并非所有交通工具都需要加油,默认——不需要
public void fuelUp() {
System.out.println(name + ": 不需要加油。");
}
}
具体实现
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " 在道路上行驶。");
}
@Override
public void fuelUp() {
System.out.println(name + ": 加注汽油。");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " 踩踏板前进。");
}
// 不重写 fuelUp —— 自行车不需要加油
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " 在轨道上飞驰。");
}
@Override
public void fuelUp() {
System.out.println(name + ": 加注柴油或使用电力。");
}
}
抽象的使用
import java.util.*;
public class TransportDemo {
public static void main(String[] args) {
List<Transport> vehicles = Arrays.asList(
new Car("Toyota"),
new Bicycle("Stels"),
new Train("Sapsan")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
运行结果:
Toyota 在道路上行驶。
Toyota: 加注汽油。
---
Stels 踩踏板前进。
Stels: 不需要加油。
---
Sapsan 在轨道上飞驰。
Sapsan: 加注柴油或使用电力。
---
优点:
- 可以以相同方式处理任何交通工具,无需检查其类型。
- 易于添加新的交通工具(例如电动滑板车)。
4. 示例 3:图形编辑器
任务说明
你在编写一个迷你图形编辑器。其中有直线、椭圆、多边形——它们都是“图形”,可以被绘制并调整大小,而且每种图形会以自己的方式实现这些操作。
抽象:类 Figure
public abstract class Figure {
protected String color = "black";
public abstract void draw();
public abstract void resize(double factor);
public void setColor(String color) {
this.color = color;
}
}
具体实现
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("绘制长度为 " + length + " 的直线,颜色为 " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("直线的新长度: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("绘制轴为 " + a + " 和 " + b + " 的椭圆,颜色为 " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("椭圆的新尺寸: " + a + " x " + b);
}
}
public class Polygon extends Figure {
private int sides;
public Polygon(int sides) {
this.sides = sides;
}
@Override
public void draw() {
System.out.println("绘制具有 " + sides + " 条边的多边形,颜色为 " + color);
}
@Override
public void resize(double factor) {
System.out.println("将具有 " + sides + " 条边的多边形的尺寸按 " + factor + " 进行缩放");
}
}
抽象的使用
import java.util.*;
public class EditorDemo {
public static void main(String[] args) {
List<Figure> figures = new ArrayList<>();
figures.add(new Line(10));
figures.add(new Ellipse(5, 3));
figures.add(new Polygon(6));
for (Figure f : figures) {
f.setColor("green");
f.draw();
f.resize(2);
System.out.println("---");
}
}
}
运行结果:
绘制长度为 10.0 的直线,颜色为 green
直线的新长度: 20.0
---
绘制轴为 5.0 和 3.0 的椭圆,颜色为 green
椭圆的新尺寸: 10.0 x 6.0
---
绘制具有 6 条边的多边形,颜色为 green
将具有 6 条边的多边形的尺寸按 2.0 进行缩放
---
优点:
- 可以把所有图形放在同一个列表中并以相同方式处理。
- 容易添加新的图形(例如星形或心形)。
- 通用方法(例如通过 setColor() 设置颜色)只需实现一次。
5. 抽象如何帮助简化代码
以上每个示例都有一个共同的模式:
- 基础抽象类定义契约(对象能做什么)。
- 具体子类实现细节。
- 使用抽象编写的代码不依赖对象的具体类型,使系统更加灵活且可扩展。
方法对比表
| 没有抽象(if-else) | 有抽象(OOP) |
|---|---|
| 大量基于类型的条件 | 出现新类型——就要改代码 |
| 逻辑重复 | 逻辑集中在一个地方 |
| 难以扩展 | 添加——很容易 |
| 难以测试 | 易于替换实现 |
6. 设计抽象时的常见错误
错误 №1:为抽象而抽象。
如果你只有一种对象类型且不计划扩展——就没必要使用抽象类。不要无故让代码复杂化。
错误 №2:抽象过于泛化。
如果基类过于“模糊”,子类可能除了名字外没有共同之处。例如,把所有东西都抽象成“Thing”。这会使维护和理解代码变得困难。
错误 №3:在子类中重复代码。
如果所有子类对某个方法的实现都相同,就应该把该实现上移到基类(让它变为非抽象)。
错误 №4:违反“由泛到专”的原则。
如果抽象类中出现了仅某个子类才需要的细节——说明抽象选错了。
错误 №5:忘记实现抽象方法。
如果没有在子类中实现所有抽象方法,编译器会迫使该类也成为抽象类。有时这会让人意外 :)
GO TO FULL VERSION