[英]Javascript: How can I replace nested if/else with a more functional pattern?
我的React應用程序代碼庫中重復了以下模式:
const {items, loading} = this.props
const elem = loading
? <Spinner />
: items.length
? <ListComponent />
: <NoResults />
雖然這比嵌套實際的 if/else
子句更清晰,但我試圖擁抱更優雅和功能性的模式。 我已經閱讀過關於使用像Either
monad這樣的東西,但我所做的所有努力最終看起來更冗長,更少可重復使用(這個偽代碼可能不起作用,因為我試圖記住以前的嘗試):
import {either, F, isEmpty, prop} from 'ramda'
const isLoading = prop('loading')
const renderLoading = (props) => isLoading(props) ? <Spinner /> : false
const loadingOrOther = either(renderLoading, F)
const renderItems = (props) => isEmpty(props.items) ? <NoResults /> : <ListComponent />
const renderElem = either(loadingOrOther, renderItems)
const elems = renderElem(props)
我可以使用哪種模式更干/可重復使用?
謝謝!
雖然這比嵌套實際的
if/else
子句更清晰render () { const {items, loading} = this.props return loading ? <Spinner /> : items.length ? <ListComponent items={items} /> : <NoResults /> }
你發布了不完整的代碼,所以我填補了一些空白,以獲得更具體的例子。
查看代碼,我發現很難讀取條件的位置以及返回值的位置。 條件分散在各種縮進級別的各行中 - 同樣,返回值也沒有視覺一致性。 實際上,直到您進一步閱讀程序才能看到loading
return loading
甚至是一個條件並不明顯?
。 在這種情況下選擇要呈現的組件是一個平坦的決策,代碼的結構應該反映出來。
使用if/else
會在此處生成一個非常易讀的示例。 沒有嵌套,您可以看到返回的各種類型的組件,整齊地放在相應的return
語句旁邊。 這是一個簡單而扁平的決策,只需簡單, 詳盡的案例分析。
我在這里強調詳盡無遺的一句話,因為至少為你的決定提供if
和else
選擇分支是很重要的。 在您的情況下,我們有第三個選項,所以else if
使用另一個選項。
render () {
const {items, loading} = this.props
if (loading)
return <Spinner />
else if (items.length)
return <ListComponent items={items} />
else
return <NoResults />
}
如果你看一下這段代碼並嘗試“修復”它,因為你正在考慮“擁抱更優雅和功能性的模式” ,你會誤解“優雅”和“功能”。
嵌套三元表達式沒有任何優雅。 功能編程不是用最少量的擊鍵來編寫程序,導致程序過於簡潔且難以閱讀。
if/else
語句就像我使用的那樣,並不是那么“功能性”,因為它們涉及不同的語法。 當然,它們比三元表達式更冗長,但它們正如我們所希望的那樣運行,並且它們仍然允許我們聲明功能行為 - 不要讓語法單獨強迫你做出關於編碼風格的愚蠢決定。
我同意不幸的是, if
是JavaScript中的語句而不是表達式 ,那就是你給予的工作。 您仍然能夠通過這種約束生成優雅且功能強大的程序。
備注
我個人認為依賴於真實的價值觀。 我寧願把你的代碼寫成
render () {
const {items, loading} = this.props
if (loading) // most important check
return <Spinner />
else if (items.length === 0) // check of next importance
return <NoResults />
else // otherwise, everything is OK to render normally
return <ListComponent items={items} />
}
與代碼相比,這不太可能吞下錯誤。 例如,假裝你的組件以某種方式具有loading={false} items={null}
prop值 - 你可以說你的代碼會優雅地顯示NoResults
組件; 我認為組件處於非加載狀態且沒有項目是錯誤的,我的代碼會產生錯誤以反映: Cannot read property 'length' of null
。
這告訴我在這個組件的范圍之上發生了一個更大的問題 - 即這個組件有load = true 或者某些項目數組(空或其他); 沒有其他道具組合是可以接受的。
我認為你的問題並不是關於if語句與三元組的關系。 我認為您可能正在尋找不同的數據結構,允許您以強大的DRY方式抽象條件。
有一些數據類型可以派上用場抽象。 例如,您可以使用Any
或All
monoid來抽象相關條件。 你可以使用Either
或者Maybe
。
您還可以查看ifElse
的cond
, when
和ifElse
等ifElse
。 你已經看過總和類型了。 這些都是特定環境中強大而有用的策略。
但根據我的經驗,這些策略確實超出了觀點。 在視圖中,我們實際上希望可視化層次結構,以便了解它們將如何呈現。 所以三元組是一個很好的方法。
人們可以不同意“功能”的含義。 有人說功能編程是關於純度或參考透明度; 其他人可能會說它只是“用功能編程”。 不同的社區有不同的解釋。
因為FP對不同的人意味着不同的東西,所以我將專注於一個特定的屬性,即聲明性代碼。
聲明性代碼在一個地方定義一個算法或一個值,並且不會強制改變或改變單獨的部分。 聲明要求寫明的東西是什么,而不是通過勢在必行不同的代碼路徑名稱分配值。 您的代碼目前是聲明性的,這很好! 聲明性代碼提供了保證:例如“此函數肯定會返回,因為return
語句位於第一行”。
這是錯誤的觀念,即三元組是嵌套的,而if語句則是扁平的。 這只是格式化的問題。
return (
condition1
? result1
: condition2
? result2
: condition3
? result3
: otherwise
)
將條件放在自己的行上,然后嵌套響應。 您可以根據需要多次重復此操作。 最后的“else”就像任何其他結果一樣縮進,但它沒有條件。 它可以根據您的需要擴展到盡可能多的情況。 我已經看到並用這樣的許多扁平三元組寫了視圖,我發現更容易完全遵循代碼,因為路徑沒有分開。
你可能會說if
聲明的可讀性,但我認為再次可讀意味着不同的事情不同的人。 所以要打開它,讓我們考慮一下我們正在強調的內容 。
當我們使用三元時,我們強調只有一種可能的方式來聲明或返回某些東西。 如果函數只包含表達式,那么我們的代碼更可能作為公式讀取,而不是公式的實現。
當我們使用if語句時,我們強調單獨的,分開的步驟來產生輸出。 如果您更願意將您的觀點視為單獨的步驟,那么如果陳述有意義。 如果您希望將視圖視為具有基於上下文的不同表示的單個實體,則三元組和聲明性代碼會更好。
總而言之,您的代碼已經正常運行。 可讀性和易讀性是主觀的,專注於您想要強調的內容。 不要覺得表達式中的多個條件是代碼氣味,它只是代表UI的復雜性,解決這個問題的唯一方法(如果需要解決)就是改變UI的設計。 UI代碼被允許是復雜的,並且讓代碼誠實且代表所有潛在狀態並不是一件容易的事。
您可以使用sum類型和模式匹配來避免if/else
語句。 由於Javascript不包含這些功能,您必須自己實現它們:
const match = (...patterns) => (...cons) => o => {
const aux = (r, i) => r !== null ? cons[i](r)
: i + 1 in patterns ? aux(patterns[i + 1](o), i + 1)
: null;
return aux(patterns[0](o), 0);
};
match
需要一堆模式函數和構造函數和數據。 除非匹配,否則針對數據測試每個模式函數。 然后使用成功模式函數的結果調用相應的構造函數,並返回最終結果。
為了使match
能夠識別模式匹配是否不成功,模式必須實現一個簡單的協議:每當模式不匹配時,該函數必須返回null
。 如果模式匹配但相應的構造函數是一個無效的構造函數,它必須只返回一個空的Object
。 這是微調框的模式函數:
({loading}) => loading ? {} : null
由於我們使用解構賦值來模仿模式匹配,因此我們必須將每個模式函數包裝在try/catch
塊中,以避免在解構期間出現未捕獲的錯誤。 因此,我們不是直接調用模式函數,而是使用特殊的應用程序:
const tryPattern = f => x => {
try {
return f(x);
} catch (_) {
return null;
}
};
最后,這是一個微調框的構造函數。 它不需要參數並返回一個JSX微調器元素:
const Spinner = () => <Spinner />;
讓我們把它們放在一起看看它是如何工作的:
// main function const match = (...patterns) => (...cons) => x => { const aux = (r, i) => r !== null ? cons[i](r) : i + 1 in patterns ? aux(patterns[i + 1](x), i + 1) : null; return aux(patterns[0](x), 0); }; // applicator to avoid uncaught errors during destructuring const tryPattern = f => x => { try { return f(x); } catch (_) { return null; } }; // constructors const Spinner = () => "<Spinner />"; const NoResult = () => "<NoResult />"; const ListComponent = items => "<ListComponent items={items} />"; // sum type const List = match( tryPattern(({loading}) => loading ? {} : null), tryPattern(({items: {length}}) => length === 0 ? {} : null), tryPattern(({items}) => items !== undefined ? items : null) ); // mock data props1 = {loading: true, items: []}; props2 = {loading: false, items: []}; props3 = {loading: false, items: ["<Item />", "<Item />", "<Item />"]}; // run... console.log( List(Spinner, NoResult, ListComponent) (props1) // <Spinner /> ); console.log( List(Spinner, NoResult, ListComponent) (props2) // <NoResult /> ); console.log( List(Spinner, NoResult, ListComponent) (props3) // <ListComponent /> );
現在我們有一個List
sum類型,它有三個可能的構造函數: Spinner
, NoResult
和ListComponent
。 輸入( props
)確定最終使用的構造函數。
如果List(Spinner, NoResult, ListComponent)
對您來說仍然過於費力,並且您不希望顯式List
的各個狀態,則可以在求和類型定義期間傳遞構造函數:
const List = match(
tryPattern(({loading}) => loading ? {} : null),
tryPattern(({items: {length}}) => length === 0 ? {} : null),
tryPattern(({items}) => items)
) (
Spinner,
NoResult,
ListComponent
);
現在你可以簡單地以干燥的方式調用List(props1)
等。
如果沒有模式匹配,則match
靜默返回null。 如果您想要保證至少有一個模式成功匹配,您也可以拋出錯誤。
由於ifElse
具有ifElse
函數,您可以使用它以可重用的無點樣式編寫條件。
可運行的示例(使用字符串而不是<Tags>
因此它可以作為堆棧代碼運行)。
const { compose, ifElse, always, prop, isEmpty } = R; const renderItems = ifElse(isEmpty, always('noResults'), always('listComponent')); const renderProps = ifElse( prop('loading'), always('spinner'), compose(renderItems, prop('items')) ); // usage: const elem = renderProps(this.props); // test console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] })); console.log(renderProps({ loading: false, items: [] })); console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
當然,另一種選擇是使用箭頭函數和條件運算符將條件分成兩個函數。 與上面的示例一樣,這為您提供了可重用的renderItems
函數:
const renderItems = list => list.length ? 'listComponent' : 'noResults'; const renderProps = props => props.loading ? 'spinner' : renderItems(props.items); // usage: const elem = renderProps(this.props); // test console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] })); console.log(renderProps({ loading: false, items: [] })); console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
您不必為此安裝額外的包:
content() {
const {items, loading} = this.props
if (loading) {
return <Spinner />;
}
return items.length ? <ListComponent /> : <NoResult />;
}
render() {
return this.content();
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.