Pull to refresh

Comments 54

В это время в шарпе:

var info1 = new Info {
    email = "email@email.com",
    phone = "79998888888"
};

Как с этим дела обстоят в котлине, кто в курсе?
val info = Info(
    email = "email@email.com",
    phone = "79998888888"
)

Отличия:


  1. val, вместо var. Если переменная неизменяемая, то val (как в нашем случае)
  2. new писать не надо. В Java классы начинаются с большой буквы, а методы — с маленькой. По этому признаку легко отличить вызов метода от вызова конструктора.
  3. ; не требуется, если это последний statement в строке.

В примере выше создается immutable объект из декларации class(val email: String, val phone: String).


Однако, если её немного изменить на data class(val email: String, val phone: String), то можно применять паттерн "прототипирование":


val default = Info( ... )

fun customEmail(email: String): Info {
     return default.copy(email = email)
}
Если не изменяет память и в java давно почти также
var info1 = new Info() {{
    email = "email@email.com";
    phone = "79998888888";
}};
удачи с отловом утечек памяти потом.
Вот что интересно ничего подобного нигде об этом не слышал, почему утечки памяти, пояснить сможете?
если это объявление происходит в не статическом методе класса A(допустим), то в качестве значения info1 будет присвоен объект (не статического)анонимного класса
new Info(){} // это синтаксис создания анонимного класса
Не статический анонимный класс содержит ссылку на класс, в методе которого был объявлен.

public class A {
    public Info m() {
        var info1 = new Info() {{
            email = "email@email.com";
            phone = "79998888888";
        }};
        return info1;
    }
}
...
A a = new A();
doSomething(a.m());

Таким образом метод m() вернёт объект info1 со ссылкой на объект a. Дальше можно положить info1 в какую-нибудь коллекцию, и таким образом существенно продлить жизненный цикл объекта a(вызвать утечку памяти).
Спасибо, вот не знал, т.е. получается при вызове в объемлющем методе
new Info() {{...}}
с инициализацией мы получаем объект анонимного класса? Странно почему так сделано

Потому что синтаксис new T() { ... } как раз и предназначен для создания анонимного класса, а вложенные { ... } — это всего лишь его блок инициализации (такой блок возможен в любом классе, компилятор дописывает код из этих блоков во все конструкторы).


Вот что и правда странно — так это почему this всегда попадает в замыкание. Другие-то переменные попадают в замыкание только в случае их использования. Видимо, это связано с тем, что компилятор захватывает this не напрямую, а сначала преобразует анонимный класс в inner class.

вторые {… } это же объявление статической секции, которая выполнится до конструктора класса, разве нет?
косяк, вы правы… {… } чуть иначе работает
Спасибо за пояснение. Тогда уж ещё вопрос.
А как узнать, что this попал в замыкание, по файлу ...$.class?
Если класс объявлен как final, то не сработает.
Как с этим дела обстоят в котлине, кто в курсе?
Замечательно:
val info1 = Info(email = "mail@example.com")
val info2 = Info(email = "anothermail@example.com", phone = "1 234 567 8900")
val info3 = Info(uuid = UUID.randomUUID().toString())
Не совсем корректный пример. Т.к. тут либо использовать C#9 с init свойствами, либо объект получится мутабельный, хотя в примерах, если не ошибаюсь, делали иммутабельный объект
Или же с давно существующим в C# синтаксисом через имена параметров:

var info1 = new Info(
    email: "email@email.com",
    phone: "79998888888");

С моей субъективной точки зрения для объектов, которые являются просто описателями некой небольшой сущности, билдер выглядит не особо нужным. Другое дело, если мы создаём сложный объект, где можно добавлять множество различных параметров. Кстати, тут неплохо бы сказать и о fluent API, обычно являющимся неотемлемой частью реализации билдера.

Ну и, разумеется, зависит от языка. Если где-то нельзя поименовать передаваемые значения, то билдер может оказаться полезным воркэраундом.
Однако, для использование в проекте jackson'а, необходимо дополнить наш клас, чтобы он успешно десериализовывался.

Нет, достаточно в lombok.config указать lombok.anyConstructor.addConstructorProperties (https://projectlombok.org/features/configuration), чтобы ломбок на конструкторы добавлял аннотацию ConstructorProperties, которую Джексон прекрасно понимает.

UFO just landed and posted this here
Мы получаем весьма элегантное построение не сложного объекта:

Как раз для построения несложного объекта здесь нет элегантности, ибо сильно пестрят в глазу builder().build(). Кроме того билдеры в отличие от конструкторов не контролируют required-поля. Они хороши, когда есть настраиваемый объект с множеством опциональных пропертей.


Я предпочитаю factory method, в который добавляю required поля, без которых объект не имеет смысла, а все опциональные поля устанавливаются при помощи wither-ов. Есть минимальные накладные расходы на копирование immutable-объекта, но я предпочитаю элегантность и читабельность.


User.of("vassily", "Vasya Pupkine")
    .withEmail("vasya@pupkine.com")
    .withPhone("555-55-55")
Не по теме поста.
Сам рекомендовал бы — Java. Полное руководство | Шилдт Герберт, лично учился на ней.
И книги банды четырех.
UFO just landed and posted this here
На мой взгляд, есть ещё одна причина использования паттерна Builder, помимо избегания комбинаторного взрыва кол-во перегружаемых конструкторов — поэтапная инициализация. Бывают ситуации, когда для инициализации объекта необходимо передать в конструктор несколько аргументов, значение которых не могут быть получены одновременно. В таких случаях удобно использовать паттерн Builder.
поэтапная инициализация


Абсолютно согласен.
2. Использовать setter'ы — субьективно, нагромождает код.

Использование билдеров ещё больше нагромождает код. Я против сеттеров, просто хочу заметить, что билдеры используют те же самые сеттеры по сути, просто в другом классе (Builder). И в этом его главная проблема. Этот класс ненужен для приложения, он не принимает участие в бизнес логике, единственная его функция — вызвать конструктор объекта. Это просто ненужный шум, излишнее усложнение. Вызвать конструктор можно и напрямую, нет никакой проблемы в том что в классе будет 5, 10 конструкторов. Одним классом меньше в проекте — проще для понимания.

Один из варианто в данном случае использовать конструктор из Map<String, String>
Info(Map<String, String> fields)) {
    this.email = fields.get("email");
    this.uuid = fields.get("uuid");
    this.phone = fields.get("phone");
}

...

Info info = new Info(new Map<String, String>() {{
    put("email", "email@email.com");
    put("phone", "1234567890");
}});
И тогда никакой статической типизации, имхо, очень плохое решение
Так и у автора нет никакой статической типизации, все поля типа String. Вот такое
Info info1 = Info.builder()
                .uuid("79998888888")
                .phone("3d107928-d225-11ea-87d0-0242ac130003")
                .build();

вполне прокатит с точки зрения компилятора
Ну это пример со строками. А если будут другие типы данных? У автора методы будут принимать нужные примитивы, а у вас так не получится.

Плюс я ещё про названия полей, у вас можно
put("notExistedField", "email@email.com");

а у автора нельзя
notExistedField("email@email.com");
Этот класс ненужен для приложения, он не принимает участие в бизнес логике, единственная его функция — вызвать конструктор объекта.


Класс добавляет читабельности, т.к. вместо конструктора вида
var complexObject = new ComplexObject(
    0,
    id,
    name,
    null,
    null,
    uniqueName,
    firstAttribute,
    null,
    someCondition,
    isAvailable)

получается код вида

var builder = builder
    .StartsFrom(0)
    .WithId(id)
    .WithName(name)
    .WithUniqueName(uniqueName)
    .WithFirstAttribute(firstAttribute);

if (isSomeCondition) {
    builder.IsSomeCondition();
}

if (isAvailable) {
    builder.IsAvailable();
}

var complexObject = builder.Build();


В данном случае, можно передавать только нужные параметры и не передавать null, которые к тому же портят читаемость. Что скрывается за первым null? Ну го смотреть объявления конструктора, чтоб понять.
Так же, можно добавлять сложную логику, типа вдогонку

if (isAdmin) {
    builder.SetPermission(Permission.Write);
    builder.AllowEditing();
    builder.ShowHiddenValues();
}


нет никакой проблемы в том что в классе будет 5, 10 конструкторов

Есть проблема, что при добавлении нового параметра придётся всех их обновлять и могут возникнуть коллизии, типа есть конструктор без параметров, а есть с числовым параметром. Добавляем новый обязательный числовой параметр — первый конструктор превращается в старую версию второго и это легко упустить при рефакторинге.
Если конструкторов много, то при создании объекта нужно всех их изучить, ибо вероятно будет много похожих сигнатур с параметрами-примитивами, ибо «Одним классом меньше в проекте — проще для понимания.» и вряд ли для параметров будут созданы какие то специальные классы.
Дополнительно, билдер удобно использовать когда создаётся действительно сложный объект. У меня есть пример, где строится сложная таблица с большим количеством параметров и только билдер имеет порядка 20 методов установки свойств.
Постоянное повторение
.withXXX
и
builder
нисколько не добавляет читаемости, т.к. засоряет код лишними ненужными словами, не относящимися к предметной области и логике приложения.
Большое количество параметров в конструкторе решается не билдерами, а конструкторами из более сложных типов, словарей, JSON объектов, текста в каком-то формате и т.п. переменные id, name, firstAttribute и прочие скорее всего откуда-то читаются, из JSON, из параметров HTTP запроса, из БД, из файла и т.д. Поэтому просто надо сделать конструктор
import com.eclipsesource.json.*;

class ComplexObject {
    ...
    ComplexObject(JsonObject json)) {
        this.startsFrom = 0;
        this.id = json.get("id").asInt();
        this.name = json.get("name").asString();
        this.uniqueName = json.get("uniqueName").asString();
        this.firstAttribute = json.get("firstAttribute").asInt();

        if (isSomeCondition) {
            this.someCondition = true;
        }

        if (isAvailable) {
            this.available = true;
        }    
    }
}

При этом не создается никаких лишних объектов и конструкторов с 20-ю аргументами.

Посмотрю я как вы будете заполнять JsonObject без постоянных повторений того же .add или .set

JsonObject заполняется очень просто
new ComplexObject(Json.parse(request.body()).asObject())

Пример с JSON приведен для случая когда исходные данные приходят например в HTTP запросе в формате JSON. Если данные читаются из БД будет конструктор
ComplexObject(Connection c, int id) соответственно

Ну отлично, теперь объект должен "знать" откуда берутся в нём данные...

Вообще-то это основная обязанность объекта, вы слышали когда-нибудь про инкапсуляцию?

То есть, вы считаете что объект знающий о том откуда к ниму приходят данные это инкапсуляци? Завтра вы начнете читать из файла, и что добавите конструктор который принимает файл?

Именно так. Именно это и есть инкапсуляция, а вот когда вся программа знает какие там внутри объекта поля и устаналивает их через setXXX или withXXX, тотже сеттер по сути, это как раз полное отсутствие инкапсуляции.

Мне кажется, но у вас какое то свое понятие, об инкапсуляции. Делегировать простому объекту контейнеру знание о бизнес логике не самая здравая идея. Если бы создатели джава руководствовались такой идеологией то они бы в класс Integer вставили бы все методы из класса Math.

Делегировать простому объекту контейнеру знание о бизнес логике не самая здравая идея
а по-моему Вы что-то путаете. Где Вы видели в каком-либо определении понятия ООП термин «бизнес логика»? С точки зрения ООП код pin2t вполне себе каноничный.
С других точек зрения — да, есть к чему придраться.
А Math это вообще не ООП ни разу. Это просто набор функций.
Делегировать простому объекту контейнеру знание о бизнес логике не самая здравая идея.

Идея на самом деле хорошая, потому что позволяет избежать слишком большого скопления бизнес-логики в одном месте. Точнее даже так, бизнес-логика становится более "бизнес", она меньше заботится о форматах JSON или сетевых соединений, а больше оперирует тем, что объекты должны сделать для общей задачи.
Если считать объекты простыми контейнерами, как вы говорите, то это приводит к тому, что где-то в условном контроллере существует огромная по размеру процедура, которая делают всю логику и занимается всем, и парсит данные и делает запросы в БД, и подготавливает ответ. По мере усложнения бизнес логики работать с такой процедурой становится все сложнее, потому что она просто очень большая. Сложнее понять что она делает и сложнее что-то поменять без ошибок.
А вот если маленькие кусочки бизнес логики отдать объектам (которые не просто контейнеры, а нормальные такие умные объекты, которые понимают JSON например или SQL), то в одном конкретной процедуре этой бизнес логики получается скапливается не супер много. И проще понять что она делает и что-то исправить.


Если бы создатели джава руководствовались такой идеологией то они бы в класс Integer вставили бы все методы из класса Math.

Посмотрите на класс BigInteger, например. Функции класса Math вообще-то работают с примитивными типами в Java, которые не объекты, какое вообще отношение Math имеет к классу Integer, ни одной функции в Math, которая работает с Integer нету.
Math тут скорее namespace для функций

Если считать объекты простыми контейнерами, как вы говорите, то это приводит к тому, что где-то в условном контроллере существует огромная по размеру процедура, которая делают всю логику и занимается всем, и парсит данные и делает запросы в БД, и подготавливает ответ.

Не вижу как из одного следует другое. Кроме конструкторов у классов бывают ещё и методы, да и самих классов допускается заводить более одного...


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

А что, если объекты не будут понимать JSON — то в одной конкретной процедуре окажется больше бизнес-логики? Какое вообще отношение JSON имеет к бизнес-логике?

Разве это не нарушение принципа единой ответственности, как считаете?
Нет, не нарушение. Принцип единой ответственности он довольно расплывчатый. Ну т.е. он про то что класс Info наверное не должен заниматься запуском системных процессов, например. А если он работает с данными относящимися к этому же классу, просто в другом формате (не Java-представлении объектов в памяти, а JSON или SQL БД), это вполне согласуется с принципом единой ответственности.
И при это всё падает при невалидном json с непонятной ошибкой.
нисколько не добавляет читаемости

var complexObject = new ComplexObject(json);

Вот это точно не добавляет читаемости.
прочие скорее всего откуда-то читаются, из JSON, из параметров HTTP запроса, из БД, из файла и т.д.

Необязательно. Это может быть просто настройка какого то сложного процесса создания объекта. Плюс передать напрямую что то из HTTP запроса или файла в конструктор — ну так себе идея. Лучше это переложить в нормальную сущность и проверить корректность.
this.id = json.get(«id»).asInt();

А если параметр опциональный? А если параметр опциональный и я ошибусь и напишу в коде
this.id = json.getOrNull("identifier").asIntOrNull();

И т.к. параметр опциональный, а пользователь будет отправлять id, а я читаю identifier, то и ошибки никакой не будет, просто свойство будет всегда игнориться и об этом можно сразу и не узнать. (не уверен по названиям методов, суть понятна думаю)
А как пользователь кода узнает, что какие поля должен содержать объект JsonObject? Лезть в конструктор и смотреть? А если это сторонняя либа?

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

Как раз таки это может относится и к логике приложения и к предметной области, когда мы делаем разные

WithPermission(Permission.Read)

или
OwnedBy(usedId)
И при это всё падает при невалидном json с непонятной ошибкой.

Проверка JSON на валидность и выброс исключения с хорошим сообщением об ошибке — это задача вообще ортогональна конструированию объекта. Это надо будет делать все равно, хоть в конструкторе объекта, хоть во внешнем методе, который вызывает конструктор
Необязательно. Это может быть просто настройка какого то сложного процесса создания объекта.

Что за сложный процесс такой. Данные-то все равно откуда-то приходят, если часть приходят из HTTP запроса, часть из БД — вообще не проблема, у объекта будет конструктор ComplexObject(JsonObject json, Connection c)
Плюс передать напрямую что то из HTTP запроса или файла в конструктор — ну так себе идея. Лучше это переложить в нормальную сущность и проверить корректность.

Почему? В конструкторе это что-то из HTTP запроса можно точно также проверить и выкинуть исключение. И что такое нормальная сущность? Чем объект плох?
А если параметр опциональный и я ошибусь и напишу в коде

Если ошибетесь придется исправлять :-) А если вы ошибетесь вот так
.withName(uniqueName)

лишние слова не защищают от ошибок магическим образом, наоборот они провоцируют ошибки, больше шума надо читать, легко что-то не заметить
А как пользователь кода узнает, что какие поля должен содержать объект JsonObject? Лезть в конструктор и смотреть?

Вообще-то это конструктор объекта пишется под спецификацию JSON API, а не наоборот.
Проверка JSON на валидность и выброс исключения с хорошим сообщением об ошибке — это задача вообще ортогональна конструированию объекта. Это надо будет делать все равно, хоть в конструкторе объекта, хоть во внешнем методе, который вызывает конструктор


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

Что за сложный процесс такой. Данные-то все равно откуда-то приходят, если часть приходят из HTTP запроса, часть из БД — вообще не проблема, у объекта будет конструктор ComplexObject(JsonObject json, Connection c)


Из своего опыта — построение сложной таблицы, когда часть данных может быть недоступна из за настроек прав, часть — из-за настроек бизнес процессов, часть — из за состояния данных и т.д. Т.е. само конструирование таблицы включает много аспектов.
Плюс в вашем примере нужна зависимость от connection и всего http стека видимо. Хотя объект вообще не должен быть завязан на это

Почему? В конструкторе это что-то из HTTP запроса можно точно также проверить и выкинуть исключение. И что такое нормальная сущность? Чем объект плох?


Ответил выше уже. Сначала вы говорите что проверка json — не дело конструктора, а теперь, что нужно проверить и выкинуть исключение.

Вообще-то это конструктор объекта пишется под спецификацию JSON API, а не наоборот.


Вот уж не факт. Что под спецификацией JSON API подразумевается? Вот я пользователь, мне нужно создать сложный объект. Он принимает на вход JSON. Я магическим образом должен знать, что туда отправить? Из интерфейса билдера я могу понять, какие параметры обязательны, а какие нет и какие вообще есть параметры. Передавать json — за гранью добра и зла
Да никакого зла, наоборот сплошное добро. Вот например, приходят вам данные по объекту в HTTP запросе в JSON формате. В случае использования билдера, HTTP обработчик разбирает JSON, перекладывает поля в локальные переменные, потом локальные переменные перекладываются в билдер, потом билдер вызывает конструктор объекта. Если что-то меняется, добавляется поле например, надо менять и конструктор объекта, и билдер, и HTTP обработчик.

Все это точно также можно сделать без билдера и без промежуточных переменных, надо просто передать JSON прямо в конструктор объекта, объект сам все проверит и заполнит свои поля. И если что-то поменяется, добавится поле, например, надо будет поменять только сам объект.
В случае использования билдера, HTTP обработчик разбирает JSON, перекладывает поля в локальные переменные, потом локальные переменные перекладываются в билдер, потом билдер вызывает конструктор объекта


Плюс-минус так, да, и всё это гарантирует своевременное обнаружение ошибки и реакцию на нужном уровне. Падать в конструкторе где то в доменной области из за структуры json — это жесть какая то.

Если что-то меняется, добавляется поле например, надо менять и конструктор объекта, и билдер, и HTTP обработчик.

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

Все это точно также можно сделать без билдера и без промежуточных переменных, надо просто передать JSON прямо в конструктор объекта

Можно, вопрос в удобстве. Всё можно в json складывать и вообще его не парсить и надеяться, что там всё всегда будет.
Но ответили вы не на все мои вопросы
Плюс-минус так, да, и всё это гарантирует своевременное обнаружение ошибки и реакцию на нужном уровне.

ага, простой код вызова конструктора, размазанный ровным слоем по всему приложению, прямо «гарантирует» обнаружение ошибки, да
Падать в конструкторе где то в доменной области из за структуры json — это жесть какая то.

Падать в конструкторе на любом уровне это вообще не проблема, потому что в языке Java есть такая прекрасная вещь как исключения. Исключения круты тем, что обработчик может напечатать весь стек вызовов, который привел исключению. Обычно по этому стеку уже понятно что чинить и где чинить. Проблем нет никаких с этим.

Я не пойму, вы троллите что ли меня?

ага, простой код вызова конструктора, размазанный ровным слоем по всему приложению, прямо «гарантирует» обнаружение ошибки, да


Это не простой код вызова конструктора, вы получаете неизвестно откуда json, его нужно сначала проверить, что он вообще корректный, что там поля есть и у них формат нужный.
Если у меня, условно, падает ошибка на контроллере при обработке json, я по крайней мере знаю, что ошибка вероятно на клиенте.
Если у меня ошибка падает дальше, то, вероятно, ошибка где то в бизнес-логике.
По крайней мере явно сломанный json у меня не дойдёт до конструктора и не создаст неопределённого поведения (вы тестируете все возможные json, переданные в конструктор?)

Исключения круты тем, что обработчик может напечатать весь стек вызовов, который привел исключению

И толку от этого конечному пользователю? Вы будете обрабатывать все возможные варианты некорректного json? Отсутствие поля, неизвестное поле, некорректный тип значения? На каждую ошибку по исключению? Или как? Или пусть просто падает, потом разберёмся?

А если данные из БД брать, что передадите в конструктор? Ничего? Пусть в конструкторе класс сам лезет в БД?
Я не пойму, вы троллите что ли меня?

Не получилось вставить тег sarcasm :-(


Это не простой код вызова конструктора, вы получаете неизвестно откуда json, его нужно сначала проверить, что он вообще корректный, что там поля есть и у них формат нужный.

Ну так ComplexObject и проверит, в чем проблема-то. Откуда какому-то там контроллеру знать какие данные и в каком формате нужны объекту. А объект знает и может проверить


Если у меня, условно, падает ошибка на контроллере при обработке json, я по крайней мере знаю, что ошибка вероятно на клиенте.

Совершенно точно также вылетит исключение ParseException: missing } например, точно также вы будете знать что проблема в корявом JSON, разницы никакой


вы тестируете все возможные json, переданные в конструктор?

А вы тестируете все возможные JSON переданные в контроллер? В контроллере их протестировать, кстати, сложнее, из-за того что есть лишние переменные и классы, и там где-нибудь может быть ошибка


И толку от этого конечному пользователю?

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

Ну так ComplexObject и проверит, в чем проблема-то. Откуда какому-то там контроллеру знать какие данные и в каком формате нужны объекту. А объект знает и может проверить


Проблема в том, что мне как пользователю невозможно пользоваться вашим complexObject, потому что я понятия не имею, что он ожидает найти внутри jsonObject. Мне придётся лезть в конструктор, смотреть поля, которые он ищет и пытаться понять их тип.
Контроллер просто должен проверить, что там JSON корректный с точки зрения структуры JSON, что он хоть парсится.
Зачем complexObject добавлять зависимость от httpRequest или откуда вы там предлагали брать json?

Совершенно точно также вылетит исключение ParseException: missing } например, точно также вы будете знать что проблема в корявом JSON, разницы никакой

Большая разница. Логика обработки может быть совсем разной. Сообщения об ошибке могут быть разными.
Вместо «ParseException: missing }» я могу выкинуть ошибку, что некорректный json ещё на контроллере и дополнить текст ошибки.

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

Вы все ошибки пользователю показываете? Ну т.е. вообще все?

А вы тестируете все возможные JSON переданные в контроллер? В контроллере их протестировать, кстати, сложнее, из-за того что есть лишние переменные и классы, и там где-нибудь может быть ошибка

Контроллеру достаточно распарсить json и всё. Ему не нужно глубже идти.
Далее при получении полей я могу проверить все поля, которые мне нужны. Я не передам чёрный ящик конструктору если что то не так.
Если можно пожертвовать иммутабельностью, то предпочитаю использовать ломбоковский @Accessors(chain=true)
Sign up to leave a comment.

Articles

Change theme settings