Лучший шаблон проектирования для управления асимметричным использованием ресурсов
Я хотел, чтобы на холсте были высказаны мнения о наилучшем шаблоне проектирования для работы с управляемыми ресурсами, в котором задействованы два разных ресурса, но вам нужно освободить их в обратном порядке, чем они были приобретены.
Сначала позвольте мне установить сцену. Мы работаем с двумя типами объектов "Документы" и "Коллекции документов". Сборник документов буквально содержит ссылки на документы и некоторые метаданные для каждого документа.
Первоначально у нас был симметричный рисунок, который протекал как:
- Коллекция блокировок
- Полезный материал с коллекцией
- Заблокировать документ
- Полезный материал с коллекцией и документом
- Разблокировать документ
- Разблокировать коллекцию
и в коде был представлен как:
Collection col = null;
try {
col = getCollection("col1 name", LockMode.WRITE_LOCK);
// Here we do any operations that only require the Collection
Document doc = null;
try {
doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK);
// Here we do some operations on the document (of the Collection)
} finally {
if (doc != null) {
doc.close();
}
}
} finally {
if (col != null) {
col.close();
}
}
Теперь, когда мы имеем try-with-resources
с Java 7, мы улучшили это, чтобы разметка кода Java автоматически освобождала ресурсы:
try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) {
// Here we do any operations that only require the Collection
try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {
// Here we do some operations on the document (of the Collection)
}
}
Проблема заключается в том, что сохранение блокировки коллекции во время выполнения операций над документом неэффективно, так как другие потоки должны ждать, и часто операции над документом не требуют изменения коллекции.
Итак, мы хотели бы перейти к асимметричной схеме, которая позволяет нам как можно скорее выпустить коллекцию. Поток должен быть следующим:
- Коллекция блокировок
- Полезный материал с коллекцией
- Заблокировать документ
- Делайте все, что требует как Collection, так и Document (редко)
- Разблокировать коллекцию
- Полезные материалы с документом
- Разблокировать документ
Мне интересно, как лучше всего реализовать этот асимметричный подход в коде. Это, очевидно, можно сделать с помощью try/finally и т.д. Так:
Collection col = null;
Document doc = null;
try {
col = getCollection("col1 name", LockMode.WRITE_LOCK);
// Here we do any operations that only require the Collection
try {
doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK);
// Here we do any operations that require both the Collection and Document (rare).
} finally {
if (col != null) {
col.close();
}
// Here we do some operations on the document (of the Collection)
} finally {
if (doc != null) {
doc.close();
}
}
}
Я также могу подумать о схеме try-with-resources
, где мы обмениваемся порядком выпуска ресурсов, но мне интересно, не делает ли это чтение кода менее понятным. Например:
try (final ManagedRelease<Collection> mcol =
new ManagedRelease<>(getCollection("col1 name", LockMode.WRITE_LOCK))) {
// Here we do any operations that only require the Collection
try (final ManagedRelease<Document> mdoc =
mcol.withAsymetrical(mcol.resource.getDocument("doc1 name", LockMode.WRITE_LOCK))) {
// Here we do any operations that require both the Collection and Document (rare).
} // NOTE: Collection is released here
// Here we do some operations on the document (of the Collection)
} // NOTE: Document is released here
Класс ManagedRelease
:
private static class ManagedRelease<T extends AutoCloseable> implements AutoCloseable {
final T resource;
private Supplier<Optional<Exception>> closer;
public ManagedRelease(final T resource) {
this.resource = resource;
this.closer = asCloserFn(resource);
}
private ManagedRelease(final T resource, final Supplier<Optional<Exception>> closer) {
this.resource = resource;
this.closer = closer;
}
public <U extends AutoCloseable> ManagedRelease<U> withAsymetrical(final U otherResource) {
// switch the closers of ManagedRelease<T> and ManagedRelease<U>
final ManagedRelease<U> asymManagedResource = new ManagedRelease<>(otherResource, closer);
this.closer = asCloserFn(otherResource);
return asymManagedResource;
}
@Override
public void close() throws Exception {
final Optional<Exception> maybeEx = closer.get();
if(maybeEx.isPresent()) {
throw maybeEx.get();
}
}
private static Supplier<Optional<Exception>> asCloserFn(final AutoCloseable autoCloseable) {
return () -> {
try {
autoCloseable.close();
return Optional.empty();
} catch (final Exception e) {
return Optional.of(e);
}
};
}
}
Я бы приветствовал мнения о том, является ли подход try-with-resources
асимметричным управлением ресурсами разумным или нет, а также любые указатели на другие шаблоны, которые могут быть более подходящими.
Ответы
Ответ 1
Первый вопрос, по-видимому, является недоопределенным ожидаемым поведением. В частности, если Collection.close
выбрасывает Exception
, что должно произойти? Должна ли продолжаться обработка Document
? Следует ли откатить часть обработки документа, сделанного под обоими замками?
Если ответ Collection.close
никогда не выдает никаких исключений (или вам все равно, что произойдет, если это произойдет), IMHO - самое простое решение сделать ваш idmpotent Collection.close
, а затем явно вызвать его в середине блок try-with-resources
, где это уместно. Также имеет смысл заставить "обычные" методы Collection
поднять что-то вроде IllegalStateException
, если вызывается в закрытом Collection
. Тогда второй пример станет примерно таким:
try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) {
// Here we do any operations that only require the Collection
try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {
// Here we do any operations that require both the Collection and Document (rare).
// NOTE: usually Collection is released here
col.close();
// optionally make `col` not final and explicitly set it to `null`
// here so IDE would notify you about any usage after this point
// Here we do some operations on the document (of the Collection)
}
}
Если вы не можете изменить код Collection.close
, вы можете изменить свой ReleaseManager
, чтобы сделать close
idempotent. Возможно, вы также можете переименовать его в нечто вроде ResourceManager
. добавьте получателя туда и всегда получайте доступ к ресурсу только через этот getter. И геттер будет бросать IllegalStateException
, если вызывается после close
.
Если Collection.close
может действительно вызвать какое-то исключение, и вы действительно заботитесь о таких сценариях, трудно предоставить решение, не зная, что такое ожидаемое поведение.
Ответ 2
Я дам вам общее, полное и цельное решение, подобное этому:
public static void sample() {
Resource resourceA = new Resource("A");
Resource resourceB = new Resource("B");
LockVisitor.create(resourceA)
.lock()// lock A
.doOnValue(Main::doSomething)// do for A
.with(resourceB)// join with B
.lock()// lock A & B (A has been locked)
.doOnBoth(Main::doSomething)// do for A and B
.toRight()// only need B (unlock A)
.doOnValue(Main::doSomething)// do for B
.close();// unlock B
}
private static void doSomething(Resource... rs) {
System.out.println("do with: " + Arrays.toString(rs));
}
и sample
выводят, что вы ожидали:
lock: Resource(A)
do with: [Resource(A)]
lock: Resource(B)
do with: [Resource(A), Resource(B)]
unlock: Resource(A)
do with: [Resource(B)]
unlock: Resource(B)
Сначала мы должны определить блокируемый ресурс. Как заблокировать и как разблокировать.
public interface Lockable extends AutoCloseable {
void lock() throws Exception;
void unlock() throws Exception;
boolean isLocked();
@Override
default void close() throws Exception {
unlock();
}
}
Вы можете позволить вашему классу реализовать этот интерфейс для более четкого вызова.
Затем мы можем построить наш LockVisitor
(для уменьшения длины этого ответа я удаляю реализацию метода. Вы можете найти полный код в github.)
import io.reactivex.functions.Consumer;
public class LockVisitor<T extends Lockable> implements AutoCloseable {
public static <T extends Lockable> LockVisitor<T> create(T lockable) {
return new LockVisitor<>(lockable);
}
T value;
Exception error;
public LockVisitor(T value);
public LockVisitor<T> lock();
public LockVisitor<T> unlock();
public LockVisitor<T> doOnValue(Consumer<T> func);
public LockVisitor<T> doOnError(Consumer<Exception> func);
public <B extends Lockable> TwoLockVisitor<T, B> with(LockVisitor<B> other);
public <B extends Lockable> TwoLockVisitor<T, B> with(B other);
}
и наш TwoLockVisitor
для совместного использования двух ресурсов:
import io.reactivex.functions.BiConsumer;
import io.reactivex.functions.Consumer;
public class TwoLockVisitor<A extends Lockable, B extends Lockable> {
public static <A extends Lockable, B extends Lockable> TwoLockVisitor<A, B> create(A a, B b) {
return new TwoLockVisitor<>(LockVisitor.create(a), LockVisitor.create(b));
}
LockVisitor<A> left;
LockVisitor<B> right;
public TwoLockVisitor(LockVisitor<A> left, LockVisitor<B> right);
public TwoLockVisitor<A, B> lock();
public TwoLockVisitor<A, B> unlock();
public TwoLockVisitor<A, B> doOnLeft(Consumer<A> func);
public TwoLockVisitor<A, B> doOnRight(Consumer<B> func);
public TwoLockVisitor<A, B> doOnBoth(BiConsumer<A, B> func);
public LockVisitor<A> toLeft();
public LockVisitor<B> toRight();
}
Теперь вы можете использовать классы для управления вашим ресурсом в любом порядке.
Ответ 3
Ваша схема ManagedRelease
определенно делает код менее понятным. Самая непосредственная явная запись ваших намерений с использованием языковых функций выглядит следующим образом:
try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) {
// Here we do any operations that only require the Collection
}
try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK;
final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {
// Here we do any operations that require both the Collection and Document (rare).
}
try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {
// Here we do some operations on the document (of the Collection)
}
Проблема заключается в дополнительном выпуске и повторном приобретении каждой блокировки, а также что col
выходит за пределы области для последнего вызова getDocument
, поэтому он не будет компилироваться как есть.
Я бы предложил разрешить это с другой точки зрения по концепции ManagedRelease
, поднятой на один уровень. Схема использования, которую я предполагаю для этого, будет работать следующим образом:
// The lambdas here are Supplier
try (final ReleaseManager<Collection> colManager = new ReleaseManager<>(() -> getCollection("col1 name", LockMode.WRITE_LOCK);
final ReleaseManager<Document> docManager = new ReleaseManager<>(() -> colManager.getResource().get().getDocument("doc1 name", LockMode.WRITE_LOCK)) {
try (final Managed<Collection> colManaged = colManager.getResource()) {
// Here we do any operations that only require the Collection
} // Here the resource close does nothing
try (final Managed<Collection> colManaged = colManager.getResourceForLastUse();
final Managed<Document> docManaged = docManager.getResource()) {
// Here we do any operations that require both the Collection and Document (rare).
} // Here the close of colManaged actually closes it, while docManaged.close() is a no-op
try (final Managed<Document> docManaged = docManager.getResourceForLastUse()) {
// Here we do some operations on the document (of the Collection)
} // Here the document gets closed
} // Here the managers get closed, which would close their resources if needed
Это имеет ту же ясность, что ресурсы используются в каждом блоке, использует функцию языка try-with-resources, выпускает каждый ресурс сразу после его последнего использования и только получает каждую блокировку один раз.
Для спецификации ReleaseManager
:
ReleaseManager
вот общий класс, который берет Supplier
для ресурса, лениво вызывает его при первом вызове getResource()
и запоминает результат для будущих вызовов. getResource()
возвращает оболочку, которая ничего не делает при закрытии, getResourceForLastUse()
возвращает оболочку, которая фактически закрывает ресурс, когда оболочка закрыта; Я написал их как один класс, но вместо этого вы могли бы сделать их разными классами, я не уверен, действительно ли он делает что-то более ясное.
ReleaseManager
сам реализует AutoCloseable
, а его реализация close()
является отказоустойчивой, которая закрывает ресурс, если он был получен, но не закрыт. Я хотел бы подумать о том, чтобы он также каким-то образом зарегистрировал предупреждение, чтобы привлечь внимание, если последнее использование ресурса не будет объявлено должным образом последним. И для одного окончательного рассмотрения оба метода поиска ресурсов должны бросать, если ресурс уже закрыт.
Я оставляю реализацию ReleaseManager
как упражнение для вас, если вам нравится это решение.