понедельник, 7 марта 2011 г.

Две стороны одной медали: удобство разработки

Хотел немного подробнее раскрыть упомянутый в предыдущем посте антипаттерн "Immutable Class", изобретение которого принадлежит моей клавиатуре. В данном конкретном случая, это была попытка в Java реализовать Immutable object. И тут возникла проблема, которая всегда актуальна в спорах "динамическая типизация vs статическая типизация". Т.е. с одной стороны можно сделать конечный код (который использует этот класс) более простым, с другой стороны мы получаем ошибки, которые, как правильно в исходном посте заметил Андрей не находятся компилятором. И это действительно произошло, у людей были проблемы с модификацией этого класса, из за чего он и был наречён immutable. Хотел бы более развёрнуто описать почему я так решил, вернее почему мне кажется, что ответ на это вопрос не так однозначен. В самом исходном посте одну из причин я уже указывал, это наследования с огромным количеством переопределенных методов. Тут хотел рассмотреть вопрос удобства изменения только этого класса. Комментарии и в особенности критика приветствуются.

И так, у нас есть простой классик, объекты которого должны быть неизменяемые (для чего это делается объяснено в предыдущем посте, да и в целом по всему интернету). Для простоты оставил только два поля, у нас это было наверно около дёсятка (что так же, кстати, не очень верно).


public class User {
    
    private final int id;
    
    private final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

Вся проблема заключается в том, что даже для двух полей, удобно иметь какой-то способ, получить такой же объект, только с измененным одним полем. Что-то вроде immutable реализации setter'а. Решение, тот же setter, но который не меняет сам объект, а возвращает его копию с изменнёным полем. Стандартное решение, которое можно увидеть в одном из самых популярных неизменяемых объектов в java: String. И вот тут я вижу два противоположных подхода, один не красивый с человеческой стороны, но компилятор его понимает и более читаемый, но в случае ошибки, мы об этом далеко не сразу узнаем.

И так, первый способ достаточно очевидный.


public User name(String newName) {
    return new User(this.id, newName);
}

Теперь задумаемся. Что будет, если мы решим в класс добавить ещё одно поле? Конструктор поменяется и соответственно нам придется поменять и это метод, так как он использует конструктор. Ну или нагенерировать различных конструкторов с разными наборами методов, но это уж совсем вакханалия какая-то. Когда у нас два поля, это всего лишь два метода. Но когда их 10, то это уже большая копипаста. Но есть другая положительная сторона. Как только мы изменили наш конструктор, компилятор нам все эти методы красненьким подчеркнет.

Другой вариант уже не так прост, но вроде и не сильно переусложнён.


public User name(String newName) {
    return new UserBuilder(this).name(newName).build();
}

private static class UserBuilder {

    private final int id;

    private String name;

    public UserBuilder(User user) {
        this.id = user.id;
    }

    public UserBuilder name(String newName) {
        this.name = newName;
        return this;
    }

    public User build() {
        return new User(this.id, this.name);
    }

}

Мы добавили целый дополнительный класс, который фактически дублирует нашу сущность. Соответственно, как только мы добавили в исходный класс ещё одно поле нас компилятор попросит только изменить метод build, можно туда какой-то шлак вставить случайно и всё. Вообщем корень проблемы, что придется задуматься и правильно перенести поведения этого поля в сам Builder, добавить туда одно поле и метод для него. Но при этом получаем другое удобство, нам не надо менять 10 методов в базовом классе, а только добавить недостающий для нашего поля.

Я выбрал второй вариант скорее из за описанных в начальном посте проблем с наследниками. Но, по моему, даже если рассматривать его в текущем описанном контексте. Все равно по моему этот способ предпочтителен. Единственное, если бы мы использовали FindBug, было бы классно написать для него правило на этот случай... просто с compile time аннотациями возится долго и они всё слишком усложняют, если бы их поудобнее сделали... java....

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

Отправить комментарий