执行多项操作并提交

时间:2019-06-14 05:36:48

标签: java api-design

系统处理两种类型的资源。有用于管理资源的写入和删除API。客户端(用户)将使用库API来管理这些资源。每次写入(或创建)资源都会导致更新商店或数据库。

API如下所示:

1)创建库客户端。用户将使用返回的客户端来操作资源。

MyClient createClient(); //to create the client

2)MyClient界面。提供对资源的操作

writeResourceType1(id);
deleteResourceType1(id);

writeResourceType2(id);
deleteResourceType2(id);

某些资源依赖于另一资源。用户可能会无序地写入它们(可能在写入依赖项之前先写入资源)。为了防止系统状态不一致,所有更改(资源更新)都将写入到暂存位置。当用户指示他/她已经写完所有内容时,所做的更改将仅写到实际的商店中。

这意味着我需要在上述MyClient界面中使用 commit 这种方法。因此,访问模式看起来像

Client client = provider.createClient();
..
client.writeResourceType1(..)

client.writeResourceType1(..)

client.deleteResourceType2(..)

client.commit(); //<----

我不愿意在MyClient界面中使用提交API。我觉得它在污染它,错误的是错误的抽象级别。

是否有更好的方法来处理?


我想到的另一种选择是作为单个呼叫的一部分获得全部更新。该API将充当批处理API

writeOrDelete(List<Operations> writeAndDeleteOpsForAllResources)

这的缺点是用户必须结合其末端的所有操作才能调用此操作。单个调用中也塞满了太多内容。因此,我不倾向于这种方法。

2 个答案:

答案 0 :(得分:10)

尽管您介绍的两种方式都是可行的选择,但事实是,在某个时间点,用户必须以某种方式说:“好吧,这些是我的更改,全部接受或放弃。”这正是IMO的提交。 而且仅此一项就必须进行某种必须在API中进行的调用。

在第一种方法中,您已经展示了其明显的显式方法,并且是通过commit方法完成的。 在第二种方法中,它相当隐式,并且由您传递到writeOrDelete方法中的列表的内容确定。

因此,据我所知,提交必须以某种方式存在,但问题是如何使它减少“烦人”:)

以下是一些技巧:

技巧1:Builder / DSL

interface MyBuilder {
   MyBuilder addResourceType1(id);
   MyBuilder addResourceType2(id);
   MyBuilder deleteResourceType1/2...();
   BatchRequest build();
}

interface MyClient {
   BatchExecutionResult executeBatchRequest(BatchRequest req);
}

此方法或多或少类似于第二种方法,但是它具有“添加资源”的明确方法。单一的创建点(几乎与MyClient类似,只是我相信最终它将有更多的方法,所以也许将其分离是一个好主意。正如您所说:“我不愿意拥有commit API在MyClient界面中。我感觉它正在污染它,而错误则是错误的抽象级别”) 这种方法的另一个论点是,现在您知道在使用此方法的代码中存在一个生成器及其“抽象”,您无需考虑将引用传递给列表,无需考虑如果有人在此列表中称呼clear()之类的东西,依此类推。构建器具有精确定义的API,可以完成哪些工作。

关于创建构建器:

您可以使用类似Static Utility类的东西,甚至可以向MyClient添加方法:

// option1

public class MyClientDSL {
   private MyClientDSL {}
   public static MyBuilder createBuilder();
}

// option 2
public interface MyClient {
    MyBuilder newBuilder();
}

此方法的参考:JOOQ(它们具有DSL这样的功能),OkHttp,它们具有Http请求的构建器,实体等等(与OkHttpClient本身分离)。

技巧2:提供执行代码块

根据您所运行的环境,现在实施起来可能很棘手, 但基本上从Spring借来了一个主意: 为了在使用数据库时保证事务,他们提供了一个特殊的注释@Transactional,该注释放在方法上时基本上说:“方法中的所有内容都在事务中运行,我将自己提交它,以便用户完全不会处理事务/提交。我还会在出现异常时回滚“

所以在代码中看起来像:

class MyBusinessService {
       private MyClient myClient; // injected
         @Transactional
         public void doSomething() {
             myClient.addResourceType1();
             ...
             myClient.addResourceType2();
             ...  
         }
}

在后台,他们应该维护ThreadLocals才能在多线程环境中实现这一点,但重点是该API很干净。方法commit可能存在,但在大多数情况下可能不会使用,更不用说用户可能真的需要“细粒度”控件的复杂场景了。

如果您使用spring /来管理代码的任何其他容器,则可以将其与spring集成(执行此操作的技术方法不在此问题的范围内,但您可以理解)。

如果没有,您可以提供最简单的方法:

public class MyClientCommitableBlock {

    public static <T> T executeInTransaction(CodeBlock<T> someBlock)
            builder) {

          MyBuilder builder = create...;
          T result = codeBlock(builder);
          // build the request, execute and commit
          return result;
      }
}

这是它的外观:

static import MyClientCommitableBlock.*;
public static void main() {

     Integer result = executeInTransaction(builder -> {
        builder.addResourceType1();
        ... 
        return 42;
     });
}

// or using method reference:

    class Bar {
       Integer foo() {
         return executeInTransaction(this::bar);    
       } 

       private Integer bar(MyBuilder builder) {
         ....
       }
    }

在这种方法中,构建器在仍然精确定义一组API的情况下,可能不会向最终用户公开“显式”提交方法。相反,它可以在MyClientCommitableBlock类中使用某些“打包私有”方法

答案 1 :(得分:0)

尝试一下是否适合您

让我们在登台表中有一个标记为status的列

状态列值

New : Record inserted by user
ReadyForProcessing : Records ready for processing
Completed : Records processed and updated in Actual Store

添加以下方法而不是commit(),并且一旦用户调用此方法/服务,请提取status: New中该用户的记录,并将其从以下位置发布到实际存储中暂存位置

client.userUpdateCompleted();

还有另一种选择,我们可以通过给client.commit(); or client.userUpdateCompleted();来消除客户的干预,相反,我们可以有一个batch process using Scheduler,它以特定的间隔运行,扫描暂存表并填充有意义的用户将完成的记录更新到实际存储中