杨魅力
我喜欢的一个答案我开始尝试使用您正在寻找的 API 来解决这个问题。我确实设法得到了一些相当接近的东西。但这不是我个人会使用的东西。我重写了 API 并多次重构了实现,直到我想出一些我想使用的东西。下面我将讨论更多我的早期步骤(这可能与您更相关),但这是我将如何使用我的版本: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设计我在这个版本中看到的主要优点是 API 感觉很干净。配置对象只是将名称映射到函数,而提供给结果函数的数据对象只是将名称映射到这些函数所需的初始参数。结果是该数据对象的增强版本。初始调用返回一个可重用的函数。这一切都非常简单。执行我写这篇文章的一些历史已经嵌入到设计中。它可能可以使用良好的重构;一些辅助函数可能不是必需的。但目前它包括:四个简单的辅助函数:isEmpty 报告数组是否为空removeIndex就像一个不可变的splice,返回一个没有第nth 个索引的数组的副本props 将属性名称数组映射到给定对象中的值error 简单地将一个字符串包装在一个错误中并抛出它一个不那么琐碎的辅助函数:parseArgs从函数中检索参数名称。它基于https://stackoverflow.com/a/9924463。(奇怪的是,我尝试的第一个https://stackoverflow.com/a/31194949在我的测试 REPL 中运行良好,但在 StackOverflow 片段中失败了。)四个主要功能:preprocess将我们的描述对象转换为一个配置对象,该对象看起来类似于问题中描述的结构(带有name和inArgs属性,但没有属性returnArgs。)makeGraphconverts 将配置对象转换为邻接图(具有name字符串和字符串数组的对象predecessors数组。)sortGraph对邻接图执行拓扑排序。它是从我在https://stackoverflow.com/a/54408852/ 上写的一个借来的,但如果图形是循环的,则可以通过抛出错误的能力得到增强。process接受配置对象和排序图并生成一元函数。该函数接受一个上下文对象并将这些函数应用到该对象的属性上,将一个新值添加到以函数名称为键的对象中。这将调用makeGraph然后sortGraph在结果上。最后,一个小的包装函数:runFunctions接受一个描述对象,调用preprocess它来创建配置对象,将它传递给process并返回结果函数。我确信有一种合理的重构可以消除对中间配置对象和/或结合了图形的创建和排序的需求。这留给读者作为练习!完整示例// helpersconst isEmpty = arr => arr .length == 0const 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 circularconst 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 graphconst 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 orderconst 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 versionconst preprocess = (def) => Object .entries (def) .reduce ( (obj, [name, fn]) => ( { ...obj, [name]: {fn, inArgs: parseArgs(fn)} }) , {} )// main functionconst runFunctions = (def) => process (preprocess (def) )// input definitionconst 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 objectconst 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))与请求设计的差异问题中的 API 不同:配置对象看起来更像:[{ name: 'makeUrl', inArgs: '[domain, subdomain]', returnArgs: '[host]',}, /* ... */]即使经过一些清理,也会看起来像这样:[{ name: 'makeHost', inArgs: ['domain', 'subdomain'], returnArgs: ['host'],}, /* ... */]这比我的解决方案更灵活,因为它允许从单个函数返回多个返回值,并封装在一个数组中。但是如果在实现中没有一些不舒服的体操,它也需要每个函数的多次返回。此外,它要求无论您为此提供什么函数,您都必须将函数与名称分开匹配,您必须确保参数名称和顺序与inArgs参数完全匹配,并且您必须包装更常见的标量以数组形式返回。这可能看起来像这样:const fns = { makeHost: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`], /* ... */}我的初步方法在我看来,添加第二个配置参数并使它们保持同步会使 API 更不符合人体工程学。但它可以做到,这就是我第一次解决这个问题的方式。这个版本需要更少的辅助函数。不需要preprocess或parseArgs。 props添加只是为了简化上面的重构版本。我还没有检查它是否对这个有帮助。请注意,process这里要复杂得多,而且makeGraph稍微复杂一些。那是因为处理多个返回参数会增加一些工作。总的来说,这个版本比上面的版本短了几行。当您创建更舒适的 API 时,这通常是权衡。但个别功能不那么复杂。执行您可以展开此代码段以查看完整示例:// helpersconst isEmpty = arr => arr .length == 0const 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 circularconst 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 graphconst makeGraph = config => config .map (({name, inArgs}) => ({ name, predecessors: inArgs .flatMap ( input => config .filter ( ({returnArgs}) => returnArgs .includes (input) ) .map ( ({name}) => name ) ) }) )// main functionconst 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))中间工作我什至不会尝试显示我的代码在初始版本和最终版本之间经历的所有阶段,但是 API 中有一个有趣的路标,我在其中使用了这样的配置对象:const config = { host: { inArgs: ['domain', 'subdomain'], fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`, }, /* ... */}该版本有一些话要说:它避免了解析函数以获取参数的需要。如何动态获取函数参数名称/值的各种脆弱答案?证明这是一个不平凡的问题。Angular 的依赖注入的用户应该对它非常熟悉。但最终,这太干净了:const config = { host: fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`, /* ... */}因此我更喜欢我的最终版本。结论这是一个不平凡的问题。在这些版本中的任何一个中,实现都不是特别困难。但是将其分解为有用的部分是具有挑战性的。当我们可以灵活地选择任何看起来正确的东西时,确定一个有用的 API 可能需要大量的思考、大量的讨论和大量的尝试。不同的开发人员会做出不同的选择,通常是出于重要的原因,但对我来说,牺牲可能罕见的设施来从单个函数中获得多个回报是完全值得的,以实现一个更简单的配置对象。事实上,很难想象一个更简单的配置。