Как стать автором
Обновить
36
0
Евгений @reforms

Back-End Разработчик

Отправить сообщение
>> Эмулируем самую строгую типизацию
Здесь опечатка: type BoatId = number & { _type: 'BookId'};

Все мы поняли что там должно быть BoatId. Но это тот самый случай, когда могут 2 разных типа проскочить по одному имени :(

Кстати, как-то копался на github и newtype-ts
Подход с интерфейсным программированием не рассматривали?

Идея — pojo объекты и билдеры к ним — чистые контракты, а реализация достигается за счет кодогенерации.
В примере ниже рабочий эскиз, правда вместо кодогенерации реализация через интерфейсное программирование

package code_gen;

import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class JustForFun {

    public static void main(String[] args) {
        // Нюансы с конфигурацией, лямбдами не рассматриваются
        IRussianUserBuilder builder = ProxyBuilder.getBuilder(IRussianUserBuilder.class);
        builder.firstName("Sergey");
        builder.lastName("Egorov");
        builder.patronymic("Valeryevich");
        IRussianUser user = builder.build();
        System.out.println(user);               // >> {firstName=Sergey, lastName=Egorov, patronymic=Valeryevich}
        System.out.println(user.fullName());    // >> Sergey Egorov Valeryevich
    }
}

//----------------- ИДЕЯ - интерфейсное программирование ---------------------------------

// Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
interface IUser {

    String firstName();

    String lastName();

    /** Бизнес логика все еще возможна, но без состояния :) */
    default String fullName() {
        return new StringBuilder().append(firstName()).append(" ").append(lastName()).toString();
    }
}

//Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
interface IRussianUser extends IUser {

    String patronymic();

    /** Бизнес логика все еще возможна, но без состояния :) */
    @Override
    default String fullName() {
        return new StringBuilder().append(firstName()).append(" ").append(lastName()).append(" ").append(patronymic()).toString();
    }
}

//Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
interface IUserBuilder {

    IUserBuilder firstName(String firstName);

    IUserBuilder lastName(String secondName);

    IUser build();
}

//Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
interface IRussianUserBuilder extends IUserBuilder {

    IRussianUserBuilder patronymic(String patronymic);

    @Override
    IRussianUser build();
}

//---------------- java.lang.reflect.InvocationHandler (для Академических целей/для тестов) ---------------------------------

class ProxyBuilder implements InvocationHandler {

    private Map<String, Object> data = new HashMap<>();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO: Обработка методов equals/hashcode и прочее
        if ("toString".equals(method.getName())) {
            return data.toString();
        }
        if ("build".equals(method.getName()) && (args == null || args.length == 0)) {
            return ProxyPojo.getPojo(method.getReturnType(), data);
        }
        // TODO: можно добавить любые методы..
        // Реализация максимум упрощена
        return data.put(method.getName(), args[0]);
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBuilder(Class<T> builderType) {
        return (T) Proxy.newProxyInstance(builderType.getClassLoader(), new Class[] {builderType}, new ProxyBuilder());
    }
}

class ProxyPojo implements InvocationHandler {

    private final Map<String, Object> data;

    ProxyPojo(Map<String, Object> data) {
        this.data = data;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO: Обработка методов equals/hashcode и прочее
        if (method.isDefault()) {
            return invokeDefaultMethod(proxy, method, args);
        }
        if ("toString".equals(method.getName())) {
            return data.toString();
        }
        // Реализация максимум упрощена
        return data.get(method.getName());
    }

    @SuppressWarnings("unchecked")
    public static <T> T getPojo(Class<T> pojoType,  Map<String, Object> data) {
        return (T) Proxy.newProxyInstance(pojoType.getClassLoader(), new Class[] {pojoType}, new ProxyPojo(data));
    }

    //--------- поддержка default --------------------------------
    private static final Lookup TRUSTED_LOOKUP = getLookupField();

    private static Lookup getLookupField() {
        try {
            Field lookupField = Lookup.class.getDeclaredField("IMPL_LOOKUP");
            lookupField.setAccessible(true);
            return (Lookup) lookupField.get(null);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
        return TRUSTED_LOOKUP
                .in(method.getDeclaringClass())
                .unreflectSpecial(method, method.getDeclaringClass())
                .bindTo(proxy)
                .invokeWithArguments(args);
    }
}
Ваш пример есть же в статье?
Мне интересно, как другие люди подходят к этой проблеме и что вы думаете о компромиссах разных подходов!

Целый день думал, как еще можно решить пролему паттерна builder с сохранением цепочки вызовов. Получилость так:
RussianUser user = RussianUser.builder().apply(userBuilder -> userBuilder
        .firstName("Sergey")
        .lastName("Egorov"))
        .patronymic("Valeryevich")
        .build();

// где apply это
public static class Builder extends User.Builder {
...
        // можно прокинуть логику заполнения базовой сущности
        public Builder apply(Consumer<User.Builder> baseConfigure) {
            baseConfigure.accept(this);
            return this;
        }
...
}


Что думаете???

К сожалению в методе configure Вам пришлось отказаться от чейна, но Вы ведь так за него боролись?
Пользоваться обычными enum, а недостающие значения доставать через маппинг:

Многие выше предлагают такой подход, но мало кто сравнивает и анализирует плюсы и минусы.
Конечно, это тема для отдельной статьи, но если кратко пройтись по неудобству работы с мапой:
— не все можно сделать с помощью мапы, см TimeUnit, (коммент выше от AndreyRubankov)
— мапа, это все же не енум. Иногда требуется прикладная/функциональная завершенность: смотришь на объект и сразу все понимаешь.
— подход с мапой более многословен, из твоего же примера в проде обращение к title будет выглядеть так: StateScope.getState(State.NEW).title vs State.NEW.title
— работа в браузере и дебаг: встречаешь ты значение 77 при отладке, а шо це таке?
И это только навскидку.
У каждого инструмента есть свои преимущества и недостатки. Я в первую очередь попытался создать инструмент. А подходит он или нет решать Вам по конкретным обстоятельствам.

Если уж очень хочется именно enum-классы, то и они уже написаны до вас...

В статье упоминание об этом имеется.
Давайте разбираться вместе, по пунктам. Моя правда такова:
TypeScript тем и хорош что придает коду строгость, а вы это теряете повсеместно используя сырые типы (как any, или просто string)

1. Про какую повсеместность Вы говорите? Если об этапе использования указанного Enum, то как раз все и затевалось, чтобы конкретный класс енума, в примере, State, обладал всеми свойствами базового класса, но строгой типизацией под свои нужды. Проверить легко, в IDE набираем State+точка и смотрим что она предлагает.
2. Если речь идет о самой реализации декоратора/базового класса — Ваша правда, но дело поправимое: ждем commit от меня на гитхабе.
3. Почему string это сырой тип?
Вместо type-safety на этапе разработки по-прежнему работаете «в стиле JS» делая рантайм проверки (throw new Error).

В моем случае, проверки (throw new Error) и «стиль JS» никак не связаны. На это есть свои причины:
// Пример 1: лишний декоратор - ошибка при мерже веток после рефакторинга объекта State
@Enum("text") 
@Enum("code") 
class State extends EnumType<State> {
    // ...
}
// Пример 2: забыли декоратор - неудачная копипаста 
class State extends EnumType<State> {
    // ...
}
// Пример 3 (js-code): создание экземпляра объекта вне области класса - защита от дурака
// функция конструктор класса State
var StateType = require("moduleName/state").State;
var deleteState = new StateType("Удален", Color.BLUE);

Я бы так сказал: все идет от декоратора, который пока не умеет менять тип исходного объекта.
Надеюсь дело поправимое Class Decorator Mutation
… а здесь может сложится ошибочное мнение о высоком уровне type-safety

Да. Я и команда знаем это. Но дух самой идеи декоратора таков, что легко можно накосячить. И не важно как и где вы их применяете. Примеры 1 и 2 тому подтверждение, или вот Enum(«incorrect_field_name»)
Если не получается задизайнить решение на TS с высоким уровнем type-safety, то может быть вообще не стоит это делать на TS

Здесь не вижу проблемы. Даже если не удастся мне прийти в решении к type-safety, есть тесты в конце концов. В целом предложение странное. Многие вещи сейчас не типизированы так как хочется. Например, связка vue-class-component и миксины, элементы самого vue (кто в теме $refs.myComponent) и многое другое. Я не в коем случае не говорю, что это хорошо, но однозначно не является причиной, чтобы отказываться от такого мощного языка как TypeScript
Я без примеров с Вашей стороны, не смогу показать разницы.
И такое тоже есть. Пока правда в одном единственном месте. На этот случай написан, свой Renderer.
Есть такое, отрицать не буду. Но как показала практика, ущерб от этого смешивания — невелик или вообще отсутствует, а польза есть. Всего один класс EnumRenderer для отображения всех объектов такого типа.
Вопрос не в том, что не подошел enum от TypeScript, мы также им пользуемся и он весьма полезен:
  • для него есть поддержка в switch/case выражениях
  • его удобно использовать в протокольных объектах

Вопрос в том, что стандартный enum нельзя расширить до требуемой функциональности. Нам, например, нужно все 'наши' перечисления отображать в таблице тем цветом, который указан в конструкторе:
@Enum("text")
export class State extends EnumType<State>() {

    static readonly NEW = new State("Новый", Color.BLACK);
    static readonly ACTIVE = new State("Действующий", Color.GREEN);
    static readonly BLOCKED = new State("Blocked", Color.RED);

    private constructor(public text: string, public color:string) {
        super();
    }
}

Color — не единственный атрибут, который требуется нашей бизнес логике. Как правило это пара: code и text.
9. Самая главная новость — ерунда :)

А это как посмотреть. Я вот чувствую сколько прод решений отъедет из-за этого :)

Вы просили реальные примеры, правда они больше подходят под слово кеширование. Был не очень адекватный клиент, который выгружал себе на страницу 50000 записей, и начинал сортировку по дате. Разумеется жаловался на то, что как-то медленно все работает. Компаратор по датам у нас был реализован относительно просто, но не оптимально. Что мы сделали: даты (как строки) преобразовывали в числа и сравнивали числа между собой, чтобы не делать постоянное преобразование из строки в число мы сделали кеш значений строка->число. Также использовали тот факт, что даты(без времени) имеют ограниченный набор реальных значений: сегодня, вчера, позавчера, максимум пол года назад, т.е кеш строк был примерно размером не более 365 записей всего. Увеличили скорость сортировки в 16 раз

Несколько мыслей по статье:
1) Некоторые моменты приходится перечитывать несколько раз, иногда совсем не понятно, складывается впечатление, что читатель должен быть разработчиком WAF, чтобы понять суть написанного
2) По поводу "… перспективой развития WAF’ов..." и понимания предметной области, здесь все очень непросто, как бы не получилось так, что WAF будет проникать в саму бизнес логику приложения, через SDK, либы или иные техники, тем самым подсаживая бекэнд, на конкретного Вендера WAF.
3) Как разработчик клиент серверного приложения могу сказать, да WAF нужен, но ещё больше нужен цикл разработки, предусматривающий разработку защищённого ПО, в котором уделяется должное внимание статической и динамической защите кода, прикладной защите выраженной в логике самого ПО, корректная настройка всех компонент системы(веб серверов, БД, сетевой инфраструктуры, файловой системы, окружения и прочие). Уже молчу о кадрах должной квалификации. Без всего выше указанного, WAF может стать пустыми воротами на футбольном поле, ну Вы понимаете.

И ещё, сфера деятельности — репетитор отсутствует, на сколько это правильно?

А у меня вопросы про визитку Карл Маркс на красном фоне:
1) номер телефона указан со скобками — это норм?
2) почему философ написан с большой буквы, а все остальное с маленькой, если везде точка?

Плюс за статью.
Так как критика приветствуется, имею следующее:
посмотрел код в пакете com.irvil.textclassifier.dao.jdbc; Сложилось впечатление, что код написан небрежно, где-то sql ошибки глотаются, где-то на консоль пишутся, con.commit есть, а con.rollback нет (правда может в SQLite jdbc драйвере это поведение по умолчанию), результаты операций не проверяются, с con.setAutoCommit нужно работать аккуратнее и много чего ещё. В общем, если проект будете развивать то рекомендую такие огрехи зачищать сразу.

В Вашем случае больше подойдёт не ОРМ-ориентированный Фреймворк, а SQL-ориентированный. Не сочтите за рекламу, попробуйте ClickHouse подружить с RefOrms Фреймворком, он как раз для таких задач подходит идеально.

Спасибо за статью. Есть пару вопросов и замечаний:
1) последний пример последнее условие !result.length === 0 не очень читаем
2) на сколько плохо сделать метод в моделе model.hasUserWithId? И сразу связанный с ним — id = 0 не допускается?
3) пример хоть и не Ваш, но раз приводите — почему if else везде вместо ПРОСТО if если каждый блок с прерыванием return?
4) что совсем не понятно — это пример с firstName/lastName и accounts — почему не выделить логику в отдельный метод и хорошенько задокументировать, мол, (1) должны быть указаны имя/фамилия, (2) введён пароль и (3) что ещё

Спасибо за статью.
Хоть выше уже многие и написали, скажу ещё раз:
1) Ваши примеры не совсем подходят к патерну visitor, так как последний чаще используют для обхода структур, имеющие вложенные структуры, те в свою очередь содержат ещё структуры и т.д.
2) Конкретно в Вашем случае, уместны 2 решения — (1) или фигуры содержат методы для их отрисовки, вывода на консоль и расчёт площади в виде реализаций т.е. по 3 метода в каждой из фигур, что иногда позволяет сокрыть данные внутри фигуры не давая к ним доступ даже на чтение (2) или добавить тип фигуры а по этому типу делать связывание с нужным действием, причём замечу, что реализации действий могут быть сделаны по одному экземпляру, быть потокобезопасными и определять функциональные слои (слой отрисовки данных, слой описания данных, слой расчета площади и иных метрик фигур) Вашего фреймворка/библиотеки. И (1) и (2) делают Ваш Фреймворк расширяемым, гибким и предсказуемым, 2ой вариант ещё легко позволит определять действие по умолчанию для конкретного слоя — что значит не обязательность в реализации всех функциональных слоёв для новой фигуры, хотя этот факт может быть и минусом.

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Дата рождения
Зарегистрирован
Активность