我应该选择代码重复还是与API服务整合 - JS

4

我正在开发一个大型内容管理系统,其中特定模块及其子模块利用相同的后端API。每个子模块的终端节点除了“文档类型”不同外,其余完全相同。

因此,遵循以下模式:

api/path/v1/{文档类型}

api/path/v1/{文档类型}/{id}

api/path/v1/{文档类型}/{id}/versions

随着时间的推移,使用此API的模块数量增加,我留下了许多冗余的API服务,实现了7种CRUD方法:

getAllXs() {...}
getX(id) {...}
getXVersion(id, versionId) {...}

etc...

使用以下这种个性化的方法:

getAllXs() {
    let endpoint = BASE.URL + ENDPOINTS.X;
    let config = ...
    return http.get(endpoint, config)
        .then(response => response.data);
        .catch(...);

}

X是特定文档类型的名称。

我遇到了这样一个问题,我决定创建一个单一的服务并像这样做:

const BASE_URL = window.config.baseUrl + Const.API_ENDPOINT;
const ENDPOINTS = {
  "W": "/v1/W/",
  "X": "/v1/X/",
  "Y": "/v1/Y/",
  "Z": "/v1/Z/",
}

getAllDocuments(docType, config={}) {
  let endpoint = BASE_URL + ENDPOINTS[docType];
  return http.get(endpoint, config)
        .then(response => response.data);
        .catch(...);
}
...other methods

在指定类型并使用映射的终端点构建路径时。

这将所有文档API服务都缩减为一个。从代码角度来看,现在更加简洁,但显然需要一个额外的参数,并且术语更加通用:

getAllXs() --> getAllDocuments()

虽然不太“白痴证明”,但它稍微有点复杂。我对当前编写方式感到不安的原因是有6个模块使用此API,每个服务中使用相同的7个方法。

我一直在问自己以下问题:

  • 我是否正在使用动态函数边缘反模式?

  • 如果我有10个以上的模块使用相同的API会怎样?

3个回答

6
你的问题让我想到了一个常见的对象关系映射设计问题。
在设计方面没有单一的真相,但如果你在构建中识别出ORM并重视面向对象设计原则,我有一些启发可以给你。
这是我自己使用过的基本ES6 ORM的简化版本(为了易于理解,重用了你的代码片段)。这个设计受到我在其他语言中使用的重型ORM框架的启发。
class ORM {
   constructor() {
      this.BASEURL = window.config.baseUrl + Const.API_ENDPOINT
      this.config = {foo:bar} // default config
   }

   getAll() {
      let endpoint = this.BASEURL + this.ENDPOINT
      return http.get(endpoint, this.config)
       .then(response => response.data)
       .catch(...)
   }

   get(id) {
      // ...
   }
}

以下是该类的扩展示例(注意其中一个具有特殊配置)

class XDocuments extends ORM {
   static endpoint = '/XDocument/'

   constuctor() {
      super()
   }

   otherMethod() {
      return 123
   }
}

class YDocuments extends ORM {
   static endpoint = '/YDocument/'

   constuctor() {
      super()
   }

   getAll() {
      this.config = {foo:not_bar}
      super.getAll()
   }
}

既然你特别问这是否趋近于反模式,我建议阅读SOLIDDRY原则,以及一般的ORM设计。你还会发现有关全局常量的代码异味,但只有在窗口上下文中才是真实的。我看到你已经在正确的道路上,试图避免代码重复异味散弹手术异味。 :-)

祝你好运,并随时在评论中提出更多问题和添加额外细节!


我实际上在没有看你的答案的情况下写了一个非常相似的答案,哈哈!我觉得这是避免被绑定在“过于动态”的解决方案中,而没有为单个修改留出空间的最佳方式。 - tcanusso
呵呵,晚了15分钟!!;-) 我可以理解这会给正在寻找经验和验证的作者更多信心。我包括了反模式、设计和代码气味文章,以增加我们类似方法的合法性。希望能有所帮助。 :-) - Kad
2
请看我在@tcanusso答案下留下的评论。确实,这让我相信我走在了正确的方向上。我对反模式的担忧是使用单个动态API服务而不是可扩展的基础,但我现在意识到那样做是多么脆弱。 - GHOST-34

1

虽然我与x00的答案大部分相同,但我会考虑您的端点是否真正静态。

模块“X”有没有可能更改其任何端点定义?例如,您需要传递一个以上的查询参数。您的模块都是完全相同的,没有任何变化的余地吗?

如果答案是否定的,那么您只能重构整个代码库(如果您按照您提出的方式实施的话)才能进行简单的更改。

如果答案是肯定的,那么我认为您可以实现您提出的动态函数而没有任何问题。就我个人而言,我会倾向于使用我的模块扩展和使用的服务,以防我想要对它们进行最小的更改。例如:

class MyGenericService {
  constructor() {
    this.url = window.config.baseUrl;
  }

  async getAllDocuments(config) {
    return http.get(this.url, config)
      .then(response => response.data);
      .catch(...);
  };

  // ...and so on
}

这使我的代码能够扩展和修改,唯一需要注意的是您需要为每个模块维护一个文件,其中包含类似以下内容的东西:
class XService extends MyGenericService {
  constructor() {
    this.url = window.config.baseUrl + '/v1/x';
  }
}

如果维护这些额外的文件负担太大,你可以在MyGenericService的构造函数中接收端点的URL,然后在你的控制器中只需要像这样做:
const myXService = new MyGenericService('/v1/x');
const myYService = new MyGenericService('/v1/y');
// ...or it could use your endpoint url mapping
// I don't really know how is your code structured, just giving you ideas

这里有一些选项,希望能帮到你!


你有考虑在继承类中使用静态属性吗?如果有的话,为什么不用呢?我发现重复使用“window.config.baseUrl”应该由父类拥有,以避免“Shotgun Surgery”的问题。 - Kad
1
我实际上先编写了一个选项,其中通用服务通过构造函数参数接收端点,然后将其更改为我保留了它。我实际上更喜欢你的方式,因此没有必要重复常量“window.bla”,它不应更改。 - tcanusso
所以在使用了几个API服务之后,我实际上转向了这种模式。正是在创建了这个基类之后,我开始质疑“它是否只能是基类?”这就是为什么我在这里发布的原因。最终,我选择了该模式,但使用ES6模块而不是类。单个动态服务的问题在于,只需要一个特定的要求出现在特定的端点上,它就会崩溃或被迫变得更加动态。冗余感目前有点多余,但我认为以后会感谢自己。 - GHOST-34

1
如果您能提供更多原始代码版本,那将更有助于指出问题。目前我只能说DRY原则通常是个好主意(不总是,但这是一个复杂的话题)。有很多关于DRY的文章,可以通过谷歌搜索。您担心您的代码变得更加复杂。不用担心,在我的经验中,新手程序员之所以失败,正是因为他们放弃了DRY原则。只有过了一段时间,当他们变得更强大时,他们才开始在KISS原则上失败。而且除了已经存在的3个参数外再添加一个额外的参数并不会增加太多复杂性。
“动态”函数是否边缘反模式?
正是因为要“动态”,函数才存在。
如果我有10个以上的模块使用相同的API怎么办?
没错!这是10多次犯错、遗漏、误读等机会,如果您不DRY化您的代码,需要做10多倍的工作才能进行更改。
PS. 如果getAllDocuments是你原始代码中的真实名称,那么它比getAllDocType1s更好。

1
好的...现在我明白了...是的,你的新函数看起来比原来的更复杂。 - x00
我现在有一些问题:1)为什么会有 if (Array.isArray(response.data)) 2)你为什么需要 then(data=>data) 3)顺便问一下,为什么不使用 async/await 4)为什么要使用 toUpperCase 而不是直接使用大写的 ENDPOINTS 5)为什么不将 ENDPOINTS 放在 config 中,如果您使用 ENDPOINTSdocType 映射到 URL,则不对其他配置属性执行相同操作,而是将它们作为参数传递... 嗯...还有更多问题,但我可能应该停止 :) - x00
正如你提到的“反模式”...嗯,没有参数的函数...我会说这是一个代码异味。再次强调-并非总是如此。但在这种情况下是这样的。 - x00
我进行了更多的编辑。我的错,我混合了示例/通用代码和一些实际代码,使其更加混乱。我认为你最好的观点是第5点,这可能解决我的问题。基本上,我可以创建一个配置文件,其中包含所有配置的定义,并将其映射到该文件。 - GHOST-34
让我们在聊天中继续这个讨论 - GHOST-34
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接