Хотя оба способа, которые вы представили, могут быть жизнеспособными, дело в том, что в какой-то момент пользователь должен как-то сказать: «Хорошо, это мои изменения, примите их все или оставьте». Это именно то, что делает IMO.
И это само по себе делает необходимым какой-то вызов, который должен присутствовать в API.
В первом подходе, который вы представили, он явно явный и выполняется методом commit
.
Во втором подходе он довольно неявный и определяется содержимым списка, который вы передаете методу writeOrDelete
.
Так что в моем понимании коммит должен как-то существовать, но вопрос в том, как сделать его менее "раздражающим":)
Вот несколько хитростей:
Трюк 1: Строитель / DSL
interface MyBuilder {
MyBuilder addResourceType1(id);
MyBuilder addResourceType2(id);
MyBuilder deleteResourceType1/2...();
BatchRequest build();
}
interface MyClient {
BatchExecutionResult executeBatchRequest(BatchRequest req);
}
Этот метод более или менее похож на второй метод, однако он имеет четкий способ «добавления ресурсов». Единственная точка создания (почти как MyClient
нет, просто я верю, что со временем у нее будет больше методов, так что, возможно, это хорошая идея для разделения. Как вы сказали: «Мне неудобно иметь 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 / любой другой контейнер, который управляет вашим кодом, вы можете интегрировать его с пружиной (технический способ сделать это выходит за рамки этого вопроса, но вы поняли идею).
Если нет, вы можете предоставить наиболее простой способ:
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, может не быть «явного» метода фиксации, доступного для конечного пользователя. Вместо этого он может иметь некоторый метод "package private", который будет использоваться из MyClientCommitableBlock
class