SOLID:软件设计原则。成为更好的开发人员

2025-05-27

SOLID:软件设计原则。成为更好的开发人员

SOLID设计原则源自面向对象编程指南。它旨在开发易于维护和扩展的软件;防止代码异味;易于重构;提高敏捷性,并最终快速整合快速频繁的变更,避免出现 Bug。

一般来说,技术债务的产生是因为优先考虑快速交付而非完美代码。为了控制技术债务,请在开发过程中遵循 SOLID 原则。

Robert Martin被认为是 SOLID 原则的作者,他指出,如果不严格遵循 SOLID,软件将面临四大问题。它们是:

  • 刚度

    • 实施哪怕是很小的改变都是困难的,因为它可能会引发一系列的改变。
  • 脆弱性

    • 任何变化都可能在很多地方破坏软件,甚至在概念上与变化不相关的领域。
  • 不动

    • 我们无法重用其他项目或同一项目内的模块,因为这些模块具有很多依赖关系。
  • 粘度

    • 很难以正确的方式实现新功能。

SOLID 是一条指导方针,而非规则。重要的是理解其核心,并将其与清晰的判断结合起来。在某些情况下,所有原则中可能只需要几条即可。

SOLID 代表:

  • 单一职责原则(SRP)
  • 开放封闭原则(OCP)
  • 里氏替换原则(LSP)
  • 接口隔离原则(ISP)
  • 依赖倒置原则(DIP)

单一职责原则(SRP)

每个函数、类或模块都应该有且仅有一个改变的原因,这意味着应该只有一项工作并封装在类中(类内凝聚力更强)。

It supports "Separation of concerns" — do one thing, and do it well!"

附注:本文最初发表于Box Piper来源。

例如,考虑这个类:

class Menu {
  constructor(dish: string) {}
  getDishName() {}
  saveDish(a: Dish) {}
}
Enter fullscreen mode Exit fullscreen mode

这个类违反了 SRP。原因如下:它管理菜单的属性,同时也处理数据库。如果数据库管理功能有任何更新,也会影响属性管理功能,从而导致耦合。

更具凝聚力且耦合度更低的类实例。

// Responsible for menu management
class Menu {
  constructor(dish: string) {}
  getDishName() {}
}

// Responsible for Menu management
class MenuDB {
  getDishes(a: Dish) {}
  saveDishes(a: Dish) {}
}
Enter fullscreen mode Exit fullscreen mode

开放封闭原则(OCP)

类、函数或模块应该对可扩展性开放,但对修改关闭。
如果你创建并发布了一个类,而对这个类进行修改,可能会破坏那些开始使用这个类的人的实现。抽象是正确实施 OCP 的关键。

例如,考虑这个类:

class Menu {
  constructor(dish: string) {}
  getDishName() {}
}
Enter fullscreen mode Exit fullscreen mode

我们想要遍历菜肴列表并返回其菜肴。

class Menu {
constructor(dish: string){ }
getDishName() { // ... }

    getCuisines(dishName) {
      for(let index = 0; index <= dishName.length; index++) {
         if(dishName[index].name === "Burrito") {
            console.log("Mexican");
         }
         else if(dishName[index].name === "Pizza") {
            console.log("Italian");
         }
      }
    }

}
Enter fullscreen mode Exit fullscreen mode

函数 getCuisines() 不符合开放封闭原则,因为它无法对新种类的菜肴进行关闭。

如果我们添加一道新菜Croissant,我们需要改变功能并添加这样的新代码。

class Menu {
constructor(dish: string){ }
getDishName() { // ... }

    getCuisines(dishName) {
      for(let index = 0; index <= dishName.length; index++) {
         if(dishName[index].name === "Burrito") {
            console.log("Mexican");
         }
         if(dishName[index].name === "Pizza") {
            console.log("Italian");
         }
         if(dishName[index].name === "Croissant") {
            console.log("French");
         }
      }
    }

}
Enter fullscreen mode Exit fullscreen mode

如果你观察一下,就会发现每添加一道新菜,getCuisines() 函数中都会添加一条新的逻辑。根据开放封闭原则,该函数应该开放以进行扩展,而不是修改。

以下是我们如何使代码库符合 OCP 标准。

class Menu {
  constructor(dish: string) {}
  getCuisines() {}
}

class Burrito extends Menu {
  getCuisine() {
    return "Mexican";
  }
}

class Pizza extends Menu {
  getCuisine() {
    return "Italian";
  }
}

class Croissant extends Menu {
  getCuisine() {
    return "French";
  }
}

function getCuisines(a: Array<dishes>) {
  for (let index = 0; index <= a.length; index++) {
    console.log(a[index].getCuisine());
  }
}

getCuisines(dishes);
Enter fullscreen mode Exit fullscreen mode

这样,每当需要添加新菜品时,我们就不需要修改代码。我们只需创建一个类,并用基类来扩展它即可。

里氏替换原则(LSP)

子类必须可以替换其基类,表明我们可以用子类替换其基类而不影响行为,从而帮助我们符合“is-a”关系。

换句话说,子类必须履行基类定义的契约。从这个意义上讲,它与Bertrand Meyer首次提出的契约式设计相关。

例如,Menu 具有getCuisinesBurrito、Pizza、Croissant 使用的功能,而没有创建单独的功能。

class Menu {
  constructor(dish: string) {}
  getCuisines(cuisineName: string) {
    return cuisineName;
  }
}

class Burrito extends Menu {
  constructor(cuisineName: string) {
    super();
    this.cuisine = cuisineName;
  }
}

class Pizza extends Menu {
  constructor(cuisineName: string) {
    super();
    this.cuisine = cuisineName;
  }
}

class Croissant extends Menu {
  constructor(cuisineName: string) {
    super();
    this.cuisine = cuisineName;
  }
}

const burrito = new Burrito();
const pizza = new Pizza();
burrito.getCuisines(burrito.cuisine);
pizza.getCuisines(pizza.cuisine);
Enter fullscreen mode Exit fullscreen mode

接口隔离原则(ISP)

客户端不应该被迫实现它不使用的接口,或者客户端不应该被迫依赖他们不使用的方法。

原则名称中的“接口”一词并不严格地表示接口,它可能是一个抽象类。

例如

interface ICuisines {
  mexican();
  italian();
  french();
}

class Burrito implements ICuisines {
  mexican() {}
  italian() {}
  french() {}
}
Enter fullscreen mode Exit fullscreen mode

如果我们在接口中添加一个新方法,所有其他类都必须声明该方法,否则将引发错误。

为了解决这个问题

interface BurritoCuisine {
  mexican();
}
interface PizzaCuisine {
  italian();
}

class Burrito implements BurritoCuisine {
  mexican();
}
Enter fullscreen mode Exit fullscreen mode

许多特定于客户端的接口比一个通用接口更好。

依赖倒置原则(DIP)

实体必须依赖于抽象,而不是具体。它指出高级模块不能依赖于低级模块,应将它们解耦并利用抽象。

高级模块是应用程序的一部分,用于解决实际问题和用例。
它们更加抽象,并映射到业务领域(业务逻辑);
它们告诉我们软件应该做什么(不是如何做,而是做什么);

低级模块包含执行业务策略所需的实现细节;关于软件如何执行各种任务;

例如

const pool = mysql.createPool({});
class MenuDB {
  constructor(private db: pool) {}
  saveDishes() {
    this.db.save();
  }
}
Enter fullscreen mode Exit fullscreen mode

这里,MenuDB 类是高级组件,而池变量是低级组件。为了解决这个问题,我们可以分离 Connection 实例。

interface Connection {
  mysql.createPool({})
}

class MenuDB {
   constructor(private db: Connection) {}
   saveDishes() {
      this.db.save();
   }
}
Enter fullscreen mode Exit fullscreen mode

结束语

遵循 SOLID 原则的代码可以轻松共享、扩展、修改、测试和重构,不会出现任何问题。随着这些原则在实际应用中的不断应用,其优势将更加凸显。

反模式和不正确的理解会导致愚蠢的代码:单例、紧耦合、不可测试、过早优化、命名不规范以及重复。SOLID 可以帮助开发人员避免这些问题。

要阅读更多此类有趣的主题,请关注并阅读BoxPiper 博客

支持我的工作,请我喝杯咖啡。这对我来说意义重大。😇

文章来源:https://dev.to/boxpiperapp/solid-software-design-principles-be-a-better-developer-1615
PREV
如何安全地存储 API 密钥
NEXT
Typescript 与 React 结合使用的初学者指南