Как избежать частичного выполнения кода при обработке исключений?

#java #exception

Вопрос:

Я создаю текстовое приключение. Следующий код-это игрушечная модель, показывающая мою проблему.

 public class Player:
    
    private Location location;
    private Inventory inventory;
    
    public take(Item item) throws ActionException {
        location.remove(item); // throws ActionException if item isn't in location
        inventory.add(item); // throws ActionException if item can't be picked up
    }
}
 

Моя проблема заключается в следующем: что делать, если предмет может быть удален из местоположения, но не может быть добавлен в инвентарь игрока? В настоящее время код удалит предмет из местоположения, но затем не сможет добавить его в инвентарь игрока.

По сути, я хочу, чтобы произошло и то, и другое, или ни то, ни другое.

Есть идеи, как я могу это сделать? Любая помощь будет признательна.

Комментарии:

1. Спасибо, это хорошая мысль. Я создам несколько функций для проверки безопасности каждого из этих действий.

Ответ №1:

В идеале у вас должна быть функция inventory.canAddItem(item), как бы она ни называлась, которая возвращает логическое значение, которое вы можете вызвать перед удалением из местоположения. Как отметил комментатор, использование исключений для потока управления-не лучшая идея.

Если это не проблема, чтобы добавить обратно в местоположение, то что-то вроде:

 public take(Item item) throws ActionException {
    location.remove(item);
    try{
        inventory.add(item);
    }
    catch(ActionException e){
        location.add(item);
    }
}
 

мог бы работать на тебя.

Ответ №2:

То, что вы обычно ищете, — это концепция транзакций.

Это нетривиально. Обычная стратегия заключается в использовании баз данных, которые поддерживают его изначально.

Если вы не хотите туда идти, вы можете черпать вдохновение в DBs.

Они работают со схемами управления версиями. Подумайте о блокчейне или системах управления версиями, таких как git: На самом деле вы нигде ничего не добавляете и не удаляете, вместо этого вы создаете клоны. Таким образом, ссылка на какое-то состояние игры никогда не может измениться, и это хорошо, потому что подумайте об этом:

Даже если удаление работает, и добавление также работает, если задействованы другие потоки или между этими двумя действиями есть какой-либо код, они могут «засвидетельствовать» ситуацию, когда элемент просто исчез. Он был удален, location но еще не был добавлен inventory .

Представьте себе файловую систему. Допустим, у вас есть каталог с 2 файлами. Вы хотите удалить первую строку из первого файла и добавить ее во второй файл. Если вы на самом деле просто сделаете это (отредактируете оба файла), всегда будет момент, когда любая другая программа, наблюдающая за каталогом, может наблюдать недопустимое состояние (когда она либо не видит эту строку ни в одном файле, либо видит ее в обоих).

Поэтому вместо этого вы сделали бы следующее: вы создаете новый каталог, копируете оба файла, настраиваете оба файла, а затем одним атомарным действием переименовываете вновь созданный каталог на его старое имя. Любая программа, предполагающая, что они «захватывают дескриптор каталога» (именно так это работает, например, в linux), не может наблюдать недопустимое состояние. Они либо получают старое состояние (строка находится в файле 1, а не в файле 2), либо новое состояние (строка находится в файле 2, а не в файле 1).

Вы можете использовать тот же подход в ваш код, где все государства является неизменяемым, все изменения делаются через строителей (изменяемых вариантов) или по одному шагу за раз, с неизменным деревьев между ними, и как только вы закончите все, что вам сделать, это взять одно поле типа GameState и обновить его, чтобы ссылаться на новое государство — Ява гарантирует, что если вы пишете: someObj = someExpr , что другие потоки будут видеть либо старое состояние или новое состояние, они не могут увидеть половину указатель или тому подобная чушь. (Вам все равно понадобится volatile synchronized или что-то еще, чтобы гарантировать, что все потоки получат обновление своевременно).

Если резьба просто не имеет значения, есть еще одна альтернатива:

Действия в режиме игры.

Вместо простого вызова location.remove вы можете вместо этого работать с действием gamestate. Такое действие знает и то, как выполнить задание (удалить элемент из местоположения), но оно также точно знает, как отменить задание.

Затем вы можете написать небольшую структуру, в которой вы составляете список действий в состоянии игры (здесь: действие, которое может выполнить или отменить «удалить из местоположения», и действие, которое может выполнить или отменить «добавить это в инвентарь»). Затем вы передаете фреймворку список действий. Затем эта структура будет выполнять каждое действие по одному, отлавливая исключения. Если он поймает один из них, он будет действовать в обратном порядке и отменять каждое действие gamestate.

Таким образом, у вас есть управляемая стратегия, в которой вы можете просто последовательно выполнить несколько операций, зная, что если одна из них завершится неудачей, все, что было сделано до сих пор, будет отменено должным образом.

Эту стратегию совершенно невозможно заставить правильно работать в многопоточной среде без глобальной блокировки состояния, поэтому убедитесь, что в будущем вам это не понадобится.

Все это довольно сложно. Как следствие… большинство людей просто использовали бы для этого механизм БД; у них есть транзакционная поддержка. В качестве бонуса, сохранение вашей игры теперь тривиально (база данных все время сохраняет ее для вас).

Обратите внимание, что h2-это бесплатный, с открытым исходным кодом, полностью java (серверы не нужны, только один jar, который должен быть там при запуске вашей программы), основанный на файлах (например, все базы данных представляют собой один файл) механизм БД, который поддерживает транзакции и приличный синтаксис SQL. Это был бы один из вариантов. Для удобного доступа объедините его с хорошей абстракцией на уровне доступа к основной базе данных java, такой как JDBI, и у вас будет система, которая:

  • Может сохранять файлы тривиально.
  • Позволяет быстро выполнять сложные запросы, такие как «найти все игровые комнаты с истекающим кровью монстром».
  • Полностью поддерживает транзакции.

Вы бы просто выполнили эти команды:

 START TRANSACTION;
DELETE FROM itemsAtLocation WHERE loc = 18 AND item = 356;
INSERT INTO inventory (itemId) VALUES (356);
COMMIT;
 

и либо то и другое происходит, либо ни то, ни другое не происходит. До тех пор, пока вы можете выражать правила в терминах ограничений БД, БД будет проверять вас и отказываться фиксировать, если вы нарушите правило. Например, вы можете тривиально заявить, что любой заданный идентификатор элемента может быть в инвентаре не более одного раза.

Последний и, возможно, самый простой, но наименее гибкий вариант-просто закодировать его: весь ваш код, изменяющий состояние игры, должен СНАЧАЛА убедиться, что он на 100% уверен, что сможет выполнить каждую задачу в последовательности неразрушающим образом. Только когда он знает, что это возможно, тогда вся работа выполнена. Если один из них выйдет из строя на полпути, просто произойдет жесткий сбой, и ваша игра теперь находится в неизвестном, нестабильном состоянии. Смысл создания исключений теперь сводится к обнаружению ошибок: исключение теперь просто означает, что вы ошиблись и ваш код проверки не охватил все базы. Если предположить, что в вашей игре нет ошибок, исключений никогда не будет. Естественно, этот тоже просто нельзя заставить работать в многопоточном режиме. На самом деле, только DBs являются надежным ответом, если вы этого хотите, или описывают большую часть того, что делают DBs.

Комментарии:

1. Спасибо за ваши отличные знания! Я не думаю, что пойду на это решение здесь, потому что в моей игре нет многопоточности, но это может быть очень полезно для другого моего проекта.