技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

前言:本文翻译自 掌握 React JS SOLID 原则 (opens new window)

# 什么是 SOLID 原则?

SOLID 原则是五个设计原则,帮助我们保持应用程序的可重用性、可维护性、可扩展性和松散耦合性。

SOLID 原则是:

  • [S] — 单一职责原则
  • [O] — 开闭原则
  • [L] — 里氏替换原理
  • [I] - 接口隔离原则
  • [D] - 依赖倒置原则

# 单一职责原则

_“一个模块应该对一个且仅一个参与者负责。” _——维基百科。

单一职责原则规定组件应该有一个明确的目的或职责。

它应该专注于特定的功能或行为,并避免承担不相关的任务。遵循 SRP 使组件更加集中、模块化并且易于理解和修改。我们来看看实际的实现。

![](https://miro.medium.com/v2/resize:fit:496/1*niFOXUCmYKblZNijZveF2w.png" style="text-align:center)


// 负责渲染用户个人资料信息的组件
const  UserProfile = ( { user } ) => { 
  return ( 
    <div> 
      <h1>User Profile</h1> 
      <p>Name: {user.name}</p> 
      < p>Email:{user.email}</p> 
    </div> 
  ); 
}; 

// 负责渲染用户头像的组件
const  ProfilePicture = ( { user } ) => { 
  return ( 
    <div> 
      <h1>头像</h1> 
      <img src={user. 
    profilePictureUrl} alt="个人资料" /> </div> 
  ); 
};

// 结合了 UserProfile 和 ProfilePicture 组件的父组件
const  App = ( ) => { 
  const user = { 
    name : "John Doe" , 
    email : "johndoe@example.com" , 
    profilePictureUrl : "https://example. com/profile.jpg" , 
  }; 

  return ( 
    <div> 
      <UserProfile user={user} /> 
      <ProfilePicture user={user} /> 
    </div> 
  ); 
}; 

export default App;

在此示例中,我们有两个独立的组件:UserProfileProfilePicture。该UserProfile组件负责渲染用户的个人资料信息(姓名和电子邮件),而该ProfilePicture组件则负责渲染用户的个人资料图片。每个组件都有单一的职责并且可以独立地重用。

通过遵守 SRP,单独管理和修改这些组件变得更加容易。例如,如果您想要更改用户的个人资料图片,您可以专注于该ProfilePicture组件而不影响该UserProfile组件。这种关注点分离改进了代码组织、可维护性和可重用性。



// 负责渲染用户个人资料信息和个人资料图片的组件
const  UserProfile = ( { user } ) => { 
  return ( 
    <div> 
      <h1>User Profile</h1> 
      <p>Name: {user.name}</p > 
      <p>Email: {user.email}</p> 
      <img src={user.profilePictureUrl} alt="个人资料" /> 
    </div> 
  ); 
}; 

export default App;

在此示例中,我们有一个名为 的组件UserProfile,负责呈现用户的个人资料信息及其个人资料图片。这违反了 SRP,因为该组件具有多重职责。

如果需要更改用户个人资料信息或个人资料图片,则需要修改此单个组件,这违背了“有理由更改”的原则。随着该组件变得越来越复杂,理解和维护它变得更加困难。

# 开闭原则

“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。” ——维基百科。

开闭原则强调组件应该对扩展开放(可以添加新的行为或功能),但对修改封闭(现有代码应保持不变)。
这一原则鼓励创建能够适应变化、​​模块化且易于维护的代码。



// Button.js
import React from 'react';

const Button = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);

export default Button;



// IconButton.js
import React from 'react';
import Button from './Button';

const IconButton = ({ onClick, children, icon }) => (
  <Button onClick={onClick}>
    <span className="icon">{icon}</span>
    {children}
  </Button>
);

export default IconButton;
  

在上面的示例中,我们有一个Button呈现基本按钮的组件。然后,我们创建一个IconButton组件来扩展该组件的功能Button。它添加一个icon道具并渲染图标以及按钮的子项。

通过使用这种方法,我们遵循了开闭原则。Button我们通过创建一个新组件( )来扩展组件的功能IconButton,而无需修改组件的现有代码Button。这使我们能够添加新的按钮类型或变体,而不影响现有的按钮实现。

通过遵循开闭原则,我们的代码变得更加可维护、模块化,并且在未来更容易扩展。

// Button.js
import React from 'react';

const Button = ({ onClick, children, icon }) => {
  if (icon) {
    return (
      <button onClick={onClick}>
        <span className="icon">{icon}</span>
        {children}
      </button>
    );
  } else {
    return (
      <button onClick={onClick}>{children}</button>
    );
  }
};

export default Button;
  

在此示例中,组件已被修改以处理传递 prop 的Button情况。icon如果icon提供了 prop,它会渲染带有图标的按钮;否则,它会呈现没有图标的按钮。

这种方法的问题在于它违反了开闭原则,因为我们修改了现有Button组件而不是扩展它。从长远来看,这使得组件更脆弱且更难以维护。

将来,如果您想添加更多变体或类型的按钮,则需要Button再次修改组件。这违反了封闭修改的原则。

# 里氏替换原则

“子类型对象应该可以替代父类型对象” ——维基百科。

里氏替换原则 (LSP) 是 SOLID 原则之一,它规定超类的对象应该可以用其子类的对象替换,而不影响程序的正确性。

在 React.js 的上下文中,让我们考虑一个示例,其中我们有一个名为 的基本组件Button以及两个子类PrimaryButtonSecondaryButtonPrimaryButtonSecondaryButton继承自Button组件。根据 LSP 的说法,我们应该能够在任何需要实例的地方使用 的PrimaryButtonSecondaryButton 实例,而不会造成任何问题。

class Button extends React.Component {
  render() {
    return (
      <button>{this.props.text}</button>
    );
  }
}

class PrimaryButton extends Button {
  render() {
    return (
      <button style={{ backgroundColor: 'blue', color: 'white' }}>{this.props.text}</button>
    );
  }
}

class SecondaryButton extends Button {
  render() {
    return (
      <button style={{ backgroundColor: 'gray', color: 'black' }}>{this.props.text}</button>
    );
  }
}

// Usage of the components
function App() {
  return (
    <div>
      <Button text="Regular Button" />
      <PrimaryButton text="Primary Button" />
      <SecondaryButton text="Secondary Button" />
    </div>
  );
}

在上面的例子中, PrimaryButtonSecondaryButtonButton的子类。我们可以看到,两个子类都继承了render()基类的方法,并且它们重写了该方法以提供自己的渲染行为。

由于 PrimaryButtonSecondaryButtonButton的子类,因此我们可以在需要 实例的地方自由使用Button的两个子类实例,例如在App组件中。这演示了里氏替换原则的实际应用,因为子类可以无缝替换基类,而不会影响程序的功能。

请注意,这是一个简化的示例,旨在使用类组件说明 React.js 中的 LSP 概念。在现实应用程序中,您通常会使用函数组件和挂钩而不是类组件。但是,原则仍然是相同的:派生组件应该能够替换其基本组件,而不会引起任何问题。

class Button extends React.Component {
  render() {
    return (
      <button>{this.props.text}</button>
    );
  }
}

class PrimaryButton extends Button {
  render() {
    return (
      <button style={{ backgroundColor: 'blue', color: 'white' }}>{this.props.text}</button>
    );
  }
}

class SecondaryButton extends Button {
  render() {
    // Violation: Changing the behavior
    return (
      <a href="#" style={{ backgroundColor: 'gray', color: 'black' }}>{this.props.text}</a>
    );
  }
}

// Usage of the components
function App() {
  return (
    <div>
      <Button text="Regular Button" />
      <PrimaryButton text="Primary Button" />
      <SecondaryButton text="Secondary Button" />
    </div>
  );
}

在此示例中,该类SecondaryButton违反了里氏替换原则。<button>它不像基类 和 那样渲染元素PrimaryButton,而是渲染一个<a>元素。这违反了原则,因为派生类 ( SecondaryButton) 的行为与基类 ( Button) 的行为不同。

当我们在组件SecondaryButton中使用 时,与和App相比,它不会像ButtonPrimaryButton 一样预期运行。这违反了原则,因为派生类不提供基类的兼容替代品。

应用里氏替换原则时,确保子类遵循与超类相同的行为非常重要。

# 接口隔离原则

任何代码都不应该被迫依赖于它不使用的方法。”——维基百科。

接口隔离原则(ISP)建议接口应该集中并根据特定的客户需求进行定制,而不是过于宽泛并迫使客户实现不必要的功能。我们来看看实际的实现。

// Interface for displaying user information
interface DisplayUser {
  name: string;
  email: string;
}

// UserProfile component implementing DisplayUser interface
const UserProfile: React.FC<DisplayUser> = ({ name, email }) => {
  return (
    <div>
      <h2>User Profile</h2>
      <p>Name: {name}</p>
      <p>Email: {email}</p>
    </div>
  );
};

// Usage of the component
const App: React.FC = () => {
  const user = {
    name: 'John Doe',
    email: 'johndoe@example.com',
  };

  return (
    <div>
      <UserProfile {...user} />
    </div>
  );
};

在这个较短的示例中,DisplayUser界面定义了显示用户信息所需的属性。该UserProfile组件是一个功能组件,通过 props 接收nameemail属性并相应地渲染用户配置文件。

App组件UserProfile通过传递nameemail属性作为 props 来使用该组件来显示用户配置文件。

通过隔离接口,UserProfile组件仅依赖于DisplayUser接口,接口提供了呈现用户配置文件所需的属性。这促进了更加集中和模块化的设计,其中组件可以重复使用,而无需不必要的依赖。

这个较短的示例演示了接口隔离原则如何帮助保持接口简洁和相关,从而产生更易于维护和灵活的代码。

// Interface for user management
interface UserManagement {
  addUser: (user: User) => void;
  displayUser: (userId: number) => void;
}

// UserProfile component implementing UserManagement interface
const UserProfile: React.FC<UserManagement> = ({ addUser, displayUser }) => {
  // ...
  return (
    // ...
  );
};

// Usage of the component
const App: React.FC = () => {
  const userManager: UserManagement = {
    addUser: (user) => {
      // Add user logic
    },
    displayUser: (userId) => {
      // Display user logic
    },
  };

  return (
    <div>
      <UserProfile {...userManager} />
    </div>
  );
};

在这个糟糕的例子中,UserManagement接口最初有两个方法:addUserdisplayUser。该UserProfile组件应实现此接口。

然而,当我们尝试使用该组件时,问题就出现了UserProfile。该UserProfile组件接收UserManagement接口作为 props,但它只需要displayUser渲染用户配置文件的方法。它不使用或不需要该addUser方法。

这违反了接口隔离原则,因为UserProfile组件被迫依赖于包含UserManagement它不需要的方法的接口 ( )。它引入了不必要的依赖关系,如果错误地调用或实现了未使用的方法,则可能会导致代码复杂性和潜在问题。

为了遵守接口隔离原则,UserManagement接口应该被分成更有针对性和更具体的接口,允许组件只依赖于它们需要的接口。

# 依赖倒置原则

一个实体应该依赖于抽象,而不是具体”——维基百科。

依赖倒置原则(DIP)强调高层组件不应该依赖于低层组件。这一原则促进了松散耦合和模块化,并有助于更轻松地维护软件系统。我们来看看实际的实现。

// Abstraction: Interface or contract
const DataService = () => {
  return {
    fetchData: () => {}
  };
};

// High-level component
const App = ({ dataService }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    dataService.fetchData().then((result) => {
      setData(result);
    });
  }, [dataService]);

  return (
    <div>
      <h1>Data:</h1>
      <ul>
        {data.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

// Dependency: Low-level component
const DatabaseService = () => {
  const fetchData = () => {
    // Simulated fetching of data from a database
    return Promise.resolve(['item1', 'item2', 'item3']);
  };

  return {
    fetchData
  };
};

// Dependency Injection: Providing the implementation
const AppContainer = () => {
  const dataService = DataService(); // Creating the abstraction
  const databaseService = DatabaseService(); // Creating the low-level dependency

  // 注入依赖
  return <App dataService={dataService} />;
};

export default AppContainer;

在这个较短的示例中,我们有DataService抽象,它表示获取数据的契约。组件App通过 prop 依赖于这个抽象dataService

组件App使用该方法获取数据dataService.fetchData并相应地更新组件的状态。

DatabaseService是低级组件,提供从数据库获取数据的实现。

AppContainer组件负责创建抽象 ( dataService) 和低级依赖 ( databaseService)。然后它将dataService依赖项注入到组件中App

通过遵循 DIP,App组件依赖于抽象 ( DataService) 而不是低级组件 ( DatabaseService)。DataService这允许在保持组件不变的同时更换不同实现时提供更好的模块化性、可测试性和灵活性App

// High-level component
const App = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // Violation: App depends directly on a specific low-level implementation
    fetchDataFromDatabase().then((result) => {
      setData(result);
    });
  }, []);

  const fetchDataFromDatabase = () => {
    // Simulated fetching of data from a specific database
    return Promise.resolve(['item1', 'item2', 'item3']);
  };

  return (
    <div>
      <h1>Data:</h1>
      <ul>
        {data.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

在此示例中,App组件直接依赖于特定的低级实现来fetchDataFromDatabase从数据库获取数据。

这违反了依赖倒置原则,因为高级组件 ( App) 与特定的低级组件 ( fetchDataFromDatabase) 紧密耦合。低级实现中的任何更改或替换都需要修改高级组件。

为了遵守依赖倒置原则,高级组件(App)应该依赖于抽象或接口,而不是具体的低级实现。通过这样做,高级组件与具体实现解耦,使其更加灵活且更易于维护。

# 结论

SOLID 原则提供了指导原则,使开发人员能够创建设计良好、可维护且可扩展的软件解决方案。通过遵循这些原则,开发人员可以实现模块化、代码可重用性、灵活性并降低代码复杂性。

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM