如何构建方法链 运行 并查找循环(​​冲突)

How to build a chain of methods run and find cycles (collisions)

我有:有一些 json 配置(描述性模板),方法以不同的顺序存储,它看起来像:

[   
  {
    "name" : "methodA", //methodA output arguments are methodB input arguments
    "inArgs" : "[arg1, arg2]",
    "returnArgs" : "[arg3, arg4]"
  },
  {
    "name" : "methodB", //methodB output arguments are methodZ input arguments
    "inArgs" : "[arg3, arg5]",
    "returnArgs" : "[arg6, arg7]"
  },
{
    "name" : "methodС",
    "inArgs" : "[arg1]",
    "returnArgs" : "[arg10]"
  },
    a bunch of other methods whose input arguments are not part of methodA or methodB
  .....
  {
    "name" : "methodZ",
    "inArgs" : "[arg6, arg11]",
    "returnArgs" : "[arg20]"
  }
]

我需要将这些方法以正确的顺序(链)放入 运行,例如:


methodC //the output of this method is not used as an input argument to other methods

methodA //chain i need right order
methodB
methodZ

第二个案例

[   
  .....
  {
    "name" : "methodX", //methodX output arguments are methodY input arguments
    "inArgs" : «arg1, arg2, arg3]»,
    "returnArgs" : «[arg4, arg5, arg6]»
  },
  {
    "name" : "methodY", //methodY output arguments are methodX input arguments
    "inArgs" : «[arg4, arg5, arg7]»,
    "returnArgs" : «[arg8, arg9, arg10]»
  },
  ....
  {
    "name" : "methodZ", //methodZ output arguments are methodX input arguments( collision or cycle, so throw error )
    "inArgs" : «[arg8, arg11, arg12]»,
    "returnArgs" : «[arg3, arg13, arg14]»
  },
]

因为一个方法的输出参数可以是另一个方法的输入参数(也是通过无限嵌套的方法链),所以有必要捕获这种冲突,最好是在解析配置的阶段。

谁能建议这种问题的最佳解决方案,目前只有图表浮现在脑海中。

对不起我的英语。

一个更简单但不是万无一失(你无法检测循环)的解决方案是将每个值包装到一个 Promise 中:当一个函数生成某些输出时,解决 Promise,然后使用 Promise.all on输入。这样,promise 将自动确定正确的顺序:

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]);
}

(抱歉,这个回答很长,希望对你有用。)

我喜欢的一个答案

我开始尝试使用您正在寻找的 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 感觉很干净。配置对象只是将名称映射到函数,而提供给结果函数的数据对象只是将名称映射到这些函数所需的初始参数。结果是该数据对象的增强版本。初始调用 return 是一个可重用的函数。一切都很简单。

实施

我写这篇文章的一些历史已经嵌入到设计中。它可能需要进行良好的重构;几个辅助函数可能不是必需的。但目前它包括:

  • 四个简单的辅助函数:

    • isEmpty报告数组是否为空
    • removeIndex 就像一个不可变的 splice,return 复制一个没有第 n 个索引的数组
    • props 将 属性 名称数组映射到给定对象中的值
    • error 简单地将一个字符串包裹在错误中并抛出它
  • 少一个琐碎的辅助函数:

    • parseArgs 从函数中检索参数名称。它基于 . (Oddly, the first one I tried, ,它在我的测试 REPL 中运行良好,但在 Whosebug 代码片段中失败了。)
  • 四大主要功能:

    • preprocess 将我们的描述对象转换为一个配置对象,看起来类似于问题中描述的结构(具有 nameinArgs 属性,尽管没有 returnArgs一个。)
    • makeGraph converts 将一个配置对象转换成一个邻接图(一个对象数组,一个 name 字符串和一个 predecessors 字符串数组。)
    • sortGraph 对邻接图执行拓扑排序。它是从我在 上写的一个借用的,但增强了如果图形是循环的则抛出错误的能力。
    • process 接受配置对象和排序图并生成一元函数。该函数采用上下文对象并将函数应用到该对象的属性,将新值添加到以函数名称为键的对象。这会调用 makeGraph,然后对结果调用 sortGraph
  • 最后,一个小的包装函数:

    • runFunctions 接受描述对象,对其调用 preprocess 以创建配置对象,将其传递给 process 和 return 生成的函数。

我确信有一个合理的重构可以消除对中间配置对象 and/or 的需求,它结合了图形的创建和排序。这留作 reader!

的练习

完整示例

// 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 (
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)
)

与请求设计的差异

问题中的 API 不同:配置对象看起来更像:

[{
  name: 'makeUrl',
  inArgs: '[domain, subdomain]',
  returnArgs: '[host]',
}, /* ... */]

甚至在一些清理之后,看起来像这样:

[{
  name: 'makeHost',
  inArgs: ['domain', 'subdomain'],
  returnArgs: ['host'],
}, /* ... */]

这比我的解决方案更灵活,因为它允许来自单个函数的多个 returns,包装在一个数组中。但是如果在实现中没有一些不舒服的体操,它也会 require 每个函数的多个 returns。此外,它要求无论您向其提供什么函数,您都必须将函数与名称分开匹配,您必须确保参数名称和顺序与 inArgs 参数完全匹配,并且您将拥有将更常见的标量 return 包装在一个数组中。这可能看起来像这样:

const fns = {
  makeHost: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
  /* ... */
}

我的初步方法

在我看来,添加第二个配置参数并使它们保持同步会大大降低人体工程学 API。但这是可以做到的,这就是我最初解决问题的方式。

此版本需要的辅助函数较少。不需要 preprocessparseArgsprops 只是为了简化上面的重构版本而添加的。我还没有检查它是否对这个有帮助。

请注意,process 在这里要复杂得多,而 makeGraph 稍微复杂一些。这是因为处理多个 return 参数会增加一些工作量。总的来说,这个版本比上面的版本少了几行。当您创建更舒适的 API 时,这通常是权衡取舍。但个别功能没那么复杂。

实施

您可以展开此代码段以查看完整示例:

// 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)
)

中级作业

我什至不会尝试显示我的代码在初始版本和最终版本之间经历的所有阶段,但是 API 中有一个有趣的路径点,我在其中使用了这样的配置对象:

const config = {
  host: {
    inArgs: ['domain', 'subdomain'], 
    fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
  },
  /* ... */
}

该版本有一些值得一提的地方:它避免了为了获取参数而解析函数的需要。 How to get function parameter names/values dynamically? 的各种脆弱答案表明这是一个不平凡的问题。 Angular依赖注入的用户应该很熟悉。

但最后,这太干净了:

const config = {
  host: fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
  /* ... */
}

因此我更喜欢我的最终版本。

结论

这是一个不平凡的问题。

在这些版本中的任何一个中实现都不是特别困难。但将其分解成有用的部分具有挑战性。当我们可以灵活地选择似乎正确的任何东西时,确定一个有用的 API 可能需要大量思考、大量讨论和大量尝试。

不同的开发人员会做出不同的选择,通常出于重要原因,但对我来说,牺牲可能很少见的设施从单个函数中获得多个 returns 是完全值得的,以实现更简单的配置对象。事实上,很难想象更简单的配置。