做者: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,有興趣的能夠關注,每週不按期更新。不斷分享,不斷進步