Сложная инициализация в конструкторе - PullRequest
1 голос
/ 03 июня 2019

У меня есть своего рода модель, которая управляет картами различных данных. Я читал, что конструкторы не должны содержать бизнес-логику, но я также читал, что конструкторы свободны делать то, что им нужно для инициализации состояния объекта. Что если, учитывая карту A и карту B в конструкторе, я хочу объединить эти две карты и установить результат в третьем поле. Возможно я также хочу сделать некоторую уборку или кое-что. Это плохая практика? Если так, то почему?

public class MapManager {

    private Map<String, Object> mapA;
    private Map<String, Object> mapB;
    private Map<String, Object> combinedMap;

    public MapManager(Map<String, Object> mapA, Map<String, Object> mapB) {
        this.mapA = mapA;
        this.mapB = mapB;
        this.combinedMaps = initCombinedMap(mapA, mapB);
    }

    public Map<String, Object> getMapA() {
        return mapA;
    }

    public Map<String, Object> getMapB() {
        return mapB;
    }

    public Map<String, Object> getCombinedMap() {
        return combinedMap;
    }

    private static Map<String, Object> initCombinedMap(Map<String, Object> mapA, Map<String, Object> mapB) {
        Map<String, Object> combinedMap = new HashMap<>(mapA);

        if (mapB != null) {
            mapB.forEach(combinedMap::putIfAbsent);
        }

        return combinedMap;
    }
}

Ответы [ 2 ]

1 голос
/ 10 июня 2019

Я читал, что конструкторы не должны содержать бизнес-логику, но я также читал, что конструкторы могут делать то, что им нужно, для инициализации состояния объекта.

Оба эти утверждения - хороший совет, и они не противоречат.

Я думаю, что ваш вопрос действительно о том, что считается "бизнес-логикой". Общая идея заключается в том, что «бизнес-логика» реализует «бизнес-правила», то есть поведения, о которых просил или мог бы заботиться фактический «бизнес-клиент» программного обеспечения (не разработчик).


Например, предположим, что бизнес-правило гласит, что в именах пользователей допускаются буквы кириллицы, , если либо (1) имя пользователя также содержит буквы латинского алфавита, либо (2) каждая буква кириллицы в имени пользователя является легко принять за латинский аналог. (Это бизнес-правило помогает предотвратить спуфинг: я не могу выдать себя за учетную запись с именем «Ben», создав одну учетную запись с именем «Веn», и учетную запись с именем «Дима» путем создания учетной записи с именем «Димa».)

Теперь у вас, по-видимому, есть класс объектов бизнес-домена, называемый чем-то вроде User, который представляет пользователя приложения; таким образом, у вас может возникнуть желание реализовать это бизнес-правило как инвариант класса User, что означает, что в буквальном смысле невозможно иметь экземпляр User, который нарушает это бизнес-правило. Для этого вы реализуете это правило в конструкторе (возможно, делегируя какой-то метод validateUsername или что-то в этом роде).

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

Другая проблема заключается в том, что, поскольку правило со временем усложняется, вы можете в конечном итоге извлечь соответствующие данные в базу данных или хранилище конфигурации, и вам, очевидно, не нужно подключение к базе данных только для создания экземпляра User в юнит-тесте.

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


К сожалению, я не понимаю ваш класс MapManager достаточно хорошо, чтобы комментировать его (и я на самом деле немного скептически отношусь к тому, что он имеет смысл как связный класс), но если это так помогает, вот несколько примеров логики инициализации, которые не считаются "бизнес-логикой":

  • Если класс представляет структуру данных, такую ​​как двусвязный список (java.util.LinkedList), хеш-таблица (java.util.HashMap), красно-черное дерево (java.util.TreeMap или java.util.TreeSet), то это абсолютно Ожидается, что его конструкторы будут включать логику для инициализации структуры данных - особенно если у него есть конструктор, который берет существующую коллекцию и копирует ее содержимое (как это делают все примеры). Это не «бизнес-логика», потому что у бизнес-клиентов нет мнения о них; они, вероятно, даже не знают терминов «связанный список» и «дерево». Все в этих классах относится к категории "технические", а не "деловые".
  • Если класснеизменяемое поле, которое не должно быть нулевым, тогда абсолютно ожидаемо, что его конструктор проверит это и выдаст java.lang.NullPointerException с информативным сообщением. Здесь может быть какое-то влияние бизнес-правила на техническую реализацию - мы можем представить новое бизнес-требование, которое довольно прямо переводится как «это поле должно быть обнуляемым сейчас» (хотя на самом деле это может быть реализовано другим способом, например, изменив поле на java.util.Optional) - но это техническое правило, потому что есть вероятность, что в нисходящем коде он будет использоваться, например писать такие вещи, как this.username.toLowerCase() без проверки на ноль. (Это вопрос прагматизма; зачем писать дополнительный код для поддержки пустых имен пользователей, если они вряд ли когда-либо будут разрешены? Лучше просто проверить заранее.)
1 голос
/ 03 июня 2019

Я думаю, что это нормально, но в зависимости от того, что вы используете, это может быть немного сложнее. Например, если вы комбинируете Map A и Map B только в конструкторе, вам следует удалить метод initCombinedMap и выполнить объединение в фактическом конструкторе.

Вы также можете использовать Pair из пакета javafx.util, чтобы иметь контейнер для ваших карт. Тогда ваши получатели могли бы получить Key для mapA и Value для mapB из пары.

Вот как будет выглядеть ваш класс после реализации этих предложений:

public class MapManager {

    private Pair<Map<String, Object>, Map<String, Object>> maps;
    private Map<String, Object> combinedMaps;

    public MapManager(Map<String, Object> mapA, Map<String, Object> mapB) {

        this.maps = new Pair<>(mapA, mapB);
        this.combinedMaps = new HashMap<>(mapA);

        if (mapB != null) {
            mapB.forEach(combinedMaps::putIfAbsent);
        }
    }

    public Map<String, Object> getMapA() {
        return maps.getKey();
    }

    public Map<String, Object> getMapB() {
        return maps.getValue();
    }

    public Map<String, Object> getCombinedMap() {
        return combinedMaps;
    }
}

Другим предложением было бы обобщить ваш класс, заменив String и Object универсальными типами K и V. Это не совсем упростит ваш класс, но обеспечит модульность, поскольку карты теперь могут содержать любые типы, которые пользователь назначает при создании нового MapManager.

public class MapManager<K, V> {

    private Pair<Map<K, V>, Map<K, V>> maps;
    private Map<K, V> combinedMaps;

    public MapManager(Map<K, V> mapA, Map<K, V> mapB) {

        this.maps = new Pair<>(mapA, mapB);
        this.combinedMaps = new HashMap<>(mapA);

        if (mapB != null) {
            mapB.forEach(combinedMaps::putIfAbsent);
        }
    }

    public Map<K, V> getMapA() {
        return maps.getKey();
    }

    public Map<K, V> getMapB() {
        return maps.getValue();
    }

    public Map<K, V> getCombinedMap() {
        return combinedMaps;
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...