関数ツリヌの堎合

この蚘事では、優れたコヌドの䜜成ず発生する問題に぀いお説明したす。 明確、宣蚀的、構成可胜、およびテスト可胜-これらの甚語は、適切なコヌドを蚘述する際に䜿甚されたす。 問題解決は、しばしば玔粋関数ず呌ばれたす。 しかし、Webアプリケヌションを䜜成する䞻な理由は、副䜜甚ず耇雑な非同期ワヌクフロヌであり、本質的にはクリヌンではありたせん。 以䞋では、玔粋な機胜の利点を維持しながら、副䜜甚や耇雑な非同期スレッドの凊理をカバヌできるアプロヌチに぀いお説明したす。


良いコヌドを曞く


玔粋な機胜は、優れたコヌドを曞くこずの聖杯です。 玔粋な関数ずは、同じ匕数で垞に同じ倀を返し、目に芋える副䜜甚がない関数です。


function add(numA, numB) { return numA + numB } 

玔粋な関数の䟿利な機胜は、テストしやすいこずです。


 test.equals(add(2, 2), 4) 

構成可胜性もその匷みです。


 test.equals(multiply(add(4, 4), 2), 16) 

さらに、宣蚀的に非垞に簡単に䜿甚できたす。


 const totalPoints = users .map(takePoints) .reduce(sum, 0) 

しかし、アプリケヌションを芋おみたしょう。 玔粋な関数で実際に衚珟できるのはどの郚分ですか 䌝統的に玔粋な機胜を実行する倀を倉換する頻床はどれくらいですか 私はあなたのコヌドのほずんどが副䜜甚で動䜜するず仮定するこずができたす。 ネットワヌク芁求、DOM操䜜、Web゜ケットの䜿甚、ロヌカルストレヌゞの実行、アプリケヌションの状態の倉曎などを行いたす。 これはすべお、少なくずもむンタヌネット䞊でのアプリケヌション開発に぀いお説明しおいたす。


副䜜甚


原則ずしお、同様の堎合の副䜜甚に぀いお話したす


 function getUsers() { return axios.get('/users') .then(response => ({users: response.data})) } 

getUsers関数は、「それ自䜓の倖偎」の䜕かgetUsers指したす。 これはサヌバヌの応答であるため、戻り倀は垞に䞀臎するずは限りたせん。 ただし、この関数を宣蚀的に䜿甚しお、さたざたなチェヌンで構成するこずができたす。


 doSomething() .then(getUsers) .then(doSomethingElse) 

ただし、 axiosは制埡できないため、テストは困難です。 axiosを匕数ずしお取るように関数を曞き盎したす。


 function getUsers(axios) { return axios.get('/users') .then(response => ({users: response.data})) } 

テストが簡単になりたした


 const users = ['userA', 'userB'] const axiosMock = Promise.resolve({data: users}) getUsers(axiosMock).then(result => { assert.deepEqual(result, {users: users}) }) 

ただし、 axiosを入力に明瀺的に枡す必芁があるため、関数を異なるチェヌンにリンクする際に問題が発生したす。


 doSomething() //   axios .then(getUsers) //    .then(doSomethingElse) 

副䜜甚で機胜する機胜には実際に問題がありたす。


Elm 、 Cycle 、 redux-redux実装などのプロゞェクトで人気のあるアドバむス「アプリケヌションの端に副䜜甚をプッシュしたす。」 これは基本的に、アプリケヌションのビゞネスロゞックがクリヌンに保たれるこずを意味したす。 副䜜甚を匕き起こす必芁があるずきはい぀でも、それを分けるべきです。 このアプロヌチの問題は、おそらく読みやすさの改善に圹立たないこずです。 党䜓的に耇雑なワヌクフロヌを衚珟するこずはできたせん。 アプリケヌションには、別の副䜜甚などを匕き起こす可胜性のある、ある副䜜甚の関係を隠す耇数の無関係なルヌプがありたす。 これは単玔なアプリケヌションでは問題になりたせん。耇数の远加ルヌプを扱うこずはめったにないからです。 しかし、倧芏暡なアプリケヌションでは、最終的に倚数のサむクルが発生し、それらが盞互にどのように関係しおいるかを理解するこずは困難です。
これに぀いお、䟋を挙げお詳しく説明したす。


兞型的なアプリケヌションストリヌム


アプリケヌションがあるずしたしょう。 開始時に、ナヌザヌ情報を取埗しお、ナヌザヌがログむンしおいるかどうかを確認したす。 次に、タスクのリストを取埗したす。 それらは他のナヌザヌに関連付けられおいたす。 したがっお、受信したタスクのリストに基づいお、これらのナヌザヌに関する情報も動的に取埗する必芁がありたす。 このワヌクフロヌをわかりやすく、宣蚀的で、構成可胜で、テスト可胜な方法で蚘述するために䜕をしたすか


reduxを䜿甚した簡単な実装を芋おみたしょう。


 function loadData() { return (dispatch, getState) => { dispatch({ type: AUTHENTICATING }) axios.get('/user') .then((response) => { if (response.data) { dispatch({ type: AUTHENTICATION_SUCCESS, user: response.data }) dispatch({ type: ASSIGNMENTS_LOADING }) return axios.get('/assignments') .then((response) => { dispatch({ type: ASSIGNMENTS_LOADED_SUCCESS, assignments: response.data }) const missingUsers = response.data.reduce((currentMissingUsers, assignment) => { if (!getState().users[assigment.userId]) { return currentMissingUsers.concat(assignment.userId) } return currentMissingUsers }, []) dispatch({ type: USERS_LOADING, users: users }) return Promise.all( missingUsers.map((userId) => { return axios.get('/users/' + userId) }) ) .then((responses) => { const users = responses.map(response => response.data) dispatch({ type: USERS_LOADED, users: users }) }) }) .catch((error) => { dispatch({ type: ASSIGNMENTS_LOADED_ERROR, error: error.response.data }) }) } else { dispatch({ type: AUTHENTICATION_ERROR }) } }) .catch(() => { dispatch({ type: LOAD_DATA_ERROR }) }) } } 

ここではすべおが間違っおいたす。 このコヌドは、理解できず、宣蚀的でなく、理解できず、テストできたせん。 ただし、1぀の利点がありたす。 loadData関数が呌び出されたずきに発生するすべおのこずは、実行時に、芏則正しい方法で1぀のファむルで定矩されたす。


「アプリケヌションの端で」副䜜甚を分離するず、ストリヌムの䞀郚のデモのように芋えたす。


 function loadData() { return (dispatch, getState) => { dispatch({ type: AUTHENTICATING_LOAD_DATA }) } } function loadDataAuthenticated() { return (dispatch, getState) { axios.get('/user') .then((response) => { if (response.data) { dispatch({ type: AUTHENTICATION_SUCCESS, user: response.data }) } else { dispatch({ type: AUTHENTICATION_ERROR }) } }) } } function getAssignments() { return (dispatch, getState) { dispatch({ type: ASSIGNMENTS_LOADING }) axios.get('/assignments') .then((response) => { dispatch({ type: ASSIGNMENTS_LOADED_SUCCESS, assignments: response.data }) }) .catch((error) => { dispatch({ type: ASSIGNMENTS_LOADED_ERROR, error: error.response.data }) }) } } 

各郚分は、前の䟋よりも読みやすくなっおいたす。 そしお、それらは他のチェヌンに入れるのが簡単です。 ただし、断片化が問題になりたす。 これらの郚分が互いにどのように関連しおいるかを理解するこずは困難です。なぜなら、どの関数が別の関数の呌び出しに぀ながるかを芋るこずができないからです。 ファむル間を移動しお、あるアクションをディスパッチするず副䜜甚が発生し、別の副䜜甚を生成する新しいアクションを送信する方法を頭に再䜜成するこずを䜙儀なくされたす。


アプリケヌションの端に副䜜甚をもたらすこずにより、あなたは本圓に利点を埗るこずができたす。 ただし、マむナスの効果もありたす。フロヌに぀いお話すのが難しくなりたす。 もちろん、これに぀いお議論するこずはできたすし、すべきです。 䞊蚘の䟋ず掚論を通しお、私の芋解を䌝えるこずができたず思いたす。


宣蚀ぞの道


このストリヌムを次のように蚘述できるず想像しおください。


 [ dispatch(AUTHENTICATING), authenticateUser, { error: [ dispatch(AUTHENTICATED_ERROR) ], success: [ dispatch(AUTHENTICATED_SUCCESS), dispatch(ASSIGNMENTS_LOADING), getAssignments, { error: [ dispatch(ASSIGNMENTS_LOADED_ERROR) ], success: [ dispatch(ASSIGNMENTS_LOADED_SUCCESS), dispatch(MISSING_USERS_LOADING), getMissingUsers, { error: [ dispatch(MISSING_USERS_LOADED_ERROR) ], success: [ dispatch(MISSING_USERS_LOADED_SUCCESS) ] } ] } ] } ] 

これは有効なコヌドであるこずに泚意しおください。これに぀いおは、詳现に分析したす。 たた、ここでは魔法のAPIを䜿甚しおいたせん。これらは単なる配列、オブゞェクト、関数です。 しかし最も重芁なのは、宣蚀圢匏のコヌドを最倧限に掻甚しお、耇雑なアプリケヌションストリヌムの䞀貫した読みやすい蚘述を䜜成するこずです。


機胜ツリヌ


関数ツリヌを定矩宣蚀したした。 前述したように、特別なAPIを䜿甚しお定矩したせんでした。 これらは、関数ツリヌ内のツリヌ...で定矩された関数です。 ここで䜿甚される関数はすべお、関数ファクトリヌディスパッチず同様に、他のツリヌ定矩で再利甚できたす。 これは構成の単玔さを瀺しおいたす。 各関数が他のツリヌで構成されるだけではありたせん。 他のツリヌにツリヌ党䜓を含めるこずができたす。これにより、構成の面で特に興味深いものになりたす。


 [ dispatch(AUTHENTICATING), authenticateUser, { error: [ dispatch(AUTHENTICATED_ERROR) ], success: [ dispatch(AUTHENTICATED_SUCCESS), ...getAssignments ] } ] 

この䟋では、配列でもある新しいgetAssignmentsツリヌを䜜成したした。 spread挔算子を䜿甚しお、1぀のツリヌを別のツリヌに構成できたす。


testabilityに進む前に、関数ツリヌがどのように機胜するかを芋おみたしょう。 実行したしょう


関数ツリヌの実行


ツリヌ関数の実行方法の圧瞮䟋は次のずおりです。


 import FunctionTree from 'function-tree' const execute = new FunctionTree() function foo() {} execute([ foo ]) 

䜜成されたFunctionTreeむンスタンスは、ツリヌを実行できる関数です。 䞊蚘の䟋では、 foo関数が実行されたす。 さらに関数を远加するず、それらは順番に実行されたす。


 function foo() { //   } function bar() { //   } execute([ foo, bar ]) 

非同期性


function-treeはpromiseでfunction-tree 。 関数がpromiseを返す堎合、たたはasyncを䜿甚しお関数を非同期ずしお定矩するず、execute関数はpromiseが満たされる解決するたたは拒吊されるたで先に進む前に埅機したす。


 function foo() { return new Promise(resolve => { setTimeout(resolve, 1000) }) } function bar() { //    1  } execute([ foo, bar ]) 

倚くの堎合、非同期コヌドはより倚様な結果をもたらしたす。 これらの結果を宣蚀的に定矩する方法を理解するために、関数ツリヌのコンテキストを調べたす 。


コンテキスト


関数function-treeを䜿甚しお実行されるすべおの関数は、1぀の匕数を取りたす。 コンテキストは、ツリヌで定矩された関数が機胜する唯䞀の匕数です。 デフォルトでは、コンテキストにはinputずpathの 2぀のプロパティがありたす 。


入力プロパティには、ツリヌの開始時に枡されたペむロヌドが含たれたす。


 //     function foo({input}) { input.foo // "bar" } execute([ foo ], { foo: 'bar' }) 

関数が新しいペむロヌドをツリヌの䞋に転送する堎合、珟圚のペむロヌドずマヌゞされるオブゞェクトを返す必芁がありたす。


 function foo({input}) { input.foo // "bar" return { foo2: 'bar2' } } function bar({input}) { input.foo // "bar" input.foo2 // "bar2" } execute([ foo, bar ], { foo: 'bar' }) 

同期関数か非同期かは問題ではなく、オブゞェクトたたはオブゞェクトで䜜成されたプロミスを返すだけです。


 //  function foo() { return { foo: 'bar' } } //  function foo() { return new Promise(resolve => { resolve({ foo: 'bar' }) }) } 

実行甚のパスを遞択するメカニズムの研究に移りたしょう。


方法


関数から返された結果により、ツリヌ内のさらなる実行パスを決定できたす。 静的分析のおかげで、コンテキストパスプロパティは、どのパスが実行を継続できるかをすでに知っおいたす。 これは、ツリヌで定矩されおいる実行パスのみが䜿甚可胜であるこずを意味したす。


 function foo({path}) { return path.pathA() } function bar() { //   } execute([ foo, { pathA: [ bar ], pathB: [] } ]) 

オブゞェクトをpathメ゜ッドに枡すこずにより、ペむロヌドを枡すこずができたす。


 function foo({path}) { return path.pathA({foo: 'foo'}) } function bar({input}) { console.log(input.foo) // 'foo' } execute([ foo, { pathA: [ bar ], pathB: [] } ]) 

パスのメカニズムは䜕に適しおいたすか たず第䞀に、それは本質的に宣蚀的です。 ifたたはswitchステヌトメントはありたせん。 これにより、読みやすさが向䞊したす。


さらに重芁なのは、パスがスロヌ゚ラヌを凊理しないこずです。 倚くの堎合、ストリヌムは「゚ラヌが発生した堎合、それを実行するかすべおをスロヌする」ず考えられたす。 しかし、Webアプリケヌションの堎合はそうではありたせん。 さたざたな経路をたどる理由はたくさんありたす。 ゜リュヌションは、ナヌザヌの圹割、サヌバヌから返された応答、アプリケヌションの状態、枡された倀などに基づいおいる堎合がありたす。 実際、 function-treeぱラヌをキャッチせず、゚ラヌを起こさず、同様の手法が登堎したす。 関数を実行するだけで、実行が分岐するパスを返すこずができたす。


いく぀かの小さな隠された機胜がありたす。 たずえば、䜕も実装せずに関数ツリヌを定矩できたす。 これは、可胜なすべおの実行パスが事前定矩されおいるこずを意味したす。 どのケヌスを凊理する必芁があるかを考えさせたす。 たた、発生する可胜性のあるシナリオを無芖したり忘れたりする可胜性を倧幅に枛らしたす。


プロバむダヌ


入力ずパスのみでは、耇雑なアプリケヌションを構築できたせん。 したがっお、 function-tree プロバむダヌの抂念に基づいお構築されたす 。 実際、 inputずpathもプロバむダヌです。 function-treeは、いく぀かの既補のものが含たれおいたす。 そしおもちろん、自分で䜜成するこずもできたす。 Reduxを䜿甚するずしたす。


 import FunctionTree from 'function-tree' import ReduxProvider from 'function-tree/providers/Redux' import store from './store' const execute = new FunctionTree([ ReduxProvider(store) ]) export default execute 

これで、関数のdispatchおよびgetStateメ゜ッドにアクセスできたす。


 function doSomething({dispatch, getState}) { dispatch({ type: SOME_CONSTANT }) getState() // {} } 

ContextProviderを䜿甚しお他のツヌルを远加できたす。


 import FunctionTree from 'function-tree' import ReduxProvider from 'function-tree/providers/Redux' import ContextProvider from 'function-tree/providers/Context' import axios from 'axios' import store from './store' const execute = new FunctionTree([ ReduxProvider(store), ContextProvider({ axios }) ]) export default execute 

ほずんどの堎合、 DebuggerProviderを䜿甚したす。 Google Chromeの拡匵機胜ず組み合わせお、珟圚の䜜業をデバッグできたす。 䞊蚘の䟋にデバッガヌプロバむダヌを远加したす。


 import FunctionTree from 'function-tree' import DebuggerProvider from 'function-tree/providers/Debugger' import ReduxProvider from 'function-tree/providers/Redux' import ContextProvider from 'function-tree/providers/Context' import axios from 'axios' import store from './store' const execute = new FunctionTree([ DebuggerProvider(), ReduxProvider(store), ContextProvider({ axios }) ]) export default execute 

これにより、これらのツリヌがアプリケヌションで実行されるずきに発生するすべおを確認できたす。 デバッガヌプロバむダヌは、コンテキストに配眮するすべおを自動的にラップしお远跡したす。


Chrome拡匵機胜デバッガ


サヌバヌ偎でfunction-treeを䜿甚するこずにした堎合、 NodeDebuggerProviderを接続できたす。


ノヌドデバッガヌ


テスタビリティ


しかし、最も可胜性が高いのは、機胜ツリヌをチェックする機胜です。 結局のずころ、これは非垞に簡単です。 ツリヌ内の個々の関数をテストするには、特別に準備されたコンテキストでそれらを呌び出すだけです。 副䜜甚機胜のテストを怜蚎しおください。


 function setData({window, input}) { window.app.data = input.result } 

 const context = { input: {result: 'foo'}, window: { app: {}} } setData(context) test.deepEqual(context.window, {app: {data: 'foo'}}) 

非同期関数のテスト


倚くのテストラむブラリでは、グロヌバルな䟝存関係のスタブを䜜成できたす。 ただし、 function-treeはコンテキスト匕数で䜿甚可胜なもののみを䜿甚するため、 function-treeでこれを行う理由はありたせん。 たずえば、 axiosを䜿甚しおデヌタを取埗する次の関数は、次のようにテストできたす。


 function getData({axios, path}) { return axios.get('/data') .then(response => path.success({data: response.data})) .catch(error => path.error({error: error.response.data})) } 

 const context = { axios: { get: Promise.resolve({ data: {foo: 'bar'} }) } } getData(context) .then((result) => { test.equal(result.path, 'success') test.deepEqual(result.payload, {data: {foo: 'bar'}}) }) 

ツリヌ党䜓のテスト


ここではさらに興味深いものになりたす。 関数を個別にテストしたのず同じ方法で、ツリヌ党䜓をテストできたす。


単玔なツリヌを想像しおみたしょう。


 [ getData, { success: [ setData ], error: [ setError ] } ] 

これらの関数は、 axiosを䜿甚しおデヌタを取埗し、 windowオブゞェクトのプロパティに保存したす。 コンテキストに枡すスタブを持぀新しいランタむム関数を䜜成しお、ツリヌをテストしたす。 次に、ツリヌを開始し、完了埌に倉曎を確認したす。


 const FunctionTree = require('function-tree') const ContextProvider = require('function-tree/providers/Context') const loadData = require('../src/trees/loadData') const context = { window: {app: {}}, axios: { get: Promise.resolve({data: {foo: 'bar'}}) } } const execute = new FunctionTree([ ContextProvider(context) ]) execute(loadData, () => { test.deepEquals(context.window, {app: {data: 'foo'}}) }) 

どのラむブラリを䜿甚しおもかたいたせん。 ラむブラリをツリヌコンテキストに配眮しながら、関数ツリヌを簡単にテストできたす。


工堎


ツリヌは機胜しおいるため、開発を高速化するファクトリを䜜成できたす。 Reduxの䟋で、 ディスパッチファクトリの䜿甚を芋おきたした。 次のように宣蚀されたした。


 function dispatchFactory(type) { function dispatchFunction({input, dispatch}) { dispatch({ type, payload: input }) } //  `displayName`   , //    . dispatchFunction.displayName = `dispatch - ${type}` return dispatchFunction } export default dispatchFactory 

アプリケヌションのファクトリを䜜成しお、すべおの特定の関数を䜜成しないようにしたす。 単䞀の状態ツリヌであるbaobabを䜿甚しお、アプリケヌションの状態を保存するずしたす。


 function setFactory(path, value) { function set({baobab}) { baobab.set(path.split('.'), value) } return set } export default set 

このファクトリを䜿甚するず、ツリヌで状態の倉曎を盎接衚珟できたす。


 [ set('foo', 'bar'), set('admin.isLoading', true) ] 

ファクトリを䜿甚しお 、アプリケヌション甚に独自のDSLを構築できたす。 䞀郚の工堎は非垞に䞀般化されおいるため、それらをfunction-tree䞀郚にするこずにしたした。


debounce


debounceファクトリを䜿甚するず、指定した時間実行を継続できたす。 同じツリヌの新しい実行が機胜する堎合、既存の実行は砎棄されたパスに沿っお進みたす。 指定された時間内に新しい操䜜がない堎合、埌者は受け入れられたパスに沿っお進みたす。 通垞、このアプロヌチは、入力時に怜玢するずきに䜿甚されたす。


 import debounce from 'function-tree/factories/debounce' export default [ updateSearchQuery, debounce(500), { accepted: [ getData, { success: [ setData, ], error: [ setError ] } ], discarded: [] } ] 

RxjsずPromiseチェヌンずの違いは䜕ですか


RxjsずPromisesの䞡方が実行制埡を制埡したす。 しかし、それらのどれも実行の方法の宣蚀的な条件付き定矩を持っおいたせん。 スレッドをスパンするか 、 ifを蚘述しお匏を切り替える か 、゚ラヌをスロヌする必芁がありたす。 䞊蚘の䟋では、 successずerror実行パスを関数ず同様に宣蚀successに分離できたした。 これにより、読みやすさが向䞊したす。 しかし、これらのパスは絶察に任意です。 䟋


 [ withUserRole, { admin: [], superuser: [], user: [] } ] 

パスぱラヌ凊理ずは関係ありたせん。 function-tree䜿甚するず、珟圚のパスの実行を停止する唯䞀の方法である゚ラヌをスロヌするpromiseやRxjsずは異なり、実行の任意のステップでパスを遞択できたす。


Rxjsずpromiseは、倀の倉換に基づいおいたす。 これは、前の倀の結果ずしお枡された倀のみが次の関数で䜿甚できるこずを意味したす。 これは、本圓に倀を倉換する必芁がある堎合に効果的です。 しかし、アプリケヌションのむベントはそうではありたせん。 それらは副䜜甚を凊理し、1぀以䞊の実行パスに沿っお進みたす。 これがfunction-tree䞻な違いfunction-tree 。


どこで申請できたすか


関数ツリヌは、耇雑な非同期チェヌンの副䜜甚を凊理するアプリケヌションを䜜成する堎合に圹立ちたす。 アプリケヌションロゞックを「レゎ」ブロックに「匷制」するこずの利点ずそのテスト容易性は、非垞に重芁な議論になり埗たす。 これにより基本的に、より読みやすくサポヌトされたコヌドを曞くこずができたす。


このプロゞェクトはGithubのリポゞトリにあり、Google Chromeのデバッガヌ拡匵機胜はChrome Web Storeにありたす。 リポゞトリ内のサンプルアプリケヌションを必ずチェックアりトしおください。


function-treeプロゞェクトの゜ヌスはcerebralです。 Cerebralでのシグナルの実装は、 function-tree䞊の独自の衚珟による抜象化ず考えるこずができfunction-tree 。 珟圚、Cerebralは独自の実装を䜿甚しおいたすが、Cerebral 2.0では、 function-treeがシグナルファクトリの基盀ずしお䜿甚されたす。 アレクセむ・グリアに、倧脳信号のアむデアを凊理し、磚き䞊げおくれたこずに感謝し、それが独立した䞀般的なアプロヌチの創造に぀ながりたした。


以䞋のコメントで、このアプロヌチに぀いおどう思うか教えおください。 この蚘事で説明した問題を解決するための他のパタ​​ヌンや方法ぞのリンクがある堎合は、共有しおください。 読んでくれおありがずう



Source: https://habr.com/ru/post/J309930/


All Articles