Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров4.5K

Вступление

Во время работы над аддоном для 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.

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

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

Всем дзякую!

Теги:
Хабы:
Всего голосов 12: ↑11 и ↓1+12
Комментарии15

Публикации

Ближайшие события