【编写高质量前端代码】SOLID原则的TypeScript体现

SOLID原则的TypeScript体现

对于前端开发者而言,理解和应用SOLID原则不仅是提升代码质量的关键,也是实现高效、可维护和可扩展前端应用的基石。

单一职责原则(Single Responsibility Principle, SRP)

  • 概念:一个类应该仅有一个引起它变化的原因。
  • 目的:确保一个类只有一个改变的理由,这样做可以减少代码的复杂性,提高其可维护性。
  • 优势:
    • 减少复杂性:当类只负责一件事情时,它们变得更简单、更直接,易于理解和维护。
    • 降低依赖性:更改类的一个功能不会影响到其他功能,从而减少了类之间的依赖。
    • 提高可测试性:单一职责的类易于测试,因为需要考虑的行为较少。
    • 更容易重用和重构:由于每个类都只关注一个任务,所以更容易在不同的上下文中重用或修改它们。
typescript 复制代码
// 不遵循SRP
class User {
  constructor(private name: string) {}

  getName(): string {
    return this.name;
  }

  saveToDatabase() {
    console.log("User saved to database");
  }
}

const user = new User("Alice");
user.saveToDatabase();  // User类同时处理用户信息和数据库操作

// 遵循SRP
class User {
  constructor(private name: string) {}

  getName(): string {
    return this.name;
  }
}

class UserRepository {
  saveUser(user: User) {
    console.log("User saved to database");
  }
}

const user = new User("Alice");
const userRepository = new UserRepository();
userRepository.saveUser(user);  // User类只处理用户信息,UserRepository类只处理数据库操作

在遵循SRP的情况下,User类仅负责处理用户信息,而UserRepository类负责与数据库的交互。这增加了代码的可维护性和可测试性。相反,在不遵循SRP的情况下,User类同时处理用户数据和数据库操作,违反了单一职责原则,导致该类的职责过多,难以维护和测试。

开放/封闭原则(Open/Closed Principle, OCP)

  • 概念:软件实体应对扩展开放,对修改封闭。
  • 目的:允许系统容易地扩展新功能,而不需要修改现有代码。
  • 优势:
    • 增强模块性:通过扩展新功能而不是修改现有代码,可以保持现有模块的稳定性。
    • 降低维护成本:避免了因修改导致的潜在错误和所需的重测。
    • 提高可扩展性:通过扩展类或实现新的接口,系统可以更灵活地适应未来的变化。
    • 促进复用:已有的代码由于不需要修改,可以在新的场景下重复使用。
typescript 复制代码
// 不遵循OCP
class Rectangle {
  constructor(public width: number, public height: number) {}
}

class Circle {
  constructor(public radius: number) {}
}

function calculateAreaOfRectangles(rectangles: Rectangle[]): number {
  return rectangles.reduce((area, rect) => area + rect.width * rect.height, 0);
}

function calculateAreaOfCircles(circles: Circle[]): number {
  return circles.reduce((area, circle) => area + Math.PI * circle.radius * circle.radius, 0);
}

const rectangles = [new Rectangle(2, 3)];
const circles = [new Circle(5)];
console.log(calculateAreaOfRectangles(rectangles) + calculateAreaOfCircles(circles));  // 新增图形时,需要增加新的函数

// 遵循OCP
interface Shape {
  area(): number;
}

class Rectangle implements Shape {
  constructor(public width: number, public height: number) {}
  area(): number {
    return this.width * this.height;
  }
}

class Circle implements Shape {
  constructor(public radius: number) {}
  area(): number {
    return Math.PI * this.radius * this.radius;
  }
}

function calculateTotalArea(shapes: Shape[]): number {
  return shapes.reduce((area, shape) => area + shape.area(), 0);
}

const shapes: Shape[] = [new Rectangle(2, 3), new Circle(5)];
console.log(calculateTotalArea(shapes));  // 新增图形时,无需修改calculateTotalArea函数

在遵循OCP的情况下,通过引入Shape接口,我们可以轻松地添加新的形状类而无需修改现有的calculateTotalArea函数。但在不遵循OCP的情况下,每添加一种新的形状,我们都需要添加一个新的计算面积的函数,这使得代码难以扩展且容易出错。

里氏替换原则(Liskov Substitution Principle, LSP)

  • 概念:子类型必须能够替换掉它们的基类型。
  • 目的:确保派生类可以替换其基类,而不破坏程序的整体功能。
  • 优势:
    • 提高可互换性:子类可以替换基类而不影响系统的整体功能。
    • 增强模块间的隔离:模块间的依赖关系更清晰,因为它们依赖于抽象,而不是具体实现。
    • 促进代码的健壮性:遵循LSP的代码更能适应未来的变化,因为基类和子类遵循同样的约束。
    • 提高代码的可维护性和可扩展性:由于子类的行为预期与基类一致,因此维护和扩展基类的功能更加容易。
typescript 复制代码
// 不遵循LSP
class Bird {
  fly() {
    console.log("This bird is flying");
  }

  eat() {
    console.log("This bird is eating");
  }
}

class Duck extends Bird { }
class Ostrich extends Bird { }

// 使用Bird类型的函数
function makeBirdFly(bird: Bird) {
  bird.fly();
}

function makeBirdEat(bird: Bird) {
  bird.eat();
}

const duck = new Duck();
const ostrich = new Ostrich();

makeBirdFly(duck);      // 正确: Duck可以飞
makeBirdFly(ostrich);   // 错误: Ostrich不应该飞,但根据当前类的设计,它会尝试飞行

makeBirdEat(duck);      // 正确: Duck可以吃
makeBirdEat(ostrich);   // 正确: Ostrich可以吃

// 遵循LSP
class Bird {
  eat() {
    console.log("This bird is eating");
  }
}

class FlyingBird extends Bird {
  fly() {
    console.log("This bird is flying");
  }
}

class Duck extends FlyingBird { }
class Ostrich extends Bird { }

// 通过Bird类型的函数来展示LSP
function makeBirdEat(bird: Bird) {
  bird.eat();
}

const duck = new Duck();
const ostrich = new Ostrich();

makeBirdEat(duck);      // 正确: Duck作为Bird的子类,可以吃
makeBirdEat(ostrich);   // 正确: Ostrich作为Bird的子类,也可以吃

// 对于飞行功能
function makeBirdFly(bird: FlyingBird) {
  bird.fly();
}

makeBirdFly(duck);      // 正确: Duck作为FlyingBird的子类,可以飞
// makeBirdFly(ostrich); // 错误: Ostrich不是FlyingBird的子类,不应该调用飞行相关的函数

在这个不遵循LSP的例子中,Bird类假设所有鸟都能飞。然而,这在现实中并不总是成立,比如鸵鸟就不能飞。当Ostrich(鸵鸟)类继承Bird类时,它也继承了飞行的行为,这明显违反了LSP。因此,当我们尝试让鸵鸟飞时,虽然从编程的角度来看代码可以正常运行,但从逻辑和现实的角度来看,这违反了鸵鸟的自然属性,导致代码逻辑错误。这个对比例子显示了不遵循LSP可能导致的问题,即子类不能适当地替换其父类而不破坏程序的预期行为。这种设计使得代码难以维护,并且增加了出现bug的风险。

接口隔离原则(Interface Segregation Principle, ISP)

  • 概念:不要强迫客户依赖于它们不使用的接口。
  • 目的:保证接口的单一性和精简性。
  • 优势:
    • 降低耦合:客户端不依赖于它们不使用的方法,减少了类之间的依赖。
    • 提高灵活性和稳定性:更改接口的一部分不太可能影响依赖于其他接口部分的类。
    • 更简单的接口设计:细化的接口更简洁、清晰,便于理解和实现。
    • 改进代码组织和可读性:通过具体的接口清晰地表达了类的用途和功能。
typescript 复制代码
// 不遵循ISP
interface IWorker {
  work(): void;
  eat(): void;
}

class Worker implements IWorker {
  work(): void {
    console.log("Working");
  }

  eat(): void {
    console.log("Eating");
  }
}

const worker = new Worker();
worker.work();
worker.eat();  // Worker类被迫实现了所有的方法,即使某些方法对它而言可能不是必须的

// 遵循ISP
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

class Worker implements Workable, Eatable {
  work(): void {
    console.log("Working");
  }

  eat(): void {
    console.log("Eating");
  }
}

const worker = new Worker();
worker.work();
worker.eat();  // Worker类实现了必要的接口

在遵循ISP的情况下,我们创建了细化的接口WorkableEatable,使得Worker类仅实现了它需要的方法。而在不遵循ISP的情况下,Worker类被迫实现了所有在IWorker接口中定义的方法,即使其中一些对它来说可能不是必须的,这违反了接口隔离原则。

依赖倒置原则(Dependency Inversion Principle, DIP)

  • 概念:高层模块不应该依赖低层模块,两者都应该依赖抽象。
  • 目的:使代码更容易应对变化,易于维护和扩展。
  • 优势:
    • 减少代码间的直接依赖:通过依赖于抽象而不是具体实现,减少了代码间的直接依赖,降低耦合。
    • 提高代码的可重用性:抽象和具体实现的分离使得代码在不同的上下文中更容易被重用。
    • 增强系统的可配置性:依赖抽象使得可以更灵活地替换或修改具体实现,增强了系统的配置能力。
    • 更容易进行单元测试:依赖倒置使得用替代实现(如mock数据)进行测试变得更容易。
typescript 复制代码
// 不遵循DIP
class LightBulb {
  turnOn() {
    console.log("LightBulb turned on");
  }

  turnOff() {
    console.log("LightBulb turned off");
  }
}

class Switch {
  constructor(private bulb: LightBulb) {}

  operate() {
    this.bulb.turnOn();
    this.bulb.turnOff();
  }
}

const lightBulb = new LightBulb();
const switchForLight = new Switch(lightBulb);
switchForLight.operate();  // Switch类直接依赖于具体的LightBulb类

// 遵循DIP
interface Switchable {
  turnOn(): void;
  turnOff(): void;
}

class LightBulb implements Switchable {
  turnOn() {
    console.log("LightBulb turned on");
  }

  turnOff() {
    console.log("LightBulb turned off");
  }
}

class Switch {
  constructor(private device: Switchable) {}

  operate() {
    this.device.turnOn();
    this.device.turnOff();
  }
}

const lightBulb = new LightBulb();
const switchForLight = new Switch(lightBulb);
switchForLight.operate();  // Switch类依赖于抽象的Switchable接口

在遵循DIP的示例中,高层模块Switch类不直接依赖于低层模块LightBulb类的具体实现,而是依赖于Switchable接口,实现了这两个模块的解耦。

相关推荐
学习ing小白1 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进2 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er2 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063712 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl2 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码2 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html
辛-夷3 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js
田哥coder3 小时前
充电桩项目:前端实现
前端