当前位置: 首页 > 科技观察

如何在React中应用SOLID原则?_0

时间:2023-03-14 15:40:14 科技观察

大家好,我叫CUGGZ。在面向对象编程(OOP)中,SOLID原则是设计模式的基础,它的每一个字母代表一个设计原则:单一职责原则(SRP)开闭原则(OCP)里氏替换原则(LSP)接口隔离原则(ISP)依赖倒置原则(DIP)让我们来看看每个原则是什么意思,以及如何在React中应用SOLID原则!1.单一职责原则(SRP)单一职责原则的定义是每个类应该只有一个职责,即只能做一件事。这个原则最容易解释,因为我们可以简单地理解为“每个功能/模块/组件应该只做一件事”。在所有这些原则中,单一职责原则是最容易遵循的,也是最有影响力的,因为它极大地提高了代码的质量。为了保证组件只做一件事,可以这样:将功能较多的大组件拆分成较小的组件。将与组件功能无关的代码提取到单独的函数中。将相关功能提取到自定义Hooks中。让我们看一个显示活跃用户列表的组件:response=awaitfetch('/some-api')constdata=awaitresponse.json()setUsers(data)}loadUsers()},[])constweekAgo=newDate();weekAgo.setDate(weekAgo.getDate()-7);返回(

    {users.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo).map(user=>

    {user.fullName}

    {user.role})}
)}这个组件虽然代码不多,但是做很多事情:获取数据、过滤数据、渲染数据。让我们看看如何分解它。首先,只要useState和useEffect都用到,就可以提取到自定义的Hook中:constuseUsers=()=>{const[users,setUsers]=useState([])useEffect(()=>{constloadUsers=async()=>{constresponse=awaitfetch('/some-api')constdata=awaitresponse.json()setUsers(data)}loadUsers()},[])return{users}}constActiveUsersList=()=>{const{users}=useUsers()constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)return(
    {users.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo).map(user=>

    {user.fullName}

    {user.role})}
)}现在,useUsersHook只关心一件事——从API获取用户。它使我们的组件代码更具可读性。接下来查看组件渲染的JSX。每当我们遍历一个对象数组时,我们应该意识到它为每个数组项生成的JSX的复杂性。如果它是一个没有附加任何事件处理程序的单行代码,那么将它保持内联是完全可以的。但是对于更复杂的JSX,将它提取到一个单独的组件中可能是一个更好的主意:constUserItem=({user})=>{return(
  • {user.fullName}

    {user.role}
  • )}constActiveUsersList=()=>{const{users}=useUsers()constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)return(
      {users.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo).map(user=>)}
    )}这里将渲染用户信息的逻辑提取到一个单独的组件中,使我们的组件更小,更易读。最后,API获取的用户列表中过滤掉所有非活跃用户的逻辑比较独立,可以在其他部分复用,所以可以抽取出一个public函数:constgetOnlyActive=(users)=>{constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)returnusers.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo)}constActiveUsersList=()=>{const{用户}=useUsers()return(
      {getOnlyActive(users).map(user=>)}
    )}至此,通过拆解以上三步,组件就变得比较简单了。不过仔细一看,这个组件还是有优化空间的。目前组件是先取数据,然后需要过滤数据。理想情况下,我们只想获取数据并渲染它而无需任何额外操作。因此,这个逻辑可以封装成一个新的自定义Hook,最终代码如下://getdataconstuseUsers=()=>{const[users,setUsers]=useState([])useEffect(()=>{constloadUsers=async()=>{constresponse=awaitfetch('/some-api')constdata=awaitresponse.json()setUsers(data)}loadUsers()},[])return{users}}//列表渲染constUserItem=({user})=>{return(
  • {user.fullName}

    {user.role}
  • )}//列表过滤器constgetOnlyActive=(users)=>{constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)returnusers.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo)}constuseActiveUsers=()=>{const{users}=useUsers()constactiveUsers=useMemo(()=>{returngetOnlyActive(users)},[users])返回{activeUsers}}constActiveUsersList=()=>{const{activeUsers}=useActiveUsers()return(
      {activeUsers.map(user=>)}
    )}这里我们创建useActiveUsersHook来处理获取和过滤数据的逻辑,而组件只做最少的事情——渲染它从Hook获取的数据现在,这个组件只有两个职责:获取数据和渲染数据,当然我们也可以在父类中component获取数据,通过props传递给组件,这样组件只需要渲染即可。当然,还是要视情况而定。我们可以简单地认为获取和渲染数据是“一件事”。总而言之,遵循单一职责原则,我们有效地将大量独立的代码进行了模块化,模块化的代码更易于测试和维护。2.开闭原则(OCP)开闭原则指出“一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭”。开闭原则提倡以一种允许在不更改源代码的情况下扩展组件的方式来构建组件。让我们看一个场景。有一个Header组件,可以在不同的页面上使用。根据页面的不同,Header组件的UI应该略有不同:constHeader=()=>{const{pathname}=useRouter()return(
    {pathname==='/dashboard'&&创建事件}{pathname==='/'&&转到仪表板}
    )}constHomePage=()=>(<>
    )constDashboardPage=()=>(<>
    )此处,根据页面的不同,显示指向不同页面组件的链接。现在想一想,如果需要在更多的页面中添加这个Header组件会怎样?每次创建新页面时,都需要引用Header组件并修改其内部实现。这种做法使得Header组件与使用它的上下文紧密耦合,违反了开闭原则。为了解决这个问题,我们可以使用组件组合。Header组件不需要关心它将在内部呈现什么,而是可以将此责任委托给将使用children属性的组件:constHeader=({children})=>(
    {children}
    )constHomePage=()=>(<>
    转到仪表板
    )constDashboardPage=()=>(<>
    创建事件
    )使用此方法,我们彻底去掉了Header组件内部的变量逻辑。现在您可以使用组合将任何您想要的内容放入Header中,而无需修改组件本身。遵循开闭原则,可以降低组件之间的耦合度,提高可扩展性和复用性。3.LiskovSubstitutionPrinciple(LSP)LiskovSubstitutionPrinciple可以理解为对象之间的一种关系,子类型对象应该可以被超类型对象替换。这个原则在很大程度上依赖于类继承来定义超类型和子类型关系,但它可能不适用于React,因为我们几乎从不处理类,更不用说类继承了。虽然远离类继承不可避免地会将这一原则完全变成其他东西,但使用继承编写React代码会使代码变得糟糕(React团队不推荐继承)。因此,这个原理就不做过多解释了。4.接口隔离原则(ISP)根据接口隔离原则,客户端不应该依赖它不需要的接口。为了更好地说明ISP所针对的问题,让我们看一个呈现视频列表的组件:{items})=>{return(
      {items.map(item=>)}
    )}Thumbnail组件的实现如下:typeProps={video:Video}constThumbnail=({video}:Props)=>{return}Thumbnail组件很小很简单,但是它有一个问题:它期望将完整的视频对象作为道具传入,但只有一个属性(coverUrl)被有效使用。除了视频,我们还需要渲染直播的缩略图,这两种媒体资源会混合在同一个列表中。让我们定义直播流类型LiveStream:typeLiveStream={name:stringpreviewUrl:string}这是更新后的VideoList组件:typeProps={items:Array}constVideoList=({items})=>{return(
      {items.map(item=>{if('coverUrl'initem){return}else{//直播组件怎么写?}})}
    )}这时,我们发现了一个问题。我们可以轻松区分视频和直播对象,但我们无法将后者传递给Thumbnail组件,因为Video和LiveStream类型不兼容。它们包含不同的属性来保存缩略图:视频对象调用coverUrl,直播对象调用previewUrl。这就是使组件依赖于比实际更多的道具的原因。让我们重构Thumbnail组件以确保它只依赖于它需要的道具:这个修改,现在我们可以用它来渲染视频和直播流的缩略图:typeProps={items:Array}constVideoList=({items})=>{return(
      {items.map(item=>{if('coverUrl'initem){return}else{return}})}
    )}当然这段代码可以简化:typeProps={items:Array}constVideoList=({items})=>{return(
      {items.map(item=>())}
    )}接口隔离原则提倡尽量减少系统组件之间的依赖关系,降低它们之间的耦合度,从而增加可重用性。5.依赖倒置原则(DIP)依赖倒置原则指出“依赖于抽象,而不是具体”。换句话说,一个组件不应该直接依赖于另一个组件,相反,它们应该都依赖于一些共同的抽象。在这里,“组件”指的是应用程序的任何部分,可以是React组件、函数、模块或第三方库。这个原理可能比较难理解,我们来看一个具体的例子。有一个LoginForm组件,它在提交表单时将用户凭据发送到某些API:importapifrom'~/common/api'constLoginForm=()=>{const[email,setEmail]=useState('')const[password,setPassword]=useState('')consthandleSubmit=async(evt)=>{evt.preventDefault()等待api。login(email,password)}return(setEmail(e.target.value)}/>setPassword(e.target.value)}/>Login)}在这段代码中,LoginForm组件直接引用了api模块,因此它们之间存在紧耦合。这种依赖性会导致一个组件的更改影响其他组件。依赖倒置原则提倡打破这种耦合,让我们看看如何实现。首先,从LoginForm组件中删除对api模块的直接引用,而是允许通过props传递所需的回调函数:typeProps={onSubmit:(email:string,password:string)=>Promise}constLoginForm=({onSubmit}:Props)=>{const[email,setEmail]=useState('')const[password,setPassword]=useState('')consthandleSubmit=async(evt)=>{evt.preventDefault()awaitonSubmit(email,password)}return(setEmail(e.target.value)}/>setPassword(e.target.value)}/>登录)}通过通过此修改,LoginForm组件不再依赖于api模块。通过onSubmit回调函数抽象出向API提交凭据的逻辑,现在父组件负责提供逻辑的具体实现。为此,创建了一个ConnectedLoginForm组件,将表单提交逻辑委托给api模块:importapifrom'~/common/api'awaitapi.login(email,password)}return()}ConnectedLoginForm组件充当api和LoginForm之间的粘合剂,而它们本身保持完全独立。这使得这两个组件可以独立修改和维护,而不用担心修改会影响其他组件。依赖倒置原则旨在最小化应用程序不同组件之间的耦合。您可能已经注意到,最小化是所有SOLID原则中反复出现的关键字——从最小化单个组件的职责到最小化它们之间的依赖性等等。6.小结通过上面的例子,相信你对如何在React中使用SOLID原理有了一定的了解。应用SOLID原则使我们的React代码更易于维护和健壮。但是请注意,虔诚地遵循这些原则可能会造成破坏并导致代码过度设计,因此我们应该学会识别组件的进一步分解或解耦何时会导致复杂性增加而收效甚微。