让我们聊聊Dart中抽象类和接口这对"好兄弟"。很多刚接触面向对象的朋友容易把它们搞混,其实它们就像咖啡机的不同功能按钮——虽然都能出咖啡,但用法和场景大不相同。

一、先认识这两位主角

抽象类像是个半成品模板。比如我们定义"动物"这个抽象类时,知道所有动物都会叫,但不同动物叫声不同。这时候用抽象类就很合适:

// [技术栈: Dart]
abstract class Animal {
  void makeSound(); // 抽象方法,没有实现
  
  void eat() {      // 普通方法可以有实现
    print('咀嚼食物');
  }
}

class Cat extends Animal {
  @override
  void makeSound() {
    print('喵喵喵'); // 必须实现抽象方法
  }
}

接口则更像是一份合同。Dart中没有专门的interface关键字,任何类都可以作为接口使用:

// [技术栈: Dart]
class Flyable {
  void fly() => throw UnimplementedError();
}

class Bird implements Flyable {
  @override
  void fly() {
    print('展翅高飞'); // 必须实现接口所有方法
  }
}

二、什么时候用哪个

抽象类适合这些场景:

  1. 需要共享部分代码实现时
  2. 有共同的基础属性时
  3. 需要约束子类必须实现某些行为时

比如开发游戏时,各种武器可能有共同的属性,但攻击方式不同:

// [技术栈: Dart]
abstract class Weapon {
  final int damage;
  
  Weapon(this.damage);
  
  void attack(); // 攻击方式由子类决定
  
  String get description => '基础武器'; // 默认实现
}

class Sword extends Weapon {
  Sword() : super(10);
  
  @override
  void attack() => print('挥剑造成$damage点伤害');
  
  @override
  String get description => '锋利的剑';
}

接口更适合:

  1. 定义纯粹的行为契约
  2. 需要多重"继承"时
  3. 不关心具体实现细节时

比如我们既想让角色能攻击又能防御:

// [技术栈: Dart]
class Attackable {
  void attack() => throw UnimplementedError();
}

class Defendable {
  void defend() => throw UnimplementedError();
}

class Knight implements Attackable, Defendable {
  @override
  void attack() => print('骑士突刺');
  
  @override
  void defend() => print('举盾格挡');
}

三、实际开发中的平衡术

好的设计往往需要两者结合。比如Flutter框架中,StatefulWidget是抽象类,而TickerProvider是接口:

// [技术栈: Dart]
abstract class StatefulWidget extends Widget {
  // 有部分具体实现
  @override
  StatefulElement createElement() => StatefulElement(this);
  
  @protected
  State createState(); // 必须实现的抽象方法
}

mixin TickerProvider {
  Ticker createTicker(TickerCallback onTick);
}

class MyWidget extends StatefulWidget with TickerProvider {
  @override
  State createState() => _MyWidgetState();
  
  @override
  Ticker createTicker(TickerCallback onTick) {
    return Ticker(onTick);
  }
}

这种组合既保证了基础功能复用,又实现了灵活扩展。

四、避坑指南与最佳实践

  1. 不要过度设计:如果只是简单场景,直接使用具体类可能更好
  2. 命名要清晰:抽象类可以用Base前缀,接口用-able后缀
  3. 接口隔离原则:一个接口最好只定义一个职责

常见错误示例:

// [技术栈: Dart]
// 反例:抽象类包含太多具体实现
abstract class Vehicle {
  void startEngine() {
    // 几十行实现代码...
  }
  
  void drive(); // 唯一抽象方法
}

// 正例:拆分成更小的抽象
abstract class Engine {
  void start();
}

abstract class Drivable {
  void drive();
}

五、应用场景深度分析

在Flutter应用开发中,这种设计特别有用。比如实现不同主题:

// [技术栈: Dart]
abstract class AppTheme {
  Color get primaryColor;
  Color get backgroundColor;
  
  TextStyle get titleStyle => TextStyle(
    fontSize: 20,
    color: primaryColor,
  );
}

class LightTheme extends AppTheme {
  @override
  Color get primaryColor => Colors.blue;
  
  @override
  Color get backgroundColor => Colors.white;
}

class DarkTheme extends AppTheme {
  @override
  Color get primaryColor => Colors.blueAccent;
  
  @override
  Color get backgroundColor => Colors.black;
}

而在服务层,接口能更好地定义契约:

// [技术栈: Dart]
abstract class DataRepository {
  Future<List<Item>> fetchItems();
  Future<void> saveItem(Item item);
}

class ApiRepository implements DataRepository {
  @override
  Future<List<Item>> fetchItems() async {
    // 网络请求实现...
  }
  
  @override
  Future<void> saveItem(Item item) async {
    // 保存到API...
  }
}

六、技术优缺点对比

抽象类的优势:

  • 可以包含具体实现,减少重复代码
  • 更容易添加新方法而不破坏现有代码
  • 天然支持模板方法模式

缺点:

  • 单继承限制
  • 容易变得臃肿

接口的优势:

  • 支持多重实现
  • 更轻量级的契约定义
  • 更适合描述角色(Role)

缺点:

  • 所有方法都必须实现
  • 修改接口会破坏所有实现类

七、总结与建议

  1. 80%的情况下,优先考虑使用抽象类
  2. 当需要多重继承或纯粹定义契约时,选择接口
  3. 可以结合使用:用抽象类提供基础实现,用接口定义额外能力
  4. 保持每个抽象类/接口的职责单一

记住,好的设计不是非此即彼的选择题,而是根据实际需求找到最佳平衡点。就像做菜时盐和酱油的搭配,用对了才能做出美味佳肴。