簡體   English   中英

Javascript:如何用更多功能模式替換嵌套的if / else?

[英]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語句旁邊。 這是一個簡單而扁平的決策,只需簡單, 詳盡的案例分析。

我在這里強調詳盡無遺的一句話,因為至少為你的決定提供ifelse選擇分支是很重要的。 在您的情況下,我們有第三個選項,所以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方式抽象條件。

有一些數據類型可以派上用場抽象。 例如,您可以使用AnyAll monoid來抽象相關條件。 你可以使用Either或者Maybe

您還可以查看ifElsecondwhenifElseifElse 你已經看過總和類型了。 這些都是特定環境中強大而有用的策略。

但根據我的經驗,這些策略確實超出了觀點。 在視圖中,我們實際上希望可視化層次結構,以便了解它們將如何呈現。 所以三元組是一個很好的方法。

人們可以不同意“功能”的含義。 有人說功能編程是關於純度或參考透明度; 其他人可能會說它只是“用功能編程”。 不同的社區有不同的解釋。

因為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類型,它有三個可能的構造函數: SpinnerNoResultListComponent 輸入( 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM