2 回答

TA貢獻(xiàn)1811條經(jīng)驗(yàn) 獲得超6個(gè)贊
我喜歡的一個(gè)答案
我開(kāi)始嘗試使用您正在尋找的 API 來(lái)解決這個(gè)問(wèn)題。我確實(shí)設(shè)法得到了一些相當(dāng)接近的東西。但這不是我個(gè)人會(huì)使用的東西。我重寫(xiě)了 API 并多次重構(gòu)了實(shí)現(xiàn),直到我想出一些我想使用的東西。下面我將討論更多我的早期步驟(這可能與您更相關(guān)),但這是我將如何使用我的版本:
const def = {
url: (server, path, query, fragment) => `${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`,
query: (parameters) => parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : '',
server: (schema, port, host) => `${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`,
host: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
}
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'path/to/resource',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
runFunctions (def) (vals)
這將生成如下輸出:
{
schema: "https",
port: "80",
domain: "example.com",
subdomain: "test",
path: "path/to/resource",
parameters: {foo: 42, bar: "abc"},
fragment: "baz",
query: "?foo=42&bar=abc",
host: "test.example.com",
server: "https://test.example.com",
url: "https://test.example.com/path/to/resource?foo=42&bar=abc#baz"
}
API設(shè)計(jì)
我在這個(gè)版本中看到的主要優(yōu)點(diǎn)是 API 感覺(jué)很干凈。配置對(duì)象只是將名稱映射到函數(shù),而提供給結(jié)果函數(shù)的數(shù)據(jù)對(duì)象只是將名稱映射到這些函數(shù)所需的初始參數(shù)。結(jié)果是該數(shù)據(jù)對(duì)象的增強(qiáng)版本。初始調(diào)用返回一個(gè)可重用的函數(shù)。這一切都非常簡(jiǎn)單。
執(zhí)行
我寫(xiě)這篇文章的一些歷史已經(jīng)嵌入到設(shè)計(jì)中。它可能可以使用良好的重構(gòu);一些輔助函數(shù)可能不是必需的。但目前它包括:
四個(gè)簡(jiǎn)單的輔助函數(shù):
isEmpty
報(bào)告數(shù)組是否為空removeIndex
就像一個(gè)不可變的splice
,返回一個(gè)沒(méi)有第n
th 個(gè)索引的數(shù)組的副本props
將屬性名稱數(shù)組映射到給定對(duì)象中的值error
簡(jiǎn)單地將一個(gè)字符串包裝在一個(gè)錯(cuò)誤中并拋出它一個(gè)不那么瑣碎的輔助函數(shù):
parseArgs
從函數(shù)中檢索參數(shù)名稱。它基于https://stackoverflow.com/a/9924463。(奇怪的是,我嘗試的第一個(gè)https://stackoverflow.com/a/31194949在我的測(cè)試 REPL 中運(yùn)行良好,但在 StackOverflow 片段中失敗了。)四個(gè)主要功能:
preprocess
將我們的描述對(duì)象轉(zhuǎn)換為一個(gè)配置對(duì)象,該對(duì)象看起來(lái)類似于問(wèn)題中描述的結(jié)構(gòu)(帶有name
和inArgs
屬性,但沒(méi)有屬性returnArgs
。)makeGraph
converts 將配置對(duì)象轉(zhuǎn)換為鄰接圖(具有name
字符串和字符串?dāng)?shù)組的對(duì)象predecessors
數(shù)組。)sortGraph
對(duì)鄰接圖執(zhí)行拓?fù)渑判?。它是從我?a >https://stackoverflow.com/a/54408852/ 上寫(xiě)的一個(gè)借來(lái)的,但如果圖形是循環(huán)的,則可以通過(guò)拋出錯(cuò)誤的能力得到增強(qiáng)。process
接受配置對(duì)象和排序圖并生成一元函數(shù)。該函數(shù)接受一個(gè)上下文對(duì)象并將這些函數(shù)應(yīng)用到該對(duì)象的屬性上,將一個(gè)新值添加到以函數(shù)名稱為鍵的對(duì)象中。這將調(diào)用makeGraph
然后sortGraph
在結(jié)果上。最后,一個(gè)小的包裝函數(shù):
runFunctions
接受一個(gè)描述對(duì)象,調(diào)用preprocess
它來(lái)創(chuàng)建配置對(duì)象,將它傳遞給process
并返回結(jié)果函數(shù)。
我確信有一種合理的重構(gòu)可以消除對(duì)中間配置對(duì)象和/或結(jié)合了圖形的創(chuàng)建和排序的需求。這留給讀者作為練習(xí)!
完整示例
// helpers
const isEmpty = arr =>
arr .length == 0
const removeIndex = (n, arr) =>
arr .slice (0, n) .concat (arr .slice (n + 1) )
const props = (names) => (obj) =>
names .map (name => obj [name] )
const error = (msg) => {
throw new Error (msg)
}
// retrieves parameter named from a function (https://stackoverflow.com/a/9924463)
const parseArgs = (func) => {
var fnStr = func.toString().replace( /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '');
var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g);
if(result === null)
result = [];
return result;
}
// chooses an appropriate order for our digraph, throwing error on circular
const sortGraph = (
graph,
sorted = [],
idx = graph .findIndex (node => isEmpty (node.predecessors) ),
nodeName = (graph [idx] || {}) .name
) => isEmpty (graph)
? sorted
: idx < 0
? error ('function definitions contains cycle')
: sortGraph (
removeIndex (idx, graph) .map (({name, predecessors}) => ({
name,
predecessors: predecessors .filter (n => n !== nodeName)
}), graph),
sorted .concat (nodeName)
)
// turns a config into an adjacensy graph
const makeGraph = config =>
Object .entries (config) .map (([name, {inArgs}]) => ({
name,
predecessors: inArgs .filter (name => name in config)
}) )
// turns a config object into a function that will run its
// functions in an appropriate order
const process = (config, order = sortGraph (makeGraph (config) )) =>
(vals) =>
order .reduce
( (obj, name) => ({
...obj,
[name]: config [name] .fn .apply (obj, props (config [name] .inArgs) (obj) )
})
, vals
)
// converts simpler configuration into complete version
const preprocess = (def) =>
Object .entries (def) .reduce
( (obj, [name, fn]) => ( { ...obj, [name]: {fn, inArgs: parseArgs(fn)} })
, {}
)
// main function
const runFunctions = (def) =>
process (preprocess (def) )
// input definition
const def = {
url: (server, path, query, fragment) => `${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`,
query: (parameters) => parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : '',
server: (schema, port, host) => `${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`,
host: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
}
// initial input object
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'path/to/resource',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
console .log (
runFunctions (def) (vals)
)
與請(qǐng)求設(shè)計(jì)的差異
問(wèn)題中的 API 不同:配置對(duì)象看起來(lái)更像:
[{
name: 'makeUrl',
inArgs: '[domain, subdomain]',
returnArgs: '[host]',
}, /* ... */]
即使經(jīng)過(guò)一些清理,也會(huì)看起來(lái)像這樣:
[{
name: 'makeHost',
inArgs: ['domain', 'subdomain'],
returnArgs: ['host'],
}, /* ... */]
這比我的解決方案更靈活,因?yàn)樗试S從單個(gè)函數(shù)返回多個(gè)返回值,并封裝在一個(gè)數(shù)組中。但是如果在實(shí)現(xiàn)中沒(méi)有一些不舒服的體操,它也需要每個(gè)函數(shù)的多次返回。此外,它要求無(wú)論您為此提供什么函數(shù),您都必須將函數(shù)與名稱分開(kāi)匹配,您必須確保參數(shù)名稱和順序與inArgs參數(shù)完全匹配,并且您必須包裝更常見(jiàn)的標(biāo)量以數(shù)組形式返回。這可能看起來(lái)像這樣:
const fns = {
makeHost: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
/* ... */
}
我的初步方法
在我看來(lái),添加第二個(gè)配置參數(shù)并使它們保持同步會(huì)使 API 更不符合人體工程學(xué)。但它可以做到,這就是我第一次解決這個(gè)問(wèn)題的方式。
這個(gè)版本需要更少的輔助函數(shù)。不需要preprocess或parseArgs。 props添加只是為了簡(jiǎn)化上面的重構(gòu)版本。我還沒(méi)有檢查它是否對(duì)這個(gè)有幫助。
請(qǐng)注意,process這里要復(fù)雜得多,而且makeGraph稍微復(fù)雜一些。那是因?yàn)樘幚矶鄠€(gè)返回參數(shù)會(huì)增加一些工作??偟膩?lái)說(shuō),這個(gè)版本比上面的版本短了幾行。當(dāng)您創(chuàng)建更舒適的 API 時(shí),這通常是權(quán)衡。但個(gè)別功能不那么復(fù)雜。
執(zhí)行
您可以展開(kāi)此代碼段以查看完整示例:
// helpers
const isEmpty = arr =>
arr .length == 0
const removeIndex = (n, arr) =>
arr .slice (0, n) .concat (arr .slice (n + 1))
const error = (msg) => {
throw new Error (msg)
}
// chooses an appropriate order for our digraph, throwing error on circular
const sortGraph = (
graph,
sorted = [],
idx = graph .findIndex (node => isEmpty (node.predecessors) ),
nodeName = (graph [idx] || {}) .name
) => isEmpty (graph)
? sorted
: idx < 0
? error ('contains cycle')
: sortGraph (
removeIndex (idx, graph) .map (({name, predecessors}) => ({
name,
predecessors: predecessors .filter (n => n !== nodeName)
}), graph),
sorted .concat (nodeName)
)
// turns a config into an adjacensy graph
const makeGraph = config =>
config .map (({name, inArgs}) => ({
name,
predecessors: inArgs .flatMap (
input => config
.filter ( ({returnArgs}) => returnArgs .includes (input) )
.map ( ({name}) => name )
)
}) )
// main function
const process = (config) => (fns, order = sortGraph (makeGraph (config) )) =>
(vals) =>
order .reduce
( (obj, name) => {
const {inArgs, returnArgs} = config .find
( node => node .name == name
)
const args = inArgs .map (key => obj [key])
const res = fns [name] .apply (obj, args)
return returnArgs .reduce
( (o, k, i) => ({...o, [k]: res [i]})
, obj
)
}
, vals
)
const config = [
{name: 'host', inArgs: ['domain', 'subdomain'], returnArgs: ['host']},
{name: 'server', inArgs: ['schema', 'port', 'host'], returnArgs: ['server']},
{name: 'query', inArgs: ['parameters'], returnArgs: ['query']},
{name: 'url', inArgs: ['server', 'path', 'query', 'fragment'], returnArgs: ['url']}
]
const fns = {
host: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
server: (schema, port, host) =>
[`${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`],
query: (parameters) => [parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : ''],
url: (server, path, query, fragment) => [`${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`]
}
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'my/path',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
console .log (
process (config) (fns) (vals)
)
中間工作
我什至不會(huì)嘗試顯示我的代碼在初始版本和最終版本之間經(jīng)歷的所有階段,但是 API 中有一個(gè)有趣的路標(biāo),我在其中使用了這樣的配置對(duì)象:
const config = {
host: {
inArgs: ['domain', 'subdomain'],
fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
},
/* ... */
}
該版本有一些話要說(shuō):它避免了解析函數(shù)以獲取參數(shù)的需要。如何動(dòng)態(tài)獲取函數(shù)參數(shù)名稱/值的各種脆弱答案?證明這是一個(gè)不平凡的問(wèn)題。Angular 的依賴注入的用戶應(yīng)該對(duì)它非常熟悉。
但最終,這太干凈了:
const config = {
host: fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
/* ... */
}
因此我更喜歡我的最終版本。
結(jié)論
這是一個(gè)不平凡的問(wèn)題。
在這些版本中的任何一個(gè)中,實(shí)現(xiàn)都不是特別困難。但是將其分解為有用的部分是具有挑戰(zhàn)性的。當(dāng)我們可以靈活地選擇任何看起來(lái)正確的東西時(shí),確定一個(gè)有用的 API 可能需要大量的思考、大量的討論和大量的嘗試。
不同的開(kāi)發(fā)人員會(huì)做出不同的選擇,通常是出于重要的原因,但對(duì)我來(lái)說(shuō),犧牲可能罕見(jiàn)的設(shè)施來(lái)從單個(gè)函數(shù)中獲得多個(gè)回報(bào)是完全值得的,以實(shí)現(xiàn)一個(gè)更簡(jiǎn)單的配置對(duì)象。事實(shí)上,很難想象一個(gè)更簡(jiǎn)單的配置。

TA貢獻(xiàn)1877條經(jīng)驗(yàn) 獲得超1個(gè)贊
一個(gè)更簡(jiǎn)單但并非萬(wàn)無(wú)一失(您無(wú)法檢測(cè)循環(huán))的解決方案是將每個(gè)值包裝到一個(gè) Promise 中:當(dāng)一個(gè)函數(shù)生成某些輸出時(shí),解析 Promise,然后Promise.all在輸入上使用。這樣,promise 將自動(dòng)確定正確的順序:
const context = { /* [var: string]: { resolve(v: T), value: Promise<T> */ };
function getVar(name) {
if(context[name]) return context[name].value;
const cont = context[name] = { };
return cont.value = new Promise(res => cont.resolve = res);
}
function setVar(name, value) {
getVar(name); // Make sure prop is initialized, don't await!
context[name].resolve(value);
}
async function run(fn, argNames, resultNames) {
const args = await Promise.all(argNames.map(getVar));
const results = fn(...args);
for(let i = 0; i < results.length; i++)
setVar(resultNames[i], results[i]);
}
添加回答
舉報(bào)