为了账号安全,请及时绑定邮箱和手机立即绑定

如何构建一系列方法运行和查找循环(​​碰撞)

如何构建一系列方法运行和查找循环(​​碰撞)

幕布斯7119047 2021-09-17 10:31:50
我所拥有的:有一些 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 methodsmethodA //chain i need right ordermethodBmethodZ第二种情况[     .....  {    "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]»  },]因为一个方法的输出参数可以是另一个方法的输入参数(也可以通过一系列无限嵌套的方法),所以有必要捕获这种冲突,最好是在解析配置的阶段。有人可以建议解决此类问题的最佳解决方案吗,到目前为止只能想到图形。对不起我的英语不好。
查看完整描述

2 回答

?
杨魅力

TA贡献1811条经验 获得超6个赞

我喜欢的一个答案

我开始尝试使用您正在寻找的 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 简单地将一个字符串包装在一个错误中并抛出它

  • 一个不那么琐碎的辅助函数:

  • 四个主要功能:

    • preprocess将我们的描述对象转换为一个配置对象,该对象看起来类似于问题中描述的结构(带有nameinArgs属性,但没有属性returnArgs。)

    • makeGraphconverts 将配置对象转换为邻接图(具有name字符串和字符串数组的对象predecessors数组。)

    • sortGraph对邻接图执行拓扑排序。它是从我在https://stackoverflow.com/a/54408852/ 上写的一个借来的,但如果图形是循环的,则可以通过抛出错误的能力得到增强。

    • process接受配置对象和排序图并生成一元函数。该函数接受一个上下文对象并将这些函数应用到该对象的属性上,将一个新值添加到以函数名称为键的对象中。这将调用makeGraph然后sortGraph在结果上。

  • 最后,一个小的包装函数:

    • runFunctions接受一个描述对象,调用preprocess它来创建配置对象,将它传递给process并返回结果函数。

我确信有一种合理的重构可以消除对中间配置对象和/或结合了图形的创建和排序的需求。这留给读者作为练习!

完整示例

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

)

与请求设计的差异

问题中的 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 时,这通常是权衡。但个别功能不那么复杂。


执行

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

// 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}`,

  },

  /* ... */

}

该版本有一些话要说:它避免了解析函数以获取参数的需要。如何动态获取函数参数名称/值的各种脆弱答案?证明这是一个不平凡的问题。Angular 的依赖注入的用户应该对它非常熟悉。


但最终,这太干净了:


const config = {

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

  /* ... */

}

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


结论

这是一个不平凡的问题。


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


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


查看完整回答
反对 回复 2021-09-17
?
冉冉说

TA贡献1877条经验 获得超1个赞

一个更简单但并非万无一失(您无法检测循环)的解决方案是将每个值包装到一个 Promise 中:当一个函数生成某些输出时,解析 Promise,然后Promise.all在输入上使用。这样,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]);

}


查看完整回答
反对 回复 2021-09-17
  • 2 回答
  • 0 关注
  • 130 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信