Как стать автором
Обновить

Комментарии 41

НЛО прилетело и опубликовало эту надпись здесь

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


Но в большинстве случаев, иммутабельность и чистые функции (и не важно, ООП у нас или нет), позволяют избежать многих непонятных и скрытых багов. Преждевременная оптимизация — ну, вы поняли. Нашли бутылочное горлышко — декомпозировали, мемоизировали, на крайний случай вынесли в явно-названную мутабельную функцию.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

В плюсах расстояния хранятся в 64-хбитных переменных.
Вы тестировали функцию на строках длиннее 4-х миллиардов символов?
Надо проверить, что скорость не увеличится от перехода на 32 бита, если это возможно.

В хаскеле раздрядность Int тоже 64, так что все честно.


P.S. по крайней мере у меня такая разрядность...


GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> maxBound :: Int
9223372036854775807
Prelude> import Data.Bits
Prelude Data.Bits> bitSizeMaybe (0 :: Int)
Just 64
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
ФП, ООП или что-то другое? В первую очередь я не задумывался о самом стиле, а именно о возможности, обезопасить код не комментарием, а как-то иначе.

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

То что это один и тот же мутабельный объект совсем не значит что у него на протяжении всей его жизни один и тот же тип. У реального дома со сменой состоянием меняется и его тип. Сначала это тип Котлован, потом Фундамент, потом ПервыйЭтажГотов. Причем в общем случае эти типы между собой никак не связаны. Тип Котлован вообще может не иметь общих методов с ТолькоЧерновойРемонтДом.
Просто многие современные языки программирования не позволяют менять тип объекта после создания кроме как ручным приведением между типами с общим родительским типом, поэтому объект проще пересоздать. Да и нагляднее это. Но это особенность языков программирования и сложности выводов типов в компиляторах. Тот же TypeScript умеет в вывод конкретного типа из объединения на основе значения одного из полей. Но опять же может быть нагляднее просто пересоздать объект.
НЛО прилетело и опубликовало эту надпись здесь
Менять тип в рантайме — это рушит весь смысл типизации.

Я не очень понял о чем Вы, возможно что Вы и правы.
По-моему же, менять тип в рантайме плохо вот по какой причине.
Продолжая пример стройки, у Вас есть тип Дом, равный алгебраической сумме всех типов состояний этого дома.
Дом = Котлован | Фундамент | ПервыйЭтажГотов | ... | ТолькоЧерновойРемонтДом | ЧистовойРемонтДом;
Код, работающий только с типом Фундамент, не скомпилируется с типом Котлован, пока вы не вызовите на своем Котловане метод залитьФундамент.
Я вижу проблему в том после вызова заливкиКотлована, где-то осталась ссылка на дом, когда он еще был котлованом, и кто-то может попробовать вызвать метод.рытьЛопатой у залитого фундамента. Но строго говоря если бы у нас на протяжении всего процесса был бы один тип Дом, вам все равно бы пришлось проверять в методе рытьЛопатой текущий статус дома.
То есть менять тип в рантайме переменной плохо, потому же почему и просто лишний раз ее менять плохо — кто-то мог с этой переменной работать и не закончить.
НЛО прилетело и опубликовало эту надпись здесь
кто-то мог с этой переменной работать и не закончить
Так в этом жеж и есть неправильное применение ООП — менять поля без проверки этого действия на допустимость. А сама эта проверка — и есть смысл ООП. Все остальные методы/функции должны стучаться к переменной только через нее (и неважно, где они находятся — снаружи или внутри класса).
А сама эта проверка — и есть смысл ООП. Все остальные методы/функции должны стучаться к переменной только через нее (и неважно, где они находятся — снаружи или внутри класса

Спасибо, положу себе в копилку самых "весёлых" определений ООП. Интересно, как вы себе другие парадигмы тогда представляете. Без ООП пользователям сразу write-доступ в базу с платежами даётся?


P.s. На самом деле грустно это. Сегодня я уже упоминал buzzword-driven подход.

Без ООП пользователям сразу write-доступ в базу с платежами даётся?
Доступ к базе — очень похоже на изменение состояния, да. Поэтому обычно с базами данных работают в ооп-режиме. Так удобнее.

Интересно, как вы себе другие парадигмы тогда представляете.
Как будто других парадигм много…
НЛО прилетело и опубликовало эту надпись здесь
В JS когда вы меняете тип переменной в рантайме, сразу слетает вся JIT-оптимизация, и производительность падает в разы.

В JS в рантайме значительной части типов вообще нет. Остаются только классы.
Если Вы так хотите обезопаситься от установки кровли на голый котлован — можно сделать прокси-объект, проверяя все комбинации параметров в одном месте.

Я все-таки очень вырожденный пример дал. Во фронтенде например такая сложная обработка состояния, если вообще потребуется будет внутри какого-нибудь Redux/Mobx/Vuex еще чего-то, которые и будут выступать прокси объектом. При этом входной код будет только получать на вход объект с как минимум одинм обязательным поле.состояниеСтроительстваДома и кучей возможных полей. Подписанные на этот объект компоненты будут просто получать уведомление о том, что объект поменялся. Как именно он должен поменяться будет определяться менеджером состояний, который и будет выступать фасадом.
Менять тип в рантайме — это рушит весь смысл типизации.

Типизация не означает проверки только во время компиляции и не означает, что в каждом конкретном месте должен использоваться только один тип. Типизация это проверки типов. Даже если тип меняется в рантайме, с типизацией он будет оставаться в пределах допустимых типов.

В ООП, вызывать методы ради сайдэффектов это обычная практика, особенно в высокоуровневом коде. Однако, в целом, это классическая с ситуация, если важна последовательность или целостность инициализации объекта, то вы используете фабрику или билдер, в зависимости от того, где находятся нужные для работы данные. Билдер даже можно передавать как аргумент, и возвращать из метода.


Вариант с бросанием исключения тоже приемлем. Не все договорённости можно выразить в коде, поэтому создание исключения с информативным сообщением и которое сможет возникать только во время разработки — не такой плохой способ, ибо Fail-fast.

Вариант с бросанием исключения тоже приемлем

В данном случае еще именованием можно обратить внимание.
Например переименовал метод у availabilityFiller с fill на что-то fillOnlyWithProductWithPrice. Такое длинное имя скорее бросится в глаза при рефакторинге и на него обратят внимание.
Еще есть вариант пересохранять объект product под другим именем
    priceFiller.fill(product, prices); 
    Product productWithPrice = product;
    availabilityFiller.fill(productWithPrice, availabilities);
    Product productWithPriceAndAvailability = productWithPrice;
    imageFiller.fill(productWithPriceAndAvailability, images);

Даже если компилятор не выкинет эти переменные потом, новых объектов мы не создадим.
Тут главный вопрос в том, почему «availabilityFiller» требует заполненных цен. Лучшим выходом, было бы избавиться от этой зависимости, переписав «availabilityFiller», а возможно и «Product».
Если же это невозможно, или слишком сложно, то самый простой выход — избавиться от «availabilityFiller», заменив его на «priceAndAvailabilityFiller», который требует одновременной передачи, как «Prices», так и «Availabilities». Ещё, понадобится метод: «Prices getPrices(Product product)».
Недостаток тут только один, и он вполне очевиден. В сценариях, когда «Prices» и «Availabilities» заполняются раздельно, «Prices» будет заполняться повторно. Но не зная всех деталей (как цены хранятся в продукте, как часто происходят подобные операции и т.д.), нельзя сказать, станет ли это узким местом.
Получается, что код выглядит рабочим, даже после перестановки вызовов, а результат оказывается неожиданным.

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

Придётся приписывать всякие флаги или прочее, чтобы отличить «нет данных» от «даже не запрашивали».

Можно сделать объект Prices, если «даже не запрашивали» он будет null, а если «нет данных», тогда будет подкласс EmptyPrices или просто цены с пустым контентом, не null. И тогда не надо даже писать Objects.requireNonNull(product.getPrices) — второй метод и так упадёт с NullPointerException, если не вызвать первый.

Ну во-первых, ваши геттеры в классе ничего не делают, кроме того что разрушают инкапсуляцию. И поэтому они просто ненужны. Просто сделайте поля public. Бдет тоже самое но кратко и понятно.

Во-вторых, ваши Builder классы тоже не нужны, потому что по сути они вызывают конструктор, попутно излишне перекладывя переменные туда-сюда, делают лишнюю работу и делают её зря (точно также как геттеры). Надо просто сделать несколько конструкторов (да да, так можно!) с разным набором аргументов. и создавать ваш объект через new, как это и должно быть.

Итого:
public class Application_2 {

    public static void main(String[] args) {
        Product product = new Product(20, 1000, "available", List.of("url1, url2"));

        System.out.println(product.id);
        System.out.println(product.availability);
        System.out.println(product.price);
        System.out.println(product.images);
    }

    static class Product {
        public final int price;
        public final long id;
        public final String availability;
        public final List<String> images;

        public Product(int price, long id) {
            this(price, id, null);
        }

        public Product(int price, long id, String availability) {
            this(price, id, availability, null);
        }

        public Product(int price, long id, String availability, List<String> images) {
            this.price = price;
            this.id = id;
            this.availability = price > 0 && availability != null ? availability : "sold out";
            this.images = images;
        }
}


Гораздо короче и понятнее, а главное тотже самый функционал. Просто без лишней ненужной фигни, а так как задумано изначально в языке.
Ну а если вы прям хотите задавать аргументы в любом порядке, можно сделать конструктор (о боже, четвертый!) из Map например

public class Application_2 {

    public static void main(String[] args) {
        Product product = new Product(1000, 20, "available", List.of("url1, url2"));
        Product product2 = new Product(new TreeMap<String, Object>() {{
            put("id", new Integer(20); put("price", new Integer(1000)); 
            put("availability", "available");
            put("images", List.of("url1, url2"));
        }};

        System.out.println(product.id);
        System.out.println(product.availability);
        System.out.println(product.price);
        System.out.println(product.images);
    }

    static class Product {
        public final int price;
        public final long id;
        public final String availability;
        public final List<String> images;

        public Product(int price, long id) {
            this(price, id, null);
        }

        public Product(int price, long id, String availability) {
            this(price, id, availability, null);
        }

        public Product(final Map<String, Object> fields) {
            if (fields.containsKey("availability") && !fields.containsKey("price"))
                throw new RuntimeException("product price is mandatory");
            this((Integer)fields.get("price"), (Long)fields.get("id"), 
                (String)fields.get("availability"), (List<String>)fields.get("images"));
        }

        public Product(int price, long id, String availability, List<String> images) {
            this.price = price;
            this.id = id;
            this.availability = price > 0 && availability != null ? availability : "sold out";
            this.images = images;
        }
}


Гораздо более объектно оринтированно чем ваши билдеры
Спасибо за предложенный вариант.
Map наверное самый гибкий и удобный объект. Но опять таки слишком гибкий. Опечатки в ключах, захламление не используемыми данными и прочее. Тогда уж лучше взять Map<«Enum», Object>, чтобы хоть как-то обозначить границы и возможности.

Опять таки пример с билдером — всего лишь один из возможных путей решения. В реальном проекте кончено же, всё не просто проставляется. Те же цены, суммируются, сравниваются и прочее.

А вот из-за открытых полей с некоторыми коллегами можно и поссориться.
Билдеры в некоторых случаях действительно нужны. Например, StringBuilder действительно делает полезные вещи, он mutable и через это можно уменьшить расход памяти и циклов процессора. Но в данном случае билдер не делает ничего полезного, потому что объект просто инкапсулирует данные в конструкторе (что вообщем правильно и так и надо делать)

Насчет открытых полей, я как бы тоже не очень хорошо отношусь к открытым полям, но ведь бесполезные геттеры фактически и делают закрытые поля открытыми, геттеры которые ничего не делают мне кажется ещё хуже
Тоже верно. Но с другой стороны, в этом и инкапсуляция. Кто-то вызывает геттер, не зная, как вырешивается значение. Таким образом, я могу позже добавить логику в сам геттер, не переписавая сами вызовы, коих к тому времени уже может быть много.
В данном конкретном случае геттер ничего не вырешивает, а просто возвращает значение, фактически разрушая инкапсуляцию, внутренние данные объекта становятся доступны извне «как есть»

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

Ну а в случае использования билдера и WrongPriceException вы получаете не инициализированный до конца билдер. И в чем разница.

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

Может, но не производит, в изначальном коде автора.
Начинающий программист удивляется встрече с объективной реальностью:
Мне не нравится, что ни одна функция не написана в стиле ФП

Потом вот так:
не люблю тесты

Или вот так:
Испльзуя mock'и, я сумел протестировать код

И наконец вот такой перл:
С другой стороны, такой подход заставляет писать столько кода, что от него сразу хочется отказаться


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

Собственно указание на ФП в самом начале уже прямым текстом говорит об отсутствии опыта работы с императивными подходами, иначе фразы типа «мне не нравится без ФП» просто не встречались бы. Но даже если забыть про ФП, то заявление «мне не нравится», не подкреплённое аргументами, выглядит как типичное недовольство не разбирающегося в проблеме, но считающего, что всё, что он видит на работе ему должно нравиться.

В тексте программы, конечно же, используется новомодный синтаксис, скрывающий от разработчика тип используемого объекта. Это всё привычно для разработчиков на JavaScript или на чём-то другом плохо типизированном, но в Java это выглядит страшно. Хотя, к сожалению, такую возможность всё же включили в какую-то из последних версий языка. Но тем не менее, привычка писать в таком стиле очередной раз показывает — перед нами самый что ни на есть начинающий разработчик на Java, скорее всего пришедший из мира редко используемых в промышленной разработке языков, что в свою очередь намекает на отсутствие опыта в промышленной разработке вообще.

Ну и собственно по смыслу проблемы. Автор видит простенькую неидеальность и пытается сделать код «совершенным». Это тоже типичная проблема новичка. И решает её новичок в таком виде, про который он сам говорит — «такой подход заставляет писать столько кода, что от него сразу хочется отказаться». Ну что-ж, полезная самокритика. Но вот результат всё же не впечатляет.

Почему результат не впечатляет? Очень просто — автор привык оптимизировать по сложно формализуемому критерию исключительно учебные кейсы для ФП, а потому автоматически применяет этот навык в более серьёзной разработке. В результате у него получается, как он сам отметил, нагромождение всего на свете.

Как исправить ситуацию? Всегда самый эффективный способ улучшения находится на уровень, а то и два, выше примитивов непосредственно кода. То есть алгоритм решения проблемы никак не зависит от любого языка программирования, но привычка новичков уделять внимание «красоте кода» (в их очень субъективном понимании) заставляет их строить то самое нагромождение, от которого сам же автор в конце концов плюётся.

Не надо переходить на уровень кода, когда ваш алгоритм глупый. Просто не надо.

Почему алгоритм глупый? Потому что автор даже не пытается понять, а что реально происходит в программе. Он видит только код и его субъективную «красоту». А что этот код делает?

Можно предположить, что объект типа Product заполняется данными. И точно так же можно предположить, что объект типа Product служит источником для заполнения каких-то дополнительных объектов. Это два архитектурно принципиально разных подхода. И решение усмотренной автором небольшой неидеальности должно в первую очередь зависеть именно от того, в каком направлении движутся данные. А на более высоком уровне всё зависит от того, зачем вообще эти данные куда-то движутся. В результате скорее всего весь рассматриваемый автором метод вообще не нужен, например потому, что итак уже содержащий данные объект типа Product (во втором варианте), можно направить в то место, где эти данные каким-то образом потребляются. Но повторюсь — автор не видит ситуацию «в большом», то есть вместо понимания ненужности условного дома, он старательно штукатурит его стены, что является самым главным признаком начинающего разработчика. Никакие зазубренные знания синтаксиса не помогут вам сделать глупость умной. Потому что глупость находится на несколько уровней выше в иерархии абстракций, по сравнению с синтаксисом языка. И вот это понимание затмевает зубрёжка синтаксиса, желание видеть очень условно понимаемую «красоту», основой для которой являются примеры из ФП, часто не имеющие к реальной жизни практически никакого отношения (да, всё те же сортировки, перестановки, операции над структурами). Погружаясь во все эти сортировки человек видит условную «красоту» низкоуровневых алгоритмов, а собственно зачем эти все алгоритмы применяются — ему совершенно по барабану. Ну и в результате имеем глупость.

Поэтому, дети (начинающие программисты) — зрите в корень! Не ведитесь на гламурный синтаксис и мнение функциональных программистов о «некрасивости» кода без функциональной шелухи. Думайте о главном. И даже больше — думайте, что такое главное. И может быть в итоге вы станете реально хорошими (полезными для проекта) разработчиками, а не поставщиками гламурной чешуи под рекламным называнием «красивый код».
НЛО прилетело и опубликовало эту надпись здесь
Спасибо за такой развёрнутый ответ.
Интересное, что у Вас сложилось именно такое мнение.
Наверное, мне надо было выражаться яснее.

Я не думаю, что кто-то из нас не сталкивался с императивным стилем программирования, т.к. именно с него обычно и начинается обучение.
Но Вы правы, не стоит слепо идти за каким-то стилем. Как я и сам уже сказал, главное, чтобы код работал.
Про тесты могу только уточнить, что я не писал, что не люблю тесты в целом. Лишь те, в которых идёт слишком сильная привязка на внутрености кода. Мне больше нравятся тесты наподобие «если я передаю А, то мне вернётся Б», нежели те, в которых проверяется, а был ли вызван такой-то сервис, с помощью того же Mockito.verify (необходимость таких тестов я не обсуждаю).
К слову о Mockito: я имел ввиду, что с помощью этого прекрасного инструмента можно написать тесты для функций, основанных на интрефейсах, не реализируя сами интрефейсы.
Против императивного программирования или любого другого абсолютно ничего не имею.
Скорее всего просто разница во вкусах и предпочтениях заставила меня, заострить внимание на этом коде. Совершенного кода просто не бывает.

Кстати, к var тоже сначала отнёсся скептически, когда их только аннонсировали. Сейчас понял, что они иногда неплохо облегчают жизнь. А тип подскажет IDE.
>> Мне больше нравятся тесты наподобие «если я передаю А, то мне вернётся Б», нежели те, в которых проверяется, а был ли вызван такой-то сервис, с помощью того же Mockito.verify (необходимость таких тестов я не обсуждаю).

Тесты, как и вообще всё программирование, это инструмент. Может нравится молоток? С точки зрения эстетики, наверное, но оценивать его нужно с точки зрения способности забивать гвозди. И если гвозди забиваются нормально — молоток хороший. Здесь нет места эмоциям. Просто работа молотком. И всё. Хотя по началу (по молодости) из-за угла может показаться некий ореол романтичности, но это именно по началу.

Вообще, проверка «был ли вызван сервис» бывает разная. Например — нужно уведомлять внешнего клиента о произошедшем событии, тогда очевидно, что это важная часть функционала и вызов сервиса является критичным. Но бывает и по другому — внешний сервис просто даёт какую-то информацию, а потом, обработав эту информацию, процесс выдаёт какой-то свой результат. В таком случае проверка вызова сервиса бессмысленна, потому что нам важен результат, а не какие-то промежуточные действия. Если результат не сходится с ожиданиями — это критично, а был ли там в середине какой-то вызов — совершенно не важно, потому что архитектуру могут переделать буквально через месяц (и такое бывает) и все тесты «про вызов» пойдут псу под хвост. А вот если проверяли результат — переделывайте свою архитектуру сколько хотите, тест всё равно будет полезным.

Мдауш...


given(priceFillerMock.fill(eq(productMock), any())).willReturn(productWithPricesMock);
given(availabilityFillerMock.fill(eq(productMockWithPrices), any())).willReturn(productMockWithAvailabilities);
given(imageFillerMock.fill(eq(productMockWithAvailabilities), any())).willReturn(productMockWithImages);

var result = productFiller.fill(productMock, p1, p2, p3);

assertThat("unexpected return value", result, is(productMockWithImages));

Что правда кто-то пишет такой код в реальности? В проектах, которые в проде обслуживают реальных потребителей?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории