如何写出高质量代码:六招助你轻松成功

如何写出高质量代码:六招助你轻松成功

招式一 YAGNI (You ain’t gonna need it)

只在需要时才添加代码和功能,避免不必要的复杂性和浪费。

两种类别的 YAGNI

  1. 产品需求设计做减法,每次只实现最必要的功能和特性。(考量点:开发周期、产品复杂度、开发成本)
  2. 软件架构设计做减法,减少依赖的组件或服务。(考量点:稳定性、成本、维护难度)

招式二 KISS (Keep it simple and stupid)

复杂的代码往往难以理解和维护,因此应该尽可能地保持代码简单。

不推荐: 这个函数混杂四个功能,

  1. 判断形状类型,矩形,圆形,三角形。
  2. 计算矩形面积的算法
  3. 计算圆形面积的算法
  4. 计算三角形面积的算法
function calculateArea(width: number, height: number, shape: string): number {
  if (shape === 'rectangle') {
    return width * height;
  } else if (shape === 'circle') {
    return Math.PI * Math.pow(width / 2, 2);
  } else if (shape === 'triangle') {
    return (width * height) / 2;
  }
}

推荐做法:分拆为四个函数, 每个函数实现一个功能点,简单易维护。

function calculateRectangleArea(width: number, height: number): number {
  return width * height;
}

function calculateCircleArea(radius: number): number {
  return Math.PI * Math.pow(radius, 2);
}

function calculateTriangleArea(base: number, height: number): number {
  return (base * height) / 2;
}

function calculateArea(shape: string, ...args: number[]): number {
  switch (shape) {
    case "rectangle":
      return calculateRectangleArea(args[0], args[1]);
    case "circle":
      return calculateCircleArea(args[0]);
    case "triangle":
      return calculateTriangleArea(args[0], args[1]);
    default:
      throw new Error(`Unsupported shape: ${shape}`);
  }
}

招式三 DRY (Don’t repeat yourself)

如果一段代码在多个地方使用,就应该把它提取出来,避免重复编写。

不同代码规模的DRY

  1. 函数,如果发现多个函数有重复的代码块,写个新函数吧 (当你严格遵循了KISS原则时,你的函数应该不会有重复的代码块)
  2. 类,如果发现多个类有重复的方法,写个新的基类吧
  3. 包,如果发现一个包在其他项目中也能复用,写个新的包吧
  4. 脚手架,使用脚手架提高项目开发效率和一致性

开启新项目时,为了避免重复的项目搭建流程,可以将该流程抽象成一个脚手架。这个脚手架可以包含项目初始化、依赖安装、配置文件生成等一系列步骤,并提供一些可配置的选项,以便根据不同项目的需求进行定制。使用脚手架可以显著提高项目的开发效率和一致性,并减少出错的机会。

招式四 六边形架构

应用程序分为三部分,核心业务逻辑、端口和外部适配器。

端口是我们的应用程序与外部世界进行通信的接口,例如数据库、消息队列、Web 服务等。

适配器是将端口与核心业务逻辑连接起来的组件,它实现了我们定义的端口,并将其注入到核心业务逻辑中。它们负责将外部数据转换为内部模型,并将内部模型转换为外部数据。

实践步骤

  1. 定义核心业务逻辑
  2. 定义端口和适配器
  3. 实现适配器

以用户注册业务逻辑举例:

不推荐:

function register(request: UserRegistrationRequest): void {
  // 验证用户信息
  if (request.username == null || request.username.trim() === "") {
    throw new Error("Invalid username");
  }

  if (request.password == null || request.password.trim() === "") {
    throw new Error("Invalid password");
  }

  if (request.email == null || request.email.trim() === "") {
    throw new Error("Invalid email");
  }

  // 写入数据库
  const user = new User(request.username, request.password, request.email);
  saveUser(user);

  // 发送通知
  const notification = new Notification(request.email, "Welcome to our site!");
  sendNotification(notification);
}

function saveUser(user: User): void {
  // 写入数据库
}

function sendNotification(notification: Notification): void {
  // 发送通知
}

推荐做法:采用六边形架构重构

思维步骤

  1. 我是客户,我要一个符合规范的注册服务,它接收符合规范的请求参数就能完成注册流程,我不管注册流程细节。
type Result = Promise<void>

// 服务规范
interface UserRegistrationPort {
  register(request: UserRegistrationRequest): Result;
}

// 请求参数规范
interface UserRegistrationRequest {
  username: string;
  password: string;
  email: string;
}

class UserRegistrationController {
  constructor(private userRegistrationPort: UserRegistrationPort) {}

  async register(request: UserRegistrationRequest): void {
    const result = await this.userRegistrationPort.register(request);
  }
}
  1. 我是服务供应商,我来管理这个服务的流程和规范,脏活累活外包出去。
    1. 流程:验证用户信息=>写入数据库=>发送通知
type Result = Promise<void>

// 用户信息验证服务的规范
interface IUserValidatorService {
  validate(request: UserRegistrationRequest): Result;
}

// 写入数据库服务规范
interface IUserRepositoryService {
   save(user: UserRegistrationRequest): Result;
}

// 通知规范
interface Notification {
  to: string;
  message: string;
}

// 发送通知服务规范
interface INotificationService {
  send(notification: Notification): Result 
}

class UserRegistrationAdapter implements UserRegistrationPort {
  constructor(
    private validator: IUserValidatorService,
    private repository: IUserRepositoryService,
    private notificationService: INotificationService
  ) {}

  async register(request: UserRegistrationRequest): void {
    try {
      // 验证用户信息
      await this.validator.validate(request)

      // 写入数据库
      await this.repository.save(request);

      // 发送通知
      const notification: Notification =  {
        to:request.email,
        message: "Welcome to our site!"
      }
      await this.notificationService.send(notification);
    }catch((e)=>{
      throw new Error(e.message)
    })
  }
}
  1. 我是最底层的,专干别人不愿意干的活
// 用户信息验证
class UserValidator implements IUserValidatorService {
  validate(request: UserRegistrationRequest): Promise<void> {
    // 验证用户信息
    return new Promise(async (resolve, reject) => {
      if (
        request.username != null &&
        request.username.trim() !== "" &&
        request.password != null &&
        request.password.trim() !== "" &&
        request.email != null &&
        request.email.trim() !== ""
      ) {
        resolve();
      } else {
        reject(new Error("invalid"));
      }
    });
  }
}

// 写入数据库
class UserRepositoryService implements IUserRepositoryService{
    save(user: UserRegistrationRequest): Promise<void>{
      return new Promise(async (resolve,reject)=>{
         // ...
        resolve()
      })
    }
}

// 发送通知
class NotificationService implements INotificationService{
  send(notification: Notification): Promise<void> {
     return new Promise(async (resolve,reject)=>{
       // ...
       resolve()
      })
  }
}

招式五 SOLID

一组面向对象编程原则,旨在提高可维护性、可扩展性和可复用性

  • S:单一职责原则(Single Responsibility Principle)
  • O:开放封闭原则(Open-Closed Principle)
  • L:里氏替换原则(Liskov Substitution Principle)
  • I:接口隔离原则(Interface Segregation Principle)
  • D:依赖倒置原则(Dependency Inversion Principle)

单一职责原则:一个类不要做太多事情,要专注,有点类似于 KISS 原则。

开放封闭原则:对扩展开放,对修改封闭。

以支付处理举例:

不推荐:每次新增支付方式就得修改PaymentProcessor 这个类,可能会导致其他支付方式也需要重新测试。

class PaymentProcessor {
  processPayment(amount: number, paymentMethod: string) {
    switch (paymentMethod) {
      case 'AliPay':
        console.log(`使用支付宝支付 $${amount}`);
        // 实现支付宝支付
        break;
      case 'WeChatPay':
        console.log(`使用微信支付 $${amount}`);
        // 实现微信支付
        break;
      default:
        throw new Error('未知的支付方式');
    }
  }
}

const paymentProcessor = new PaymentProcessor();
paymentProcessor.processPayment(100.00, 'AliPay');

推荐: 不会影响到已有的代码,仅需保证新增的代码正常即可。

interface PaymentStrategy {
  pay(amount: number): void;
}

class AliPay implements PaymentStrategy {
  pay(amount: number) {
    console.log(`使用支付宝支付 $${amount}`);
    // 实现支付宝支付
  }
}

class WeChatPay implements PaymentStrategy {
  pay(amount: number) {
    console.log(`使用微信支付 $${amount}`);
    // 实现微信支付
  }
}

class PaymentProcessor {
  private paymentStrategy: PaymentStrategy | undefined;

  setPaymentStrategy(paymentStrategy: PaymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  processPayment(amount: number) {
    if (!this.paymentStrategy) {
      throw new Error('未选择支付方式');
    }
    this.paymentStrategy.pay(amount);
  }
}

const paymentProcessor = new PaymentProcessor();
paymentProcessor.setPaymentStrategy(new AliPay());
paymentProcessor.processPayment(100.00);

里氏替换原则:使用继承时,子类必须能够替换掉父类并且不影响程序的正确性。我的理解是被继承的类应该是抽象类,或者干脆不用继承,用接口约束子类的行为。 一言以蔽之,别用继承,你 hold 不住它。

接口隔离原则:将一个庞大的接口拆分成多个小接口,以避免类依赖于它们不需要的方法。

依赖倒置原则:依赖于抽象而不是具体实现。类似于六边形架构的思路。

以支付处理举例:

不推荐:这种做法写死了只能用Paypal 网关,新增网关就要重写支付处理代码了。

class PaymentProcessor {
  private paypalGateway: PaypalGateway;

  constructor() {
    this.paypalGateway = new PaypalGateway();
  }

  processPayment(amount: number) {
    this.paypalGateway.sendPayment(amount);
  }
}

class PaypalGateway {
  sendPayment(amount: number) {
    // Send payment using Paypal API
  }
}

推荐:支付处理器不再依赖于具体点处理实现而是抽象接口,这对于替换实现很方便。

// PaymentGateway 接口定义了一些通用的支付操作
interface PaymentGateway {
  pay(amount: number): void;
  refund(amount: number): void;
}

// PayPalPaymentGateway 实现了 PaymentGateway 接口
class PayPalPaymentGateway implements PaymentGateway {
  pay(amount: number) {
    // PayPal 支付逻辑
  }

  refund(amount: number) {
    // PayPal 退款逻辑
  }
}

// StripePaymentGateway 实现了 PaymentGateway 接口
class StripePaymentGateway implements PaymentGateway {
  pay(amount: number) {
    // Stripe 支付逻辑
  }

  refund(amount: number) {
    // Stripe 退款逻辑
  }
}

// PaymentProcessor 高层模块依赖于 PaymentGateway 接口
class PaymentProcessor {
  constructor(private paymentGateway: PaymentGateway) {}

  processPayment(amount: number) {
    this.paymentGateway.pay(amount);
  }

  processRefund(amount: number) {
    this.paymentGateway.refund(amount);
  }
}

// 使用 PayPalPaymentGateway 实例化 PaymentProcessor
const paymentProcessor = new PaymentProcessor(new PayPalPaymentGateway());

// 处理支付请求
paymentProcessor.processPayment(100);

// 处理退款请求
paymentProcessor.processRefund(50);

依赖倒置原则看起来有点像开放封闭原则,但两者侧重点不同,前者重在对依赖的管理,依赖接口而不是实现。后者重在不要修改已有的代码,想要改变软件行为引入一个新的符合规范的类。

招式六 IoC(Inversion of Control)

核心思想是将控制权从程序员手中转移给某个框架或容器,由框架或容器来管理对象之间的依赖关系和对象的生命周期。把分散在各处的对象 new 的操作集中在一起,并提供统一的访问入口。

不推荐:这是创建和使用对象的常规用法,不适用于大量对象的管理。

// 定义一个服务接口
interface IService {
  doSomething(): void;
}

// 实现服务接口的具体类
class Service implements IService {
  doSomething() {
    console.log('Service is doing something.');
  }
}

// 手动创建服务实例
const service = new Service();

// 使用服务实例
service.doSomething();

推荐用法:需要什么服务就跟容器要。

// 定义一个服务接口
interface IService {
  doSomething(): void;
}

// 实现服务接口的具体类
class Service implements IService {
  doSomething() {
    console.log('Service is doing something.');
  }
}

// 定义一个控制反转容器
class Container {
  private services: { [key: string]: any } = {};

  // 注册服务
  register(key: string, service: any) {
    this.services[key] = service;
  }

  // 获取服务实例
  resolve<T>(key: string): T {
    const service = this.services[key];
    if (!service) {
      throw new Error(`Service ${key} not found.`);
    }
    return service as T;
  }
}

// 在容器中注册服务
const container = new Container();
container.register('service', new Service());

// 获取服务实例并使用
const service = container.resolve<IService>('service');
service.doSomething();

其他提升代码质量的手段和习惯

  1. 使用 ESLint 和 Prettier 进行代码格式化和规范检查
  2. 避免 any 类型,多用接口和泛型
  3. 总是声明函数的参数和返回值类型