Dart 中的整洁代码

此仓库是对 ryanmcdermott/clean-code-javascript 的 Dart 语言改编。

简介

Humorous image of software quality estimation as a count of how many expletives you shout when reading code

来自 Robert C. Martin 的著作 Clean Code 的软件工程原理,适用于 Dart。这不是风格指南。它是一份用于在 Dart 中编写可读、可重用和可重构软件的指南。

并非所有在此的原则都必须严格遵守,甚至更少的原则会得到普遍同意。这些只是指导方针,仅此而已,但它们是由《整洁代码》作者多年集体经验所沉淀下来的。

我们的软件工程技术至今只有 50 多年的历史,我们仍在学习很多东西。当软件架构像建筑本身一样古老时,也许那时我们才会有更硬性的规则要遵循。目前,请将这些指南作为评估您和您的团队所编写 Dart 代码质量的试金石。

还有一件事:了解这些并不能让你立即成为一名更好的软件开发人员,而且工作多年也不意味着你不会犯错。每一段代码都是从初稿开始的,就像湿泥土被塑造成最终的形状。最后,我们在与同行评审时会一点点打磨掉不完美之处。不要因为需要改进的初稿而自责。去责备代码吧!

变量

使用有意义且可读的变量名

糟糕

final yyyymmdstr = DateFormat('yyyy/MM/dd').format(DateTime.now());

final currentDate = DateFormat('yyyy/MM/dd').format(DateTime.now());

⬆ 返回顶部

对同类变量使用相同的词汇

糟糕

getUserInfo();
getClientData();
getCustomerRecord();

getUser();

⬆ 返回顶部

使用可搜索的名称

我们将阅读比我们编写的代码多得多的代码。我们编写的代码具有可读性和可搜索性非常重要。通过*不*命名那些对理解我们的程序有意义的变量,我们会伤害到读者。使您的名称可搜索。

糟糕

// What the heck is 86400000 for?
Future.delayed(Duration(milliseconds: 86400000), blastOff);

// Declare them as final and lowerCamelCase.
// millisecondsPerDay is int, because the type is inferred.
final millisecondsPerDay = 86400000;

Future.delayed(Duration(milliseconds: millisecondsPerDay), blastOff);

⬆ 返回顶部

使用解释性变量

糟糕

final address = <String>['One Infinite Loop', 'Cupertino', '95014'];
saveCityZipCode(address[1], address[2]);

final address = <String>['One Infinite Loop', 'Cupertino', '95014'];
final city = address[1];
final zipCode = address[2];
saveCityZipCode(city, zipCode);

⬆ 返回顶部

避免心智映射

明确优于隐晦。

糟糕

final locations = <String>['Austin', 'New York', 'San Francisco'];
locations.forEach((l) {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  // Wait, what is `l` for again?
  dispatch(l);
});

final locations = <String>['Austin', 'New York', 'San Francisco'];
locations.forEach((location) {
  doStuff();
  doSomeOtherStuff();
  // ...
  // ...
  // ...
  dispatch(location);
});

⬆ 返回顶部

不要添加不必要的上下文

如果您的类/对象名称能说明问题,请不要在变量名中重复。

糟糕

final car = Car(
  carMake: 'Honda',
  carModel: 'Accord',
  carColor: 'Blue',
);

void paintCar(Car car, String color) {
  car.carColor = color;
}

final car = Car(
  make: 'Honda',
  model: 'Accord',
  color: 'Blue',
);

void paintCar(Car car, String color) {
  car.color = color;
}

⬆ 返回顶部

使用默认参数代替短路或条件语句

默认参数通常比短路求值更清晰。请注意,如果您使用它们,您的函数将只为 null 提供默认值。

糟糕

void createMicrobrewery({String? name}) {
  final breweryName = name ?? 'Hipster Brew Co.';
  // ...
}

void createMicrobrewery({String breweryName = 'Hipster Brew Co.'}) {
  // ...
}

⬆ 返回顶部

函数

函数参数(最好是 2 个或更少)

限制函数参数的数量极其重要,因为它使测试函数更容易。超过三个会导致组合爆炸,您必须测试每种单独参数的各种不同情况。

一个或两个参数是理想情况,三个应尽量避免。任何超过三个的都应该合并。通常,如果您有三个以上的参数,那么您的函数可能承担了过多的责任。在某些情况下并非如此,但大多数时候一个更高级别的对象足以作为参数。

为了清楚地表明函数期望的属性,您可以使用命名参数。它们有几个优点

  1. 当有人查看函数签名时,可以立即清楚正在使用哪些属性。
  2. 如果属性是 required 的,Linters 可以警告您有关未使用属性的警告。

糟糕

void createMenu(String title, String body, String buttonText, bool cancellable) {
  // ...
}

void createMenu({
  required String title,
  required String body,
  required String buttonText,
  required bool cancellable,
}) {
  // ...
}

createMenu(
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true,
);

⬆ 返回顶部

函数应该只做一件事

这是迄今为止软件工程中最重要的一条规则。当函数做多于一件事时,它们更难组合、测试和理解。当您可以将函数隔离到一项操作时,它可以轻松地重构,并且您的代码将读起来更加清晰。如果您从本指南中只记住这一点,您将领先于许多开发人员。

糟糕

void emailClients(List<Client> clients) {
  for(final client in clients) {
    final clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  }
}

void emailActiveClients(List<Client> clients) {
  clients
    .where(isActiveClient)
    .forEach(email);
}

bool isActiveClient(Client client) {
  final clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

⬆ 返回顶部

函数名应该说明其功能

糟糕

void addToDate(DateTime date, int months) {
  // ...
}

final currentDate = DateTime.now();

// It's hard to tell from the function name what is added
addToDate(currentDate, 1);

void addMonthsToDate(int months, DateTime date) {
  // ...
}

final currentDate = DateTime.now();
addMonthsToDate(1, currentDate);

⬆ 返回顶部

函数应该只有一个抽象级别

当您有多个抽象级别时,您的函数通常会做太多事情。拆分函数可以提高可重用性并简化测试。

糟糕

void parseBetterAlternative(String code) {
  const regexes = [
    // ...
  ];

  final statements = code.split(' ');
  final tokens = [];
  for (final regex in regexes) {
    for (final statement in statements) {
      tokens.add( /* ... */ );
    }
  }

  final ast = <Node>[];
  for (final token in tokens) {
    ast.add( /* ... */ );
  }

  for (final node in ast) {
    // parse...
  }
}

List<String> tokenize(String code) {
  const regexes = [
    // ...
  ];

  final statements = code.split(' ');
  final tokens = <String>[];
  for (final regex in regexes) {
    for (final statement in statements) {
      tokens.add( /* ... */ );
    }
  }

  return tokens;
}

List<Node> lexer(List<String> tokens) {
  final ast = <Node>[];
  for (final token in tokens) {
    ast.add( /* ... */ );
  }
  
  return ast;
}

void parseBetterAlternative(String code) {
  final tokens = tokenize(code);
  final ast = lexer(tokens);
  for (final node in ast) {
    // parse...
  }
}

⬆ 返回顶部

删除重复代码

尽您最大的努力避免重复代码。重复代码不好,因为它意味着如果您需要更改某项逻辑,就有不止一个地方需要修改。

想象一下,如果您经营一家餐厅并跟踪您的库存:所有番茄、洋葱、大蒜、香料等。如果您有多个清单来跟踪这些,那么当您上出一道菜时,所有清单都必须更新。如果您只有一个清单,那么只有一个地方需要更新!

通常,您会有重复的代码,因为您有两个或更多略有不同的东西,它们有很多共同点,但它们的差异迫使您拥有两个或更多单独的函数来完成大部分相同的事情。删除重复代码意味着创建一个抽象,该抽象可以使用一个函数/模块/类来处理这组不同的事物。

正确获取抽象至关重要,这就是为什么您应该遵循“类”部分中概述的 SOLID 原则。糟糕的抽象可能比重复代码更糟糕,所以要小心!话虽如此,如果您能做出好的抽象,那就去做吧!不要重复自己,否则每次想更改一件事时,您都会发现自己需要更新多个地方。

糟糕

Widget buildDeveloperCard(Developer developer) {
  return CustomCard(
    expectedSalary: developer.calculateExpectedSalary(),
    experience: developer.getExperience(),
    projectsLink: developer.getGithubLink(),
  );
}

Widget buildManagerCard(Manager manager) {
  return CustomCard(
    expectedSalary: manager.calculateExpectedSalary(),
    experience: manager.getExperience(),
    projectsLink: manager.getMBAProjects(),
  );
}

Widget buildEmployeeCard(Employee employee) {
  String projectsLink;

  switch (employee.runtimeType) {
    case Manager:
      projectsLink = manager.getMBAProjects();
      break;
    case Developer:
      projectsLink = developer.getGithubLink();
      break;
  }

  return CustomCard(
    expectedSalary: employee.calculateExpectedSalary(),
    experience: employee.getExperience(),
    projectsLink: projectsLink,
  );
}

⬆ 返回顶部

不要将标志用作函数参数

标志告诉您的用户此函数不止做一件事。函数应该只做一件事。如果您的函数基于布尔值遵循不同的代码路径,请将它们拆分。

糟糕

void createFile(String name, bool temp) {
  if (temp) {
    File('./temp/${name}').create();
  } else {
    File(name).create();
  }
}

void createFile(String name) {
  File(name).create();
}

void createTempFile(String name) {
  File('./temp/${name}').create();
}

⬆ 返回顶部

避免副作用(第一部分)

如果函数除了接收一个值并返回另一个值或值之外还执行任何其他操作,那么它就会产生副作用。副作用可能是写入文件、修改全局变量或意外地将您的所有钱转给陌生人。

现在,您确实需要在程序中偶尔产生副作用。就像前面的例子一样,您可能需要将内容写入文件。您想要做的是集中处理您执行此操作的位置。不要让多个函数和类写入特定文件。让一个服务来做这件事。只有一个。

关键在于避免常见的陷阱,例如在没有任何结构的情况下共享对象状态,使用可以被任何东西写入的可变数据类型,以及没有集中处理副作用的发生位置。如果您能做到这一点,您将比绝大多数其他程序员更快乐。

糟糕

// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
dynamic name = 'Ryan McDermott';

void splitIntoFirstAndLastName() {
  name = name.split(' ');
}

splitIntoFirstAndLastName();

print(name); // ['Ryan', 'McDermott'];

List<String> splitIntoFirstAndLastName(name) {
  return name.split(' ');
}

final name = 'Ryan McDermott';
final newName = splitIntoFirstAndLastName(name);

print(name); // 'Ryan McDermott';
print(newName); // ['Ryan', 'McDermott'];

⬆ 返回顶部

避免副作用(第二部分)

在 Dart 中,有些值是不可变的(immutable),有些是可变的(mutable)。对象和数组是两种可变值,因此在将它们作为参数传递给函数时,需要谨慎处理。Dart 函数可以更改对象的属性或修改数组的内容,这很容易导致其他地方出现错误。

假设有一个函数接受一个数组参数,该参数代表购物车。如果该函数修改了该购物车数组——例如,通过添加要购买的商品——那么使用相同 cart 数组的任何其他函数都将受到此添加的影响。这可能很棒,但它也可能很糟糕。让我们想象一个糟糕的情况

用户点击“购买”按钮,该按钮会调用 purchase 函数,该函数会启动网络请求并将 cart 数组发送到服务器。由于网络连接不佳,purchase 函数必须不断重试请求。现在,如果在此期间用户意外点击了他们实际上不想要的商品的“添加到购物车”按钮,然后再网络请求开始呢?如果发生这种情况并且网络请求开始,那么该购买函数将发送意外添加的商品,因为 cart 数组已被修改。

一个很好的解决方案是,addItemToCart 函数应始终克隆 cart,对其进行编辑,然后返回克隆。这将确保仍在使用旧购物车的函数不受更改的影响。

需要提及此方法的两个注意事项

  1. 可能存在您确实想要修改输入对象的情况,但当您采用这种编程实践时,您会发现这些情况相当罕见。大多数事情都可以重构为没有副作用!

  2. 克隆大型对象在性能方面可能会非常昂贵。幸运的是,这在实践中不是一个大问题,因为有 很棒的库 允许这种编程方法快速且不像手动克隆对象和数组那样占用内存。

糟糕

void addItemToCart(List<int> cart, int item) {
  cart.add(item);
} 

final cart = [1, 2];
addItemToCart(cart, 3);

print(cart); // [1, 2, 3]

List<int> addItemToCart(List<int> cart, int item) {
  return [...cart, item];
}

final cart = [1, 2];
final newCart = addItemToCart(cart, 3);

print(cart); // [1, 2]
print(newCart); // [1, 2, 3]

⬆ 返回顶部

偏好函数式编程而非命令式编程

Dart 不是像 Haskell 那样的函数式语言,但它具有函数式风格。函数式语言可以更清晰、更易于测试。在可能的情况下,请偏好这种编程风格。

糟糕

final programmerOutput = <Programmer>[
  Programmer(name: 'Uncle Bobby', linesOfCode: 500),
  Programmer(name: 'Suzie Q', linesOfCode: 1500),
  Programmer(name: 'Jimmy Gosling', linesOfCode: 150),
  Programmer(name: 'Gracie Hopper', linesOfCode: 1000),
];

var totalOutput = 0;

for (var i = 0; i < programmerOutput.length; i++) {
  totalOutput += programmerOutput[i].linesOfCode;
}

final programmerOutput = <Programmer>[
  Programmer(name: 'Uncle Bobby', linesOfCode: 500),
  Programmer(name: 'Suzie Q', linesOfCode: 1500),
  Programmer(name: 'Jimmy Gosling', linesOfCode: 150),
  Programmer(name: 'Gracie Hopper', linesOfCode: 1000),
];

final totalOutput = programmerOutput.fold<int>(
    0, (previousValue, programmer) => previousValue + programmer.linesOfCode);

⬆ 返回顶部

封装条件语句

糟糕

if (programmer.language == 'dart' && programmer.projectsList.isNotEmpty) {
  // ...
}

bool isValidDartProgrammer(Programmer programmer) {
  return programmer.language == 'dart' && programmer.projectsList.isNotEmpty;
}

if (isValidDartProgrammer(programmer)) {
  // ...
}

⬆ 返回顶部

避免负面条件

糟糕

bool isFileNotValid(File file) {
  // ...
}

if (!isFileNotValid(file)) {
  // ...
}

bool isFileValid(File file) {
  // ...
}

if (isFileValid(file)) {
  // ...
}

⬆ 返回顶部

避免条件语句

这似乎是一项不可能完成的任务。第一次听到这个时,大多数人会说:“我该如何不使用 if 语句来做任何事情?”答案是,在许多情况下,您可以使用多态性来实现相同的任务。第二个问题通常是:“好吧,那很好,但我为什么要这样做呢?”答案是我们之前学到的一个整洁代码概念:函数应该只做一件事。当类和函数包含 if 语句时,您就是在告诉用户您的函数做不止一件事。记住,只做一件事。

糟糕

class Airplane {
  // ...
  double getCruisingAltitude() {
    switch (type) {
      case '777':
        return getMaxAltitude() - getPassengerCount();
      case 'Air Force One':
        return getMaxAltitude();
      case 'Cessna':
        return getMaxAltitude() - getFuelExpenditure();
    }
  }
}

class Airplane {
  // ...
}

class Boeing777 extends Airplane {
  // ...
  double getCruisingAltitude() {
    return getMaxAltitude() - getPassengerCount();
  }
}

class AirForceOne extends Airplane {
  // ...
  double getCruisingAltitude() {
    return getMaxAltitude();
  }
}

class Cessna extends Airplane {
  // ...
  double getCruisingAltitude() {
    return getMaxAltitude() - getFuelExpenditure();
  }
}

⬆ 返回顶部

删除死代码

死代码和重复代码一样糟糕。代码库中没有理由保留它。如果它没有被调用,就删除它!如果您仍然需要它,它仍然安全地保存在您的版本历史记录中。

糟糕

Future<void> oldRequest(url) {
  // ...
}

Future<void> newRequest(url) {
  // ...
}

await newRequest();

Future<void> newRequest(url) {
  // ...
}

await newRequest();

⬆ 返回顶部

对象和数据结构

仅在必要时使用 getter 和 setter

与其它语言不同,在 Dart 中,建议仅在访问属性之前有一些逻辑时才使用 getter 和 setter。如果您只想获取或编辑属性,请不要使用它们。

糟糕

class BankAccount {
  // "_" configure as private
  int _balance;

  int get balance => _balance;

  set balance(int amount) => _balance = amount;

  BankAccount({
    int balance = 0,
  }) : _balance = balance;
}

final account = BankAccount();
account.balance = 100;

class BankAccount {
  int balance;
  // ...

  BankAccount({
    this.balance = 0,
    // ...
  });
}

final account = BankAccount();
account.balance = 100;

⬆ 返回顶部

使用私有方法和属性

如果一个方法或属性仅在类中使用,则必须是私有的。

糟糕

class Employee {
  String name;

  Employee({required this.name});
}

final employee = Employee(name: 'John Doe');
print(employee.name); // John Doe
employee.name = 'Uncle Bob';
print(employee.name); // Uncle Bob

class Employee {
  String _name;

  Employee({required String name}) : _name = name;
}

final employee = Employee(name: 'John Doe');
print(employee.name); // Can't access outside the class.

⬆ 返回顶部

使用方法链(级联表示法)

它可以使您的代码更具表现力,并且更简洁。因此,我建议使用方法链,看看您的代码会多么整洁。

糟糕

class Car {
  String make;
  String model;
  String color;

  Car({
    required this.make,
    required this.model,
    required this.color,
  });

  save() => print('$make, $model, $color');
}

final car = Car(make: 'Ford', model: 'F-150', color: 'red');
car.color = 'pink';
car.save();

class Car {
  String make;
  String model;
  String color;

  Car({
    required this.make,
    required this.model,
    required this.color,
  });

  save() => print('$make, $model, $color');
}

final car = Car(make: 'Ford', model: 'F-150', color: 'red')
  ..color = 'pink'
  ..save();

⬆ 返回顶部

偏好组合而非继承

正如 Gang of Four 在 Design Patterns 中著名的说法,在可能的情况下,您应该偏好组合而非继承。使用继承和使用组合都有很多好的理由。这条格言的主要观点是,如果您的想法本能地倾向于继承,请尝试思考组合是否能更好地模拟您的问题。在某些情况下是可以的。

您可能会想,“我什么时候应该使用继承?”这取决于您当前的问题,但这有一个不错的列表,说明了继承何时比组合更有意义

  1. 您的继承代表“is-a”关系,而不是“has-a”关系(Human->Animal vs. User->UserDetails)。
  2. 您可以从基类重用代码(人类可以像所有动物一样移动)。
  3. 您想通过更改基类来对派生类进行全局更改。(移动时更改所有动物的卡路里消耗)。

糟糕

class Employee {
  String name;
  String email;

  Employee({
    required this.name,
    required this.email,
  });

  // ...
}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
  String ssn;
  double salary;

  EmployeeTaxData({
    required this.ssn,
    required this.salary,
    required super.name,
    required super.email,
  });

  // ...
}

class EmployeeTaxData {
  String ssn;
  double salary;

  EmployeeTaxData({
    required this.ssn,
    required this.salary,
  });

  // ...
}

class Employee {
  String name;
  String email;
  EmployeeTaxData? taxData;

  Employee({
    required this.name,
    required this.email,
  });

  void setTaxData(String ssn, double salary) {
    taxData = EmployeeTaxData(ssn: ssn, salary: salary);
  }

  // ...
}

⬆ 返回顶部

SOLID

单一职责原则 (SRP)

正如《整洁代码》中所述,“一个类永远不应该有超过一个的修改原因”。试图在一个类中塞入大量功能是诱人的,就像您一次飞行只能携带一个行李箱一样。问题在于,您的类不会在概念上保持内聚,并且会给它带来许多修改的原因。最大程度地减少您需要更改类的次数很重要。这很重要,因为如果一个类中有太多的功能,并且您修改了其中的一部分,那么理解它将如何影响您代码库中的其他依赖模块就会变得很困难。

糟糕

class UserSettings {
  String user;
  
  UserSettings({
    required this.user,
  });

  void changeSettings(Settings settings) {
    if (verifyCredentials()) {
      // ...
    }
  }

  bool verifyCredentials() {
    // ...
  }
}

class UserAuth {
  String user;

  UserAuth({
    required this.user,
  });

  bool verifyCredentials() {
    // ...
  }
}

class UserSettings {
  String user;
  UserAuth auth;

  UserSettings({
    required this.user,
  }) : auth = UserAuth(user: user);

  void changeSettings(Settings settings) {
    if (auth.verifyCredentials()) {
      // ...
    }
  }
}

⬆ 返回顶部

开闭原则 (OCP)

正如 Bertrand Meyer 所述,“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。”但这又意味着什么呢?此原则基本上说明您应该允许用户在不更改现有代码的情况下添加新功能。

糟糕

double getArea(Shape shape) {
  if (shape is Circle) {
    return getCircleArea(shape);
  } else if (shape is Square) {
    return getSquareArea(shape);
  }
}

double getCircleArea(Shape shape) {
  // ...
}

double getSquareArea(Shape shape) {
  // ...
}

abstract class Shape {
  double getArea();
}

class Circle extends Shape {
  @override
  double getArea() {
    // ...
  }
}

class Square extends Shape {
  @override
  double getArea() {
    // ...
  }
}

// ...
final area = shape.getArea();

⬆ 返回顶部

里氏替换原则 (LSP)

这是一个听起来很吓人的术语,但它是一个非常简单的概念。它被正式定义为“如果 S 是 T 的子类型,那么类型 T 的对象可以被类型 S 的对象替换(即,类型 S 的对象可以替代类型 T 的对象),而不会改变该程序的任何期望属性(正确性、执行的任务等)。”这是一个更可怕的定义。

最好的解释是,如果您有一个父类和一个子类,那么基类和子类可以互换使用而不会得到错误的结果。这可能仍然令人困惑,所以让我们看看经典的 Square-Rectangle 示例。在数学上,正方形是长方形,但如果您使用继承通过“is-a”关系来建模它,您很快就会遇到麻烦。

糟糕

class Rectangle {
  double width;
  double height;

  Rectangle({
    this.width = 0,
    this.height = 0,
  });

  // setWidth e setHeight used just for example
  void setWidth(double value) => width = value;

  void setHeight(double value) => height = value;

  double getArea() {
    return width * height;
  }
}

class Square extends Rectangle {
  Square({
    super.width = 0,
    super.height = 0,
  });

  @override
  void setWidth(double value) {
    width = value;
    height = value;
  }

  @override
  void setHeight(double value) {
    width = value;
    height = value;
  }
}

final rectangles = [Rectangle(), Rectangle(), Square()];

for (final rectangle in rectangles) {
  rectangle.setWidth(4);
  rectangle.setHeight(5);

  final area = rectangle.getArea();
  print(area); // BAD: Returns 25 for Square. Should be 20.
}

abstract class Shape {
  double getArea();
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle({
    required this.width,
    required this.height,
  });

  @override
  double getArea() {
    return width * height;
  }
}

class Square extends Shape {
  double length;

  Square({
    required this.length,
  });

  @override
  double getArea() {
    return length * length;
  }
}

final rectangles = [
  Rectangle(width: 4, height: 5),
  Rectangle(width: 4, height: 5),
  Square(length: 4),
];

for (final rectangle in rectangles) {
  final area = rectangle.getArea();
  print(area); // Show the correct values: 20, 20, 16.
}

⬆ 返回顶部

接口隔离原则 (ISP)

ISP 规定“客户端不应被迫依赖于他们不使用的接口”。您应该始终创建更具体的接口,而不是只创建一个通用的接口。换句话说,如果您的类实现了接口,但使用了著名的 throw UnimplementedError(),那么它很可能没有遵守该原则。

糟糕

abstract class Book {
  int getNumberOfPages();
  void download();
}

class EBook implements Book {
  @override
  int getNumberOfPages() {
    // ...
  }

  @override
  String download() {
    // ...
  }
}

class PhysicalBook implements Book {
  @override
  int getNumberOfPages() {
    // ...
  }

  @override
  void download() {
    throw UnimplementedError(); // Physical book doesn't download.
  }
}

abstract class Book {
  int getNumberOfPages();
}

abstract class DownloadableBook {
  void download();
}

class EBook implements Book, DownloadableBook {
  @override
  int getNumberOfPages() {
    // ...
  }

  @override
  void download() {
    // ...
  }
}

class PhysicalBook implements Book {
  @override
  int getNumberOfPages() {
    // ...
  }
}

⬆ 返回顶部

依赖倒置原则 (DIP)

此原则陈述了两项基本内容

  1. 高层模块不应依赖于低层模块。两者都应依赖于抽象。
  2. 抽象不应依赖于细节。细节应依赖于抽象。

您可能已经看到此原则的一种实现形式,即依赖注入 (DI)。虽然它们不是完全相同的概念,但 DIP 使高层模块无需了解其低层模块的详细信息并进行设置。它可以通过 DI 来实现。这有一个巨大的好处,那就是它减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使您的代码难以重构。

糟糕

class InventoryRequester {
  void requestItem(item) {
    // ...
  }
}

class InventoryTracker {
  final requester = InventoryRequester(); // InventoryTracker depends on low-level module.
  List<String> items;

  InventoryTracker({
    required this.items,
  });

  void requestItems() {
    for (var item in items) {
      requester.requestItem(item);
    }
  }
}

final inventoryTracker = InventoryTracker(items: ['apples', 'bananas']);
inventoryTracker.requestItems();

class InventoryTracker {
  List<String> items;
  InventoryRequester requester;

  InventoryTracker({
    required this.items,
    required this.requester,
  });

  void requestItems() {
    for (var item in items) {
      requester.requestItem(item);
    }
  }
}

abstract class InventoryRequester {
  void requestItem(item);
}

class InventoryRequesterV1 implements InventoryRequester {
  @override
  void requestItem(item) {
    // ...
  }
}

class InventoryRequesterV2 implements InventoryRequester {
  @override
  void requestItem(item) {
    // ...
  }
}

// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one.
final inventoryTracker = InventoryTracker(
  items: ['apples', 'bananas'],
  requester: InventoryRequesterV2(),
);
inventoryTracker.requestItems();

⬆ 返回顶部

测试

测试比发货更重要。如果您没有测试或测试不足,那么每次发货代码时,您都不会确定自己没有破坏任何东西。决定测试的充分程度取决于您的团队,但实现 100% 的覆盖率(所有语句和分支)是您实现非常高的信心和开发人员安心的方式。

始终为您引入的每一项新功能/模块编写测试。如果您的首选方法是测试驱动开发 (TDD),那很好,但关键是要在发布任何功能或重构现有功能之前确保您达到了覆盖率目标。

每次测试一个概念

糟糕

import 'package:test/test.dart';

test('String', () {
  var string = 'foo,bar,baz';
  expect(string.split(','), equals(['foo', 'bar', 'baz']));

  string = '  foo ';
  expect(string.trim(), equals('foo'));
});

import 'package:test/test.dart';

group('String', () {
  test('.split() splits the string on the delimiter', () {
    final string = 'foo,bar,baz';
    expect(string.split(','), equals(['foo', 'bar', 'baz']));
  });

  test('.trim() removes surrounding whitespace', () {
    final string = '  foo ';
    expect(string.trim(), equals('foo'));
  });
});

⬆ 返回顶部

并发

使用 async/await 而不是 then

使用 async/await 可以使您的代码更简单、更易于理解。

糟糕

final albumTitle = await client
    .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'))
    .then((response) {
  // ...
  return title;
});

Future<String> getAlbumTitle() async {
  final response = await client
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  // ...

  return title;
}

⬆ 返回顶部

错误处理

抛出的错误是件好事!它们意味着运行时已成功识别出程序中的错误,并通过停止当前堆栈上的函数执行、终止进程并使用堆栈跟踪在控制台中通知您来告知您。

不要忽略捕获的错误

对捕获的错误不做任何处理,就无法修复或响应该错误。将错误记录到控制台(log)好不了多少,因为它们通常会消失在控制台中打印的内容中。如果您将任何代码块包装在 try/catch 中,这意味着您认为那里可能会发生错误,因此您应该有一个计划,或者在它发生时创建一个代码路径。

糟糕

try {
  functionThatMightThrow();
} catch (error) {
  print(error);
}

try {
  functionThatMightThrow();
} catch (e, s) {
  // Option 1:
  log('Error description...', error: e, stackTrace: s);
  // Option 2:
  notifyUserOfError(e, s);
  // Option 3:
  reportErrorToService(e, s);
}

不要忽略 Future 错误

如果您想使用 future/then,请记住处理错误。

糟糕

functionThatMightThrow().then((value) {
  // ...
}).onError((e, s) {
  print(e);
});

functionThatMightThrow().then((value) {
  // ...
}).onError((e, s) {
  // Option 1:
  log('Error description...', error: e, stackTrace: s);
  // Option 2:
  notifyUserOfError(e, s);
  // Option 3:
  reportErrorToService(e, s);
});

⬆ 返回顶部

格式化

格式化是主观的。就像这里的大多数规则一样,没有强制性的规则必须遵循。关键是*不要*争论格式。我建议您阅读 Effective Dart,其中有几条规则需要遵循,但没有什么是强制性的。

使用正确的资本化

糟糕

const DAYS_IN_WEEK = 7;

const Bands = ['AC/DC', 'Led Zeppelin', 'The Beatles'];

void restore_database() {}

class animal {}

typedef predicate<T> = bool Function(T value);

// lowerCamelCase for constant names
const daysInWeek = 7;
const bands = ['AC/DC', 'Led Zeppelin', 'The Beatles'];

// lowerCamelCase for functions
void restoreDatabase() {}

// UpperCamelCase for classes, enum types, typedefs, and type parameters
class Animal {}
typedef Predicate<T> = bool Function(T value);

⬆ 返回顶部

函数调用者和被调用者应该靠近

如果一个函数调用另一个函数,请将这些函数在源文件中保持垂直上的接近。理想情况下,将调用者放在被调用者的正上方。我们倾向于从上到下阅读代码,就像读报纸一样。因此,让您的代码也像那样阅读。

糟糕

class Smartphone {
  // ...

  String getOS() {
    // ...
  }

  void showPlatform() {
    final os = getOS();
    final chipset = getChipset();
    // ...
  }

  String getResolution() {
    // ...
  }

  void showSpecifications() {
    showPlatform();
    showDisplay();
  }

  String getChipset() {
    // ...
  }

  void showDisplay() {
    final resolution = getResolution();
    // ...
  }
}

class Smartphone {
  // ...

  void showSpecifications() {
    showPlatform();
    showDisplay();
  }

  void showPlatform() {
    final os = getOS();
    final chipset = getChipset();
    // ...
  }

  String getOS() {
    // ...
  }

  String getChipset() {
    // ...
  }

  void showDisplay() {
    final resolution = getResolution();
    // ...
  }

  String getResolution() {
    // ...
  }
}

⬆ 返回顶部

注释

只注释有业务逻辑复杂性的内容。

注释是一种歉意,而不是必需品。好的代码*大部分*是自文档化的。

糟糕

List<String> getCitiesNames(List<String> cities) {
  // Cities names list
  final citiesNames = <String>[];

  // Loop through every city
  for (final city in cities) {
    // Gets only the string before the comma
    final filteredCityName = city.split(',')[0];

    // Add the filtered city name
    citiesNames.add(filteredCityName);
  }

  // Returns the cities names list
  return citiesNames;
}

List<String> getCitiesNames(List<String> cities) {
  final citiesNames = <String>[];

  for (final city in cities) {
    // Gets only the string before the comma
    final filteredCityName = city.split(',')[0];

    citiesNames.add(filteredCityName);
  }

  return citiesNames;
}

⬆ 返回顶部

不要在代码库中留下注释掉的代码

版本控制的存在是有原因的。将旧代码保留在历史记录中。

糟糕

doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

doStuff();

⬆ 返回顶部

不要有日志注释

请记住,使用版本控制!不需要死代码、注释掉的代码,尤其是日志注释。使用 git log 来获取历史记录!

糟糕

/**
 * 2016-12-20: Removidas monads, não entendia elas (RM)
 * 2016-10-01: Melhoria utilizando monads especiais (JP)
 * 2016-02-03: Removido checagem de tipos (LI)
 * 2015-03-14: Adicionada checagem de tipos (JR)
 */
int combine(int a, int b) {
  return a + b;
}

int combine(int a, int b) {
  return a + b;
}

⬆ 返回顶部

避免位置标记

它们通常只会增加噪音。让函数和变量名以及正确的缩进和格式化为您的代码提供视觉结构。

糟糕

////////////////////////////////////////////////////////////////////////////////
// Programmer Instantiation
////////////////////////////////////////////////////////////////////////////////
final programmer = Programmer(
  name: 'Jack',
  linesOfCode: 500,
);

////////////////////////////////////////////////////////////////////////////////
// startProject implementation
////////////////////////////////////////////////////////////////////////////////
void startProject() {
  // ...
};

final programmer = Programmer(
  name: 'Jack',
  linesOfCode: 500,
);

void startProject() {
  // ...
};

⬆ 返回顶部

翻译

这在其他语言中也可用

⬆ 返回顶部

GitHub

查看 Github