您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件开发专栏 > 开发技术 > 正文

每个开发者都应该知道的七个原则

发表于:2023-11-17 作者:小技术君 来源:小技术君

软件开发是一门复杂的领域。是什么让高质量的软件与容易出错、充满错误的软件有所不同?答案通常在开发人员在编写代码时采用的核心原则中。

编程原则:优秀代码的基石

编程原则是卓越软件的基石。这些建议和最佳实践指导开发人员编写既功能强大又优雅、易维护和可扩展的代码。

在本文中,我们深入探讨了每个开发者工具包中都应该有的七个基本编程原则:

1. DRY 原则

DRY:不要重复自己 — 减少冗余的关键原则。如果你发现自己复制粘贴同一段代码超过两次,现在是考虑抽象的时候了。

考虑这种情况:你有三个函数,每个函数都以相同的方式格式化日期。与其在所有三个函数中都有重复的格式化代码,不如创建一个单一的辅助函数:

// 格式化日期的辅助函数
function formatDate(date) {  
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}

// 函数 1: 显示今天的日期
function displayTodaysDate() {  
  const today = new Date();  
  return formatDate(today);
}

// 函数 2: 显示一周后的日期
function displayDateOneWeekFromNow() {  
  const oneWeekFromNow = new Date();  
  oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7);  
  return formatDate(oneWeekFromNow);
}

// 函数 3: 显示一个月前的日期
function displayDateOneMonthAgo() {  
  const oneMonthAgo = new Date();  
  oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);  
  return formatDate(oneMonthAgo);
 

2. KISS 原则

KISS:保持简单,愚蠢 — 在你的代码中追求简单。例如,如果你写了一个复杂的 if-else 链,也许使用 switch 语句或字典会简化和美化结构:

之前:

function getErrorMessage(errorCode) {  
  if (errorCode = 'E001') {  
    return 'Invalid input.';  
  } else if (errorCode = 'E002') {  
    return 'Connection timed out.';  
  } else if (errorCode = 'E003') {  
    return 'Database error.';  
  } else if (errorCode = 'E004') {  
    return 'File not found.';  
  } else {  
    return 'Unknown error.';  
  }
}
 

重构后:


const ERROR_MESSAGES = {  
  'E001': 'Invalid input.',  
  'E002': 'Connection timed out.',  
  'E003': 'Database error.',  
  'E004': 'File not found.'
};

function getErrorMessage(errorCode) {  
  return ERROR_MESSAGES[errorCode] || 'Unknown error.';
}
 

3. SOLID 原则

SOLID 不是一个单一的原则,而是五个设计原则的集合。尽管它们根植于面向对象编程(OOP),但它们的智慧可以更广泛地应用。

(1) 单一职责原则(SRP): 一个类应该只有一个改变的理由。这意味着每个类应该只有一个任务或功能,确保更容易维护和在更改过程中减少副作用。

考虑这个例子:

// 错误的方法

class UserManager {  
  saveUser(user) {  
    // 保存用户到数据库的逻辑  
  }

  generateReport(user) {  
    // 生成用户报告的逻辑  
  }
}
 

更优雅的解决方案是将其拆分为两个各自处理单一职责的类:

// 正确的方法

class UserDatabase {  
  save(user) {  
    // 将用户数据保存到数据库  
  }
}

class UserReport {  
  generate(user) {  
    // 为用户生成报告  
  }
}
 

在上面的代码片段中,我们分担了责任:UserReport 处理用户的报告生成,而 UserDatabase 管理将用户数据保存到数据库。

(2) 开闭原则(OCP): 软件组件应该对扩展开放,对修改关闭。这允许开发人员在不修改现有代码的情况下添加新功能,促进可重用性并减少错误。

假设你有一个 AreaCalculator 类,用于计算矩形的面积。现在,如果我们添加一个 Circle,AreaCalculator 将需要修改。

// 错误的方法

class AreaCalculator {  
  calculateArea(shape) {  
    if (shape.type = "circle") {  
      return 3.14 * shape.radius * shape.radius;  
    } else if (shape.type = "square") {  
      return shape.side * shape.side;  
    }  
  }
}
 

相反,使用 OCP:我们从一个基础的 Shape 类扩展我们的形状,允许轻松添加新形状而不修改 AreaCalculator。

// 正确的方法

class Shape {  
  calculateArea() {}
}

class Circle extends Shape {  
  constructor(radius) {  
    super();  
    this.radius = radius;  
  }

  calculateArea() {  
    return 3.14 * this.radius * this.radius;  
  }
}

class Square extends Shape {  
  constructor(side) {  
    super();  
    this.side = side

;  
  }

  calculateArea() {  
    return this.side * this.side;  
  }
}
 

(3) 里氏替换原则(LSP): 子类应该能够替换其基类而不产生异常。这确保继承类保持其父类的属性和行为。

遵循 LSP,我们应该重构设计以确保正确的继承:

class Bird {  
  fly() {  
    // 通用飞行行为  
  }
}

class Penguin extends Bird {  
  // 企鹅不能飞,所以这个方法不应该在这里
}
 

正确的方法是我们将形状从基本的 Shape 类扩展出来,允许轻松添加新的形状而不修改 AreaCalculator。

(4) 接口隔离原则(ISP): 类不应该被迫实现它们不使用的接口。相反,接口应该对其目的具体而清晰。

这意味着接口不应该有太多方法,尽量我们将小接口抽取出来,以便类可以只实现它们需要的接口,就像下面的例子:

// 错误的方法

interface Worker {  
  work();  
  eat();  
  sleep();  
  swim();
}

// 正确的方法

interface Worker {  
  work();
}

interface Eater {  
  eat();
}

interface Swimmer {  
  swim();
}
 

(5) 依赖反转原则(DIP): 高层模块不应与低层模块纠缠在一起;它们都应依赖于抽象。例如在开关和设备的设计中可以找到:

// 错误的方法

class LightBulb {  
  turnOn() {}  
  turnOff() {}
}

class Switch {  
  constructor(bulb) {  
    this.bulb = bulb;  
  }

  operate() {  
    // 直接控制灯泡  
  }
}
 

我们可以重构这样,以便 Switch 可以对任何实现 SwitchableDevice 的设备进行操作,而不仅仅是 LightBulb。

// 正确的方法

class SwitchableDevice {  
  turnOn() {}  
  turnOff() {}
}

class Bulb extends SwitchableDevice {  
  // 实现 turnOn 和 turnOff 方法  
}

class SwitchDIP {  
  constructor(device) {  
    this.device = device;  
  }

  operate() {  
    // 控制设备  
  }
}
 

4. YAGNI 原则

YAGNI,“你不会需要它”,警告不要在必要之前添加功能。

例如,如果你正在构建一个博客网站,并考虑添加一个基于用户写作的功能来预测用户的心情,但这对于网站正常运行并不是必需的,那么最好将其留在一边,至少现在是这样。

有不必要功能的应用:

class Blog {  
  constructor(posts) {  
    this.posts = posts;  
  }

  addPost(post) {  
    this.posts.push(post);  
  }

  displayPosts() {  
    // 显示所有帖子  
  }

  predictUserMoodBasedOnWritings(post) {  
    // 预测情绪的复杂算法  
    // ...  
    return "Happy"; // 只是一个示例情绪  
  }

  notifyUserAboutMood(mood) {  
    // 通知逻辑  
    console.log(`Based on your writing, you seem to be ${mood}`);  
  }
}
 

删除不必要功能后:

class Blog {  
  constructor(posts) {  
    this.posts = posts;  
  }

  addPost(post) {  
    this.posts.push(post);  
  }

  displayPosts() {  
    // 显示所有帖子  
  }
}
 

5. SoC 原则

SoC,或“关注点分离”,建议不同的功能区域应由不同且最小重叠的模块管理。

例如,在一个天气应用程序中,一个模块可以处理数据获取,另一个可以管理数据存储,另一个则可以控制用户界面。每个都有自己的关注点,与其他模块分开。

// 1. 数据获取模块

function fetchWeatherData(city) {  
  const apiKey = 'YOUR_API_KEY';  
  const response = fetch(`https://api.weather.com/v1/${city}?apiKey=${apiKey}`);  
  return response.json();
}

// 2. 数据存储模块

function storeWeatherData(data) {  
  localStorage.setItem('weatherData', JSON.stringify(data));
}

// 3. 用户界面模块

function displayWeatherData(data) {  
  const weatherBox = document.querySelector('#weather-box');  
  weatherBox.innerHTML = `<h1>${data.city}</h1><p>${data.temperature}°C</p>`;
}

// 在主应用程序函数中组合它们

function weatherApp(city) {  
  const data = fetchWeatherData(city);  
  storeWeatherData(data);  
  displayWeatherData(data);
}
 

6. LoD 原则

LoD(迪米特法则)是开发软件的一个指导原则,特别是面向对象的程序。在其一般形式中,LoD是松散耦合的一个具体案例。

想象一下餐厅的场景:顾客将订单(方法)交给服务员,然后服务员将订单交给厨师。顾客不直接与厨师互动。

class Customer {
  constructor(waiter) {
    this.waiter = waiter;
  }

  giveOrder(order) {
    console.log("Customer: I'd like to order " + order);
    this.waiter.takeOrder(order);
  }
}

class Waiter {
  constructor(chef) {
    this.chef = chef;
  }

  takeOrder(order) {
    console.log('Waiter: Order received - ' + order);
    this.chef.prepareOrder(order);
  }
}

class Chef {
  prepareOrder(order) {
    console.log('Chef: Preparing ' + order);
    // Logic to prepare the food...
  }
}
 

7. COI 原则

组合优于继承原则(COI)建议使用组合(将简单对象组合以创建更复杂的对象)而不是类继承。

想象一下你有一个类 Bird 和一个类 Airplane。它们都能飞,但有继承关系并不合理。相反,你可以有一个 CanFly 类,并将你的 Bird 和 Airplane 类与它组合。

// 错误的方法:继承

class CanFly {
  fly() {
    console.log(this.constructor.name + ' is flying!');
  }
}

class BirdInherit extends CanFly {}

class AirplaneInherit extends CanFly {}
 

通过以下组合方法,你可以轻松地向 BirdCompose 或 AirplaneCompose 添加或删除功能,而无需进行结构更改或添加不必要的冗余,强调灵活性。

// 正确的方法:组合

class CanFlyComposition {
  fly() {
    console.log('Flying in the sky!');
  }
}

class BirdCompose {
  constructor() {
    this.flyingCapability = new CanFlyComposition();
  }

  fly() {
    this.flyingCapability.fly();
  }
}

class AirplaneCompose {
  constructor() {
    this.flyingCapability = new CanFlyComposition();
  }

  fly() {
    this.flyingCapability.fly();
  }
}