Pull to refresh

Java. Мое решение для поиска изменений между двумя объектами. ChangeChecker

Level of difficultyMedium
Reading time15 min
Views4K

Вступление

Во время работы над аддоном для Jakarta-валидации мне пришлось писать логику по проверке изменений в модели по собственной аннотации CheckExistingByConstraintAndUnmodifiableAttributes.

Долго разглядывал получившейся код, и в голову пришла светлая (наверное) идея: почему бы не вынести все это в полноценный настраиваемый класс?

Для чего это решение

Как уже было сказано, решение предназначено для поиска и получения подробной информации о различиях (далее буду называть "дельтой") между двумя объектами.

Скажем, нам нужно проверить изменения по конкретным полям, которых может не быть в equals, и получить информацию о различиях отдельно для каждого поля. Допустим, как раз в рамках проверки определенных (не всех) полей на неизменяемость для моделек. И полной информации об ошибке, если изменения есть.

Вот в подобных кейсах мое решение - ChangeChecker - и можно использовать.

Поговорим о реализации идеи. Два объекта.

Я не буду сильно вдаваться в детали реализации (опять-таки, детали можно будет посмотреть в репозитории) и постараюсь сконцентрироваться на "спецификации".

ChangeChecker

Реализации этого интерфейса, собственно, и проделывают всю работу по поиску "дельты" между объектами. Про реализации - чуть позже, ну а пока выглядит он вот так.

Скрытый текст
/**
 * Interface for finding differences between two objects.
 * @param <T> - type of objects
 * @see ValueChangesCheckerResult
 *
 * @author Ihar Smolka
 */
public interface ChangesChecker<T> {

    /**
     * Find differences between two objects.
     * @param oldObj - old object
     * @param newObj - new object
     * @return finding result
     */
    ValueChangesCheckerResult getResult(T oldObj, T newObj);
}

Все просто: на вход поступают два объекта одинакового типа, на выходе - получаем подробный результат сопоставления по двум объектам.

Как выглядит результат.

Скрытый текст
/**
 * Result for check two objects.
 * @see com.ismolka.validation.utils.change.ChangesChecker
 *
 * @param differenceMap - difference map
 * @param equalsResult - equals result
 * @author Ihar Smolka
 */
public record ValueChangesCheckerResult(
        Map<String, Difference> differenceMap,
        boolean equalsResult
) implements Difference, CheckerResult {

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ValueChangesCheckerResult that = (ValueChangesCheckerResult) o;
        return equalsResult == that.equalsResult && Objects.equals(differenceMap, that.differenceMap);
    }

    @Override
    public int hashCode() {
        return Objects.hash(differenceMap, equalsResult);
    }

    @Override
    public <T extends Difference> T unwrap(Class<T> type) {
        if (type.isAssignableFrom(ValueChangesCheckerResult.class)) {
            return type.cast(this);
        }

        throw new ClassCastException(String.format("Cannot unwrap ValueChangesCheckerResult to %s", type));
    }

    @Override
    public CheckerResultNavigator navigator() {
        return new DefaultCheckerResultNavigator(this);
    }
}

И связанный интерфейс Difference.

Скрытый текст
/**
 * Difference interface
 *
 * @author Ihar Smolka
 */
public interface Difference {

    /**
     * for unwrapping a difference
     *
     * @param type - toType
     * @return unwrapped difference
     * @param <TYPE> - type
     */
    <TYPE extends Difference> TYPE unwrap(Class<TYPE> type);
}

  • Difference по смыслу близок к "интерфейсам-маркерам", т.к. он помечает все классы, касающиеся информации о "дельте". Если бы не метод unwrap, предназначенный для более "красивого" приведения Difference-объекта к конкретной реализации - можно было бы считать его таковым.

  • differenceMap - необходимо для хранения развернутой информации по различиям между двумя объектами. Здесь название поля/путь к полю маппится на определенный Difference. Это позволяет хранить сложную структуру "дельты" с вложениями самых разных видов (и результатам по Map, и по Collection, и прочее).

  • equalsResult - думаю, смысл понятен. Говорит, есть ли "дельта" у объектов.

ValueDifference

Выглядит так.

Скрытый текст
/**
 * Difference between two values.
 *
 * @param valueFieldPath - attribute path from the root class.
 * @param valueFieldRootClass - attribute root class.
 * @param valueFieldDeclaringClass - attribute declaring class.
 * @param valueClass - value class.
 * @param oldValue - old value.
 * @param newValue - new value.
 * @param <F> - value type.
 *
 * @author Ihar Smolka
 */
public record ValueDifference<F>(String valueFieldPath,
                                    Class<?> valueFieldRootClass,

                                    Class<?> valueFieldDeclaringClass,
                                    Class<F> valueClass,
                                    F oldValue,
                                    F newValue) implements Difference {

    @Override
    public <T extends Difference> T unwrap(Class<T> type) {
        if (type.isAssignableFrom(ValueDifference.class)) {
            return type.cast(this);
        }

        throw new ClassCastException(String.format("Cannot unwrap AttributeDifference to %s", type));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ValueDifference<?> that = (ValueDifference<?>) o;
        return Objects.equals(valueFieldPath, that.valueFieldPath) && Objects.equals(valueFieldRootClass, that.valueFieldRootClass) && Objects.equals(valueClass, that.valueClass) && Objects.equals(oldValue, that.oldValue) && Objects.equals(newValue, that.newValue);
    }

    @Override
    public int hashCode() {
        return Objects.hash(valueFieldPath, valueFieldRootClass, valueClass, oldValue, newValue);
    }
}

Это класс для хранения базовой информации о двух различающихся объектах. Тут мы видим oldObject и newObject (смысл очевиден), их класс, а так же остальную мета-информацию, которая может оказаться полезной в рамках сопоставления объектов, как атрибутов определенного класса.

ValueCheckDescriptorBuilder

Основное содержимое такое.

Скрытый текст
/**
 * Builder for {@link ValueCheckDescriptor}.
 * @see ValueCheckDescriptor
 *
 * @param <Q> - value type
 * @author Ihar Smolka
 */
public class ValueCheckDescriptorBuilder<Q> {

    Class<?> sourceClass;

    Class<Q> targetClass;

    String attribute;

    Set<String> equalsFields;

    Method equalsMethodReflection;

    BiPredicate<Q, Q> biEqualsMethod;

    ChangesChecker<Q> changesChecker;

   ...
}

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

  • sourceClass - класс, в котором атрибут определен.

  • targetClass - класс атрибута.

  • attribute - название атрибута/путь.

  • equalsFields - внутренние поля для сопоставления по equals. Может работать совместно с установленным changesChecker, но с equalsMethodReflection и biEqualsMethod несовместимо.

  • equalsMethodReflection - экземпляр Method. Может пригодиться, когда передаем какой-то "кастомный equals" по рефлексии.

  • biEqualsMethod - BiPredicate, по которому будут сопоставляться объекты. Можно просунуть, например, Objects.equals (хотя это бессмысленно, т.к. Objects.equals вызовется в случае, если другие способы сопоставления не обозначены).

  • changesChecker - можно передавать для проверки какой-то вложенный ChangeChecker. Как это используется - можно будет понять по ходу статьи.

И ключевое.

DefaultValueChangesCheckerBuilder

Выглядит вот так и определяет настройки для проверки двух объектов.

Скрытый текст
/**
 * Builder for {@link ValueCheckDescriptor}.
 * @see DefaultValueChangesChecker
 *
 * @param <T> - value type
 * @author Ihar Smolka
 */
public class DefaultValueChangesCheckerBuilder<T> {

    Class<T> targetClass;

    Set<ValueCheckDescriptor<?>> attributesCheckDescriptors;

    boolean stopOnFirstDiff;

    Method globalEqualsMethodReflection;

    BiPredicate<T, T> globalBiEqualsMethod;

    Set<String> globalEqualsFields;

   ...
}

  • targetClass - класс объектов.

  • attributesCheckDescriptors - описываются "сложные" чеки по атрибутам, используя предыдущий класс. Совместимо с globalEqualsFields, несовместимо с globalEqualsMethodReflection и globalBiEqualsMethod.

  • stopOnFirstDiff - останавливать ли проверку на первом различии.

  • globalEqualsFields - по каким атрибутам будет простой equals. По смыслу тоже самое, что и equalsFields в предыдущем классе, только работает уже "над" переданными ValueCheckDescriptor.

Примеры использования в виде тестов.

Скрытый текст
    @Test
    public void test_innerObject() {
        ChangeTestObject oldTestObj = new ChangeTestObject();
        ChangeTestObject newTestObj = new ChangeTestObject();

        oldTestObj.setInnerObject(new ChangeTestInnerObject(OLD_VAL_STR));
        newTestObj.setInnerObject(new ChangeTestInnerObject(NEW_VAL_STR));

        CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
                .addAttributeToCheck(
                        ValueCheckDescriptorBuilder.builder(ChangeTestObject.class, ChangeTestInnerObject.class)
                                .attribute("innerObject")
                                .addEqualsField("valueFromObject")
                                .build()
                )
                .build().getResult(oldTestObj, newTestObj);

        ValueDifference<?> valueDifference = result.navigator().getDifference("innerObject.valueFromObject").unwrap(ValueDifference.class);

        String oldValueFromCheckResult = (String) valueDifference.oldValue();
        String newValueFromCheckResult = (String) valueDifference.newValue();

        Assertions.assertEquals(oldValueFromCheckResult, oldTestObj.getInnerObject().getValueFromObject());
        Assertions.assertEquals(newValueFromCheckResult, newTestObj.getInnerObject().getValueFromObject());
    }

Скрытый текст
    @Test
    public void test_innerObjectWithoutValueDescriptor() {
        ChangeTestObject oldTestObj = new ChangeTestObject();
        ChangeTestObject newTestObj = new ChangeTestObject();

        oldTestObj.setInnerObject(new ChangeTestInnerObject(OLD_VAL_STR));
        newTestObj.setInnerObject(new ChangeTestInnerObject(NEW_VAL_STR));

        CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
                .addGlobalEqualsField("innerObject.valueFromObject")
                .build().getResult(oldTestObj, newTestObj);

        ValueDifference<?> valueDifference = result.navigator().getDifference("innerObject.valueFromObject").unwrap(ValueDifference.class);

        String oldValueFromCheckResult = (String) valueDifference.oldValue();
        String newValueFromCheckResult = (String) valueDifference.newValue();

        Assertions.assertEquals(oldValueFromCheckResult, oldTestObj.getInnerObject().getValueFromObject());
        Assertions.assertEquals(newValueFromCheckResult, newTestObj.getInnerObject().getValueFromObject());
    }

Продолжаем разговор. Две коллекции/массива

CollectionChangesChecker

Для сравнения двух коллекций есть интерфейс CollectionChangesChecker, расширяющий базовый ChangesChecker.

Скрытый текст
/**
 * Interface for check differences between two collections.
 * @see CollectionChangesCheckerResult
 *
 * @param <T> - collection value type
 *
 * @author Ihar Smolka
 */
public interface CollectionChangesChecker<T> extends ChangesChecker<T> {

    /**
     * Find difference between two collections.
     *
     * @param oldCollection - old collection
     * @param newCollection - new collection
     * @return {@link CollectionChangesCheckerResult}
     */
    CollectionChangesCheckerResult<T> getResult(Collection<T> oldCollection, Collection<T> newCollection);

    /**
     * Find difference between two arrays
     *
     * @param oldArray - old array
     * @param newArray - new array
     * @return {@link CollectionChangesCheckerResult}
     */
    CollectionChangesCheckerResult<T> getResult(T[] oldArray, T[] newArray);
}

Как видим, появилось еще два метода - getResult по коллекциям и по массивам (в реализации массивы просто оборачиваются в List и проходят через getResult с коллекциями).

Возвращают они CollectionChangesCheckerResult.

CollectionChangesCheckerResult

Скрытый текст
/**
 * Result for check two collections.
 *
 * @param collectionClass - collection value class.
 * @param collectionDifferenceMap - collection difference.
 * @param equalsResult - equals result
 * @param <F> - type of collection values
 *
 * @author Ihar Smolka
 */
public record CollectionChangesCheckerResult<F>(
        Class<F> collectionClass,
        Map<CollectionOperation, Set<CollectionElementDifference<F>>> collectionDifferenceMap,
        boolean equalsResult) implements Difference, CheckerResult {

...
}

Скрытый текст
/**
 * Possible modifying operations for {@link java.util.Collection}.
 *
 * @author Ihar Smolka
 */
public enum CollectionOperation {

    /**
     * Add element
     */
    ADD,

    /**
     * Remove element
     */
    REMOVE,

    /**
     * Update element
     */
    UPDATE
}

Скрытый текст
/**
 * Difference between two elements of {@link java.util.Collection}.
 *
 * @param diffBetweenElementsFields - difference between elements.
 * @param elementFromOldCollection - element from old collection.
 * @param elementFromNewCollection - element from new collection.
 * @param elementFromOldCollectionIndex - index of element from old collection.
 * @param elementFromNewCollectionIndex - index of element from new collection.
 * @param <F> - type of collection elements.
 *
 * @author Ihar Smolka
 */
public record CollectionElementDifference<F>(
        Map<String, Difference> diffBetweenElementsFields,
        F elementFromOldCollection,
        F elementFromNewCollection,
        Integer elementFromOldCollectionIndex,
        Integer elementFromNewCollectionIndex
) implements Difference {

...
}

Как видим, в этот раз хранящая "дельту" информация представлена в виде мапы, в которой операция по изменению коллекции сопоставлена с множеством изменений этого типа.

Ну а CollectionElementDifference содержит информацию про то, какие элементы из каких коллекций различаются, на каких индексах и какие именно между ними различия. Для операции UPDATE оба элемента должны быть заполнены. Для ADD будет отсутствовать старый элемент, для REMOVE - соответственно, новый.

DefaultCollectionChangesCheckerBuilder

Скрытый текст
**
 * Builder for {@link DefaultCollectionChangesChecker}
 *
 * @param <T> - type of collection elements.
 *
 * @author Ihar Smolka
 */
public class DefaultCollectionChangesCheckerBuilder<T> {

    Class<T> collectionGenericClass;

    Set<ValueCheckDescriptor<?>> attributesCheckDescriptors;

    boolean stopOnFirstDiff;

    Set<CollectionOperation> forOperations;

    Set<String> fieldsForMatching;

    Method globalEqualsMethodReflection;

    BiPredicate<T, T> globalBiEqualsMethod;

    Set<String> globalEqualsFields;
...
}

В принципе, все почти аналогично DefaultValueChangesCheckerBuilder, поговорим о различиях.

  • fieldsForMatching - по каким полям будут сопоставляться объекты в рамках коллекций. Т.е., если эти поля у двух элементов в разных коллекциях совпадают - то они будут сопоставляться друг с другом, и если "дельта" между ними есть - тогда это UPDATE элемента в коллекции. Если это не определено - в качестве такого "ключа" будет выступать индекс в коллекции.

  • forOperations - для каких операций мы получаем "дельту". По умолчанию для всех.

  • collectionGenericClass - экземпляры какого класса коллекция в себе держит.

Пример использования в виде теста.

Скрытый текст
    @Test
    public void test_collection() {
        String key = "ID_IN_COLLECTION";

        ChangeTestObject oldTestObj = new ChangeTestObject();
        ChangeTestObject newTestObj = new ChangeTestObject();

        ChangeTestObjectCollection oldCollectionObj = new ChangeTestObjectCollection(key, OLD_VAL_STR);
        ChangeTestObjectCollection newCollectionObj = new ChangeTestObjectCollection(key, NEW_VAL_STR);

        oldTestObj.setCollection(List.of(oldCollectionObj));
        newTestObj.setCollection(List.of(newCollectionObj));

        CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
                .addAttributeToCheck(
                        ValueCheckDescriptorBuilder.builder(ChangeTestObject.class, ChangeTestObjectCollection.class)
                                .attribute("collection")
                                .changesChecker(
                                        DefaultCollectionChangesCheckerBuilder.builder(ChangeTestObjectCollection.class)
                                                .addGlobalEqualsField("valueFromCollection")
                                                .addFieldForMatching("key")
                                                .build()
                                ).build()

                ).build().getResult(oldTestObj, newTestObj);

        CollectionElementDifference<ChangeTestObjectCollection> difference = result.navigator().getDifferenceForCollection("collection", ChangeTestObjectCollection.class).stream().findFirst().orElseThrow(() -> new RuntimeException("Result for collection is not present"));

        Assertions.assertEquals(difference.elementFromOldCollection().getValueFromCollection(), oldCollectionObj.getValueFromCollection());
        Assertions.assertEquals(difference.elementFromNewCollection().getValueFromCollection(), newCollectionObj.getValueFromCollection());
    }

Разговор приближается к концу. Две мапы

MapChangesChecker

На то у нас есть следующий интерфейс.

Скрытый текст
/**
 * Interface for check differences between two maps.
 * @see MapChangesCheckerResult
 *
 * @param <K> - key type
 * @param <V> - value type
 *
 * @author Ihar Smolka
 */
public interface MapChangesChecker<K, V> extends ChangesChecker<V> {

    /**
     * Find difference between two maps.
     *
     * @param oldMap - old map
     * @param newMap - new map
     * @return difference result
     */
    MapChangesCheckerResult<K, V> getResult(Map<K, V> oldMap, Map<K, V> newMap);
}

K описывает класс ключа, V - соответственно, класс значения для мап.

MapChangesCheckerResult

Скрытый текст
/**
 * Result for check two maps.
 * @see MapElementDifference
 *
 * @param keyClass - key class
 * @param valueClass - value class
 * @param mapDifference - map difference
 * @param equalsResult - equals result
 * @param <K> - key type
 * @param <V> - value type
 *
 * @author Ihar Smolka
 */
public record MapChangesCheckerResult<K, V>(
        Class<K> keyClass,

        Class<V> valueClass,

        Map<MapOperation, Set<MapElementDifference<K, V>>> mapDifference,

        boolean equalsResult
) implements Difference, CheckerResult {

...
}

Скрытый текст
/**
 * Possible modifying operations for {@link java.util.Map}.
 *
 * @author Ihar Smolka
 */
public enum MapOperation {

    /**
     * Add element
     */
    PUT,

    /**
     * Remove element
     */
    REMOVE,

    /**
     * Update element
     */
    UPDATE
}

Скрытый текст
/**
 * Difference between two elements of {@link Map}.
 *
 * @param diffBetweenElementsFields - difference between elements
 * @param elementFromOldMap - element from the old map
 * @param elementFromNewMap - element from tht new map
 * @param key - map key with difference
 * @param <K> - key type
 * @param <V> - value type
 *
 * @author Ihar Smolka
 */
public record MapElementDifference<K, V>(
        Map<String, Difference> diffBetweenElementsFields,

        V elementFromOldMap,

        V elementFromNewMap,

        K key
) implements Difference {

...
}

В целом, похоже на CollectionChangesCheckerResult, только теперь здесь присутствуют классы ключа и значения. Ну и мапа с "дельтой" держит чуть другую информацию - подробно останавливаться на ней вряд ли имеет смысл, все должно быть понятно уже без лишних слов.

DefaultMapChangesCheckerBuilder

Скрытый текст
/**
 * Builder for {@link DefaultMapChangesChecker}
 *
 * @param <K> - key type
 * @param <V> - value type
 */
public class DefaultMapChangesCheckerBuilder<K, V> {

    Class<K> keyClass;

    Class<V> valueClass;

    Set<MapOperation> forOperations;

    Set<ValueCheckDescriptor<?>> attributesCheckDescriptors;

    boolean stopOnFirstDiff;

    Method globalEqualsMethodReflection;

    BiPredicate<V, V> globalBiEqualsMethod;

    Set<String> globalEqualsFields;

...
}

Опять-таки, думаю, здесь все понятно без лишних слов, т.к. очень похоже на предыдущие билдеры.

По традиции.

Скрытый текст
    @Test
    public void test_map() {
        String key = "ID_IN_MAP";

        ChangeTestObject oldTestObj = new ChangeTestObject();
        ChangeTestObject newTestObj = new ChangeTestObject();

        ChangeTestObjectMap oldMapObj = new ChangeTestObjectMap(OLD_VAL_STR);
        ChangeTestObjectMap newMapObj = new ChangeTestObjectMap(NEW_VAL_STR);

        oldTestObj.setMap(Map.of(key, oldMapObj));
        newTestObj.setMap(Map.of(key, newMapObj));

        CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
                .addAttributeToCheck(
                        ValueCheckDescriptorBuilder.builder(ChangeTestObject.class, ChangeTestObjectMap.class)
                                .attribute("map")
                                .changesChecker(
                                        DefaultMapChangesCheckerBuilder.builder(String.class, ChangeTestObjectMap.class)
                                                .addGlobalEqualsField("valueFromMap")
                                                .build()
                                ).build()
                ).build().getResult(oldTestObj, newTestObj);

        MapElementDifference<String, ChangeTestObjectMap> difference = result.navigator().getDifferenceForMap("map", String.class, ChangeTestObjectMap.class).stream().findFirst().orElseThrow(() -> new RuntimeException("Result for map is not present"));

        Assertions.assertEquals(difference.elementFromOldMap().getValueFromMap(), oldMapObj.getValueFromMap());
        Assertions.assertEquals(difference.elementFromNewMap().getValueFromMap(), newMapObj.getValueFromMap());
    }

Разговор почти окончен. Навигация по результату

На мой взгляд, пользуясь этими инструментами можно относительно легко получить "дельту" по объектами любой (ну или практически любой) структуры.

Вопрос теперь в том, как нам удобно «навигировать» по полученному бардаку полученной дельте. На помощь приходит следующий интерфейс.

Скрытый текст
/**
 * Interface for navigation in {@link com.ismolka.validation.utils.change.CheckerResult}.
 * @see com.ismolka.validation.utils.change.CheckerResult
 *
 * @author Ihar Smolka
 */
public interface CheckerResultNavigator {

    /**
     * Get difference for {@link java.util.Map}
     *
     * @param fieldPath - attribute path with difference.
     * @param keyClass - key class.
     * @param valueClass - value class.
     * @param operations - return for {@link MapOperation}.
     * @return {@link Set} of {@link MapElementDifference} - if differences are there and 'null' - if aren't.
     * @param <K> - key type.
     * @param <V> - value type.
     */
    <K, V> Set<MapElementDifference<K, V>> getDifferenceForMap(String fieldPath, Class<K> keyClass, Class<V> valueClass, MapOperation... operations);

    /**
     * Get difference for {@link java.util.Collection}
     *
     * @param fieldPath - attribute path with difference.
     * @param forClass - class of collection values.
     * @param operations - return for {@link CollectionOperation}.
     * @return {@link Set} of {@link CollectionElementDifference} - if differences are there and 'null' - if aren't.
     * @param <T> - value type
     */
    <T> Set<CollectionElementDifference<T>> getDifferenceForCollection(String fieldPath, Class<T> forClass, CollectionOperation... operations);

    /**
     * Get difference for {@link java.util.Map}
     *
     * @param keyClass - key class.
     * @param valueClass - value class.
     * @param operations - return for {@link MapOperation}.
     * @return {@link Set} of {@link MapElementDifference} - if differences are there and 'null' - if aren't.
     * @param <K> - key type.
     * @param <V> - value type.
     */
    <K, V> Set<MapElementDifference<K, V>> getDifferenceForMap(Class<K> keyClass, Class<V> valueClass, MapOperation... operations);

    /**
     * Get difference for {@link java.util.Collection}
     *
     * @param forClass - class of collection values.
     * @param operations - return for {@link CollectionOperation}.
     * @return {@link Set} of {@link CollectionElementDifference} - if differences are there and 'null' - if aren't.
     * @param <T> - value type
     */
    <T> Set<CollectionElementDifference<T>> getDifferenceForCollection(Class<T> forClass, CollectionOperation... operations);

    /**
     * Get difference for attribute.
     *
     * @param fieldPath - attribute path with difference.
     * @return {@link Difference} - if differences are there and 'null' - if aren't.
     */
    Difference getDifference(String fieldPath);

    /**
     * Get difference.
     *
     * @return {@link Difference}
     */
    Difference getDifference();
}

И каждый класс результата проверки отдаст нам по методу navigator() дефолтную реализацию этого интерфейса.

Через навигатор мы можем продираться через множество вложений и получать интересующую нас "дельту". Ну или null, если таковой не найдено.

Для "распаковки дельт" из коллекций и мап нужно использовать соответствующие методы getDifferenceForMap и getDifferenceForCollection (если интересует конкретная операция/операции - передаем в конце методов).

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

Как все это выглядит - можно увидеть по примерам в тестах.

Конец разговора

С решением можно ознакомиться во все том же репозитории с аддоном для валидации, в пакете com.ismolka.validation.utils.change.

Код еще не до конца приведен в божеский вид и, скорее всего, еще будет мелкий рефакторинг (как минимум).

Интересует ваше мнение. Насколько нужная штука, насколько хорошее решение, замечания и рекомендации по коду тоже приветствуются.

Всем дзякую!

Tags:
Hubs:
+12
Comments15

Articles