做者:Dmitri Pavlutinjavascript
原文连接html
正交性是几何学中的术语,互为直角的直角坐标系就具备正交性; 在计算技术中表示不依赖性或解耦性。非正交的系统意味着系统中各组件互相高度依赖,这类系统中是再也不有局部修正的状况了。
在5年前,我正在为一家欧洲初创公司开发跨平台移动应用。初期的功能是易于实现的,进展顺利。java
6个月过去,须要不断的在现有功能上添加新的功能,随着时间的推移,对现有模块的更改愈来愈困难。react
在部分需求上,开始拒绝某些新的功能和更新,由于它们将须要太多的时间实施。这个故事以移动应用程序彻底重写为原生应用而了结,主要是由于进一步的维护很是的困难。ios
我将上述问题归咎于跨平台框架中的错误,归咎于客户端需求变动。但这不是主要问题,我没有意识到一点,我一直在于高度耦合的模块组件作战,就像堂吉柯德大战风车同样。编程
我忽略了组件易于更改的特性。我未遵循良好的设计原则,没有赋予组件适应潜在的变化的特性。学习设计原则,一个特别有影响力的正交原理,它能够隔离因为不一样缘由而变化的事物。axios
若是A和B正交的,则更改A不会更改B(反之亦然)。这就是正交性的概念。在广播设备中,音量和电台选择控件是正交的。音量控制仅更改音量,而电台选择控件仅更改接收到的电台。浏览器
想象一下广播设备坏了,音量控制可更改音量,但也可修改选定的广播电台。音量控制和电台选择控制不是正交的:音量控制会产生反作用。当你尝试向紧密耦合的组件中添加更改时,也会发生相同的状况:你不得不面对更改产生的反作用。服务器
若是一个组件的更改不影响其余组件,则两个或多个组件正交。例如,显示文章列表的组件应与获取文章的逻辑正交。微信
一个好的React应用程序设计是正交的:
将组件隔离,并独立封装。这将使你的组件正交,而且你所作的任何更改都将被隔离,而且仅集中在一个组件上。这就是可预测且易于开发的系统的诀窍。
让咱们来看看下面的例子:
import React, { useState } from 'react'; import axios from 'axios'; import EmployeesList from './EmployeesList'; function EmployeesPage() { const [isFetching, setFetching] = useState(false); const [employees, setEmployees] = useState([]); useEffect(function fetch() { (async function() { setFetching(true); const response = await axios.get("/employees"); setEmployees(response.data); setFetching(false); })(); }, []); if (isFetching) { return <div>Fetching employees....</div>; } return <EmployeesList employees={employees} />; }
在以上代码中<EmployeesPage>
经过axios库,执行GET请求获取数据。
若是之后从axios和REST切换到GraphQL会发生什么?若是应用程序具备数十个与获取数据逻辑耦合的组件,则必须手动更改全部组件。其实有更好的方法,让咱们从组件中分离出获取数据逻辑细节。
一个很好的方法是使用React的新功能Suspense:
import React, { Suspense } from "react"; import EmployeesList from "./EmployeesList"; function EmployeesPage({ resource }) { return ( <Suspense fallback={<h1>Fetching employees....</h1>}> <EmployeesFetch resource={resource} /> </Suspense> ); } function EmployeesFetch({ resource }) { const employees = resource.employees.read(); return <EmployeesList employees={employees} />; }
如今,直到<EmployeesFetch>
读取异步资源以前,<EmployeesPage>
都会挂起.
重要的是<EmployeesPage>
与获取数据逻辑正交。<EmployeesPage>
不在意axios是否实现抓取,你能够轻松地将axios更改成本地获取、或迁移为GraphQL:<EmployeesPage>
不受影响。
假设您你要跳转到顶部按钮,以在用户向下滚动500px以上时显示。单击该按钮时,页面将自动滚动到顶部。
<ScrollToTop>
第一个简单的实现:
import React, { useState, useEffect } from 'react'; const DISTANCE = 500; function ScrollToTop() { const [crossed, setCrossed] = useState(false); useEffect( function() { const handler = () => setCrossed(window.scrollY > DISTANCE); handler(); window.addEventListener("scroll", handler); return () => window.removeEventListener("scroll", handler); }, [] ); function onClick() { window.scrollTo({ top: 0, behavior: "smooth" }); } if (!crossed) { return null; } return <button onClick={onClick}>Jump to top</button>; }
<ScrollToTop>
实现滚动监听器并呈现一个将页面滚动到顶部的按钮,问题在于这些概念可能会以不一样的形式变化。
更好的正交设计应将滚动监听器与UI隔离,让咱们将滚动监听器逻辑提取到自定义钩子useScrollDistance()
中:
import { useState, useEffect } from 'react'; function useScrollDistance(distance) { const [crossed, setCrossed] = useState(false); useEffect(function() { const handler = () => setCrossed(window.scrollY > distance); handler(); window.addEventListener("scroll", handler); return () => window.removeEventListener("scroll", handler); }, [distance]); return crossed; }
而后,在组件<IfScrollCrossed>
中使用useScrollAtBottom()
:
function IfScrollCrossed({ children, distance }) { const isBottom = useScrollDistance(distance); return isBottom ? children : null; }
<IfScrollCrossed>
仅在用户滚动特定距离时才显示,最后,这是单击时滚动到顶部的按钮:
function onClick() { window.scrollTo({ top: 0, behavior: 'smooth' }); } function JumpToTop() { return <button onClick={onClick}>Jump to top</button>; }
如今,若是你想使一切正常工做,只需将<JumpToTop>
放在<IfAtBottom>
的中便可:
import React from 'react'; // ... const DISTANCE = 500; function MyComponent() { // ... return ( <IfScrollCrossed distance={DISTANCE}> <JumpToTop /> </IfScrollCrossed> ); }
重要的是<IfScrollCrossed>
隔离滚动监听器,UI元素的更改也隔离在<JumpToTop>
组件中,在这里滚动监听器逻辑和UI元素是正交的。另外一个好处是你能够将<IfScrollCrossed>
与任何UI结合使用。例如,当用户向下滚动300px时,您能够显示新闻表单:
import React from 'react'; // ... const DISTANCE_NEWSLETTER = 300; function OtherComponent() { // ... return ( <IfScrollCrossed distance={DISTANCE_NEWSLETTER}> <SubscribeToNewsletterForm /> </IfScrollCrossed> ); }
尽管将变化隔离到单独的组件中是正交性的所有内容,可是可能因为不一样的缘由改变组件。这些就是所谓的“Main”组件(也称为“App”)组件。
你能够在最外层的index.jsx(或app。jsx)文件内找到“Main”组件:即启动应用程序的组件。它知道有关该应用程序的全部细节:初始化全局状态提供程序(如Redux),配置获取库(如GraphQL Apollo),将路由与组件关联等等。
你可能有几个“Main”组件:用于客户端(在浏览器中运行)和用于服务器端(实现服务器端渲染)。
当组件是正交设计时,对组件所作的任何更改都将隔离在组件内。
因为正交组件仅负责一个任务,所以更容易了解该组件的功能,它不被不属于这里的细节所困扰。
正交组件仅专一于执行单个任务,你要作的只是测试组件是否正确执行任务。一般,非正交组件须要大量的模拟和手动设置才能进行测试,并且,若是难以测试。而如今你只需修改单个组件。
我喜欢新的React功能,例如hooks
,suspense
等。可是,我也尝试着更普遍地思考,探索这些功能是否有助于我遵循良好的设计。
让咱们回想一下“星球大战之西斯的复仇”电影中的一幕。在阿纳金·天行者被他的前导师欧比·旺·克诺比(Obi-Wan Kenobi)击败后,后者说:
给原力带来平衡,不要把它留在黑暗中
阿纳金·天行者被选为绝地武士,在黑暗与光明的两面之间取得平衡。
正交设计经过YAGNI: “You ain't gonna need it”原则来平衡。
YAGNI成为极限编程的原则:
始终在真正须要它们时执行这些事情,永远不要在仅仅预见到可能须要它们时才执行。
(个人理解:只有真正须要时才使用)
回顾一下文章的开头部分个人故事:我最终得到了一个困难且更改为本很高的应用程序,个人错误是:我无心中建立了并不是为更改而设计的组件。这是YAGNI的极端状况。
另外一方面,若是每条逻辑正交,那么你最终将建立过多不须要的抽象,这是正交设计的极限。
实际的方法是预见变化,详细研究你的应用程序解决的领域问题,并提供潜在功能的列表。若是你认为某个地方会发生变化,请使用正交设计原则。
编写软件不只与实现应用程序的要求有关,一样重要的是,要努力设计好组件。
良好设计的关键原则是隔离最有可能改变的逻辑:使其正交。这使你的整个系统具备灵活性,而且能够适应更改或添加新功能。
你想知道更多吗?你的下一步是能够阅读全英版:The Pragmatic Programmer。
ps: 微信公众号:Yopai,有兴趣的能够关注,每周不按期更新。不断分享,不断进步