Рубрики
Без рубрики

Чистое Блаженство С Чистыми функциями на Java

Как вы пишете простой, понятный и понятный Java-код? Чистые функции приходят на помощь!. Помеченный java, функциональный, программирование, рефакторинг.

Писать программное обеспечение сложно. Писать хорошее программное обеспечение сложнее. Писать хорошее, простое программное обеспечение – самое сложное. Писать хорошее, простое программное обеспечение в команде труднее всего… есть.

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

“Это пуристическая чушь!” ты говоришь. “Интеллектуальная мастурбация! Программное обеспечение должно быть смоделировано по образцу реальных изменяемых объектов!”

Я не говорю, что вы должны отправиться в поиск видения и вернуться в качестве хардкорного функционального программиста. Это было бы не очень продуктивно. Функциональное и объектно-ориентированное программирование могут значительно дополнять друг друга.

Я покажу тебе, как это делается.

Но сначала давайте перейдем к что и почему этого.

Что?

Чистая функция – это функция, которая:

  1. …всегда возвращает один и тот же результат при одних и тех же входных данных.
  2. …не имеет никаких побочных эффектов.

Проще говоря: при вызове чистой функции с вводом A , он всегда возвращает B , независимо от того, как часто, когда и где вы его вызываете. Кроме того, он не делает ничего другого .

В Java чистая функция может выглядеть так:

public static int sum(int a, int b) {
    return a + b;
}

Если a является 2 и б есть 3 , в результате получается всегда 5 , независимо от того, как часто или как быстро вы это называете, даже если это делается одновременно.

В качестве контрпримера это будет нечистая функция:

public static int sum(int a, int b) {
    return new Random().nextInt() + a + b;
}

Его выход может быть любым, независимо от ввода. Это нарушает первое правило. Если бы это было частью программы, о программе стало бы почти невозможно рассуждать.

Пример второго правила, функция, которая имеет побочные эффекты:

public static int sum(int a, int b) {
    writeSomethingToFile();
    return a + b;
}

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

Эти примеры могут показаться простыми и безобидными. Но нечистота быстро накапливается.

Почему?

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

Делая это, вы создаете мысленную модель программы. Вы пытаетесь вместить в свой разум все возможные потоки данных. Человеческий мозг не оптимизирован для этой задачи. После нескольких изменений состояния здесь, некоторых условностей там, вы начинаете немного туманиться.

Теперь у нас есть питательная среда для насекомых.

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

Как?

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

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

Теперь найти эту ошибку намного проще. Так же как и добавление новой классной функции. Чтобы сделать вещи более конкретными, вот некоторые рекомендации.

Раздел

Во-первых, разделите большие части по их функциональности. Подумайте о различных частях решения и о том, как они взаимосвязаны.

расставаться

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

Документ

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

Промойте и повторите.

Определение сделано является личным и может меняться с течением времени. Не переусердствуйте. Если это все еще недостаточно ясно, вы узнаете об этом позже. Если не во время проверки кода, то, возможно, при добавлении новой функции в проект.

Пока все идет хорошо, верно? Теория всегда звучит здорово. Но нам нужно что-то убедительное в виде практического примера.

Пример

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

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

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

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

Наивная Реализация

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

После выполнения некоторых строительных работ мы быстро добираемся до сути проблемы: определения того, следует ли отправлять уведомления для каждого автономного устройства. Звучит просто, не так ли?

public void run() {
    // @todo Send notifications for offline devices
}

Мы добавляем некоторые для циклов здесь.

for (Map.Entry offlineDevice : offlineDevices.entrySet()) {
    for (Duration threshold : thresholds) {
        // ...
    }
}

Некоторые утверждения if есть.

if (firstThresholdWasPassed) {
    // ...
}

Хорошо выглядишь!

Подождите, для последнего порога есть особый случай.

if (i == thresholds.size()) {
    // ...
}

О, и еще один для одного порога.

if (thresholds.size() == 1) {
    // ...
}

Дерьмо, мы забыли проверить, было ли уже отправлено уведомление для каждого порога. В 3-х местах.

if (!lastOfflineNotificationInstant.isPresent()) {
    // ...
}

А что, если оно было отправлено до того, как устройство отключилось?

if (Duration.between(disconnectInstant, lastOfflineNotificationInstant.get()).isNegative()) {
    // ...
}

О боже .

SonarLint пенится у рта, советуя нам уменьшить когнитивную сложность 69, где разрешено 15. Модульные тесты также становятся довольно сложными, поскольку приходится использовать внешние Часы еще что-то.

Мы не можем позволить нашим будущим “я” увидеть это, они убьют нас!

К счастью, у нас есть наша трехэтапная программа. Давайте начнем с 1.

Разделение по функциям

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

Мы могли бы визуализировать нашу проблему, нарисовав временную шкалу, начиная с момента отключения устройства:

На временной шкале мы можем построить различные пороговые значения:

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

Если мы действительно преодолеем порог, мы должны отправить уведомление и запомнить время, когда мы его отправили:

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

Только при прохождении другого порога мы можем отправить другое уведомление:

И так далее, и тому подобное.

Как мы можем выразить это в коде? Похоже, нам нужно знать:

  • Какой порог (если таковой имеется) мы преодолели последним.
  • Для какого порога (если таковой имеется) мы отправили последнее уведомление.

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

Теперь нам нужно посмотреть, сможем ли мы еще больше упростить наше решение, разбив его на составные части.

Разбивая Его На Составные Части

Два вышеприведенных утверждения выглядят ужасно знакомыми, не так ли? Это потому, что так оно и есть. Они оба определяют порог или ничего. Они оба нуждаются в аналогичном вводе:

  • В тот момент, когда устройство отключилось.
  • Порог(пороги).
  • Момент времени, который мы в настоящее время оцениваем.

Теперь, когда мы знаем вход и выход нашего устройства, мы можем определить его сигнатуру. Использование фантазии java.time API, мы можем выразить это как таковое:

Optional calculateLastPassedThreshold(Instant start, Instant current, Duration[] thresholds);

Теперь мы можем использовать эту функцию, чтобы получить оба необходимых нам порога. Я сказал “функция”? Давайте сделаем это чистой функцией : объявим ее статической и убедимся, что она детерминирована и не создает никаких побочных эффектов:

static Optional calculateLastPassedThreshold(Instant start, Instant current, List thresholds) {
    Duration timePassed = Duration.between(start, current);

    if (timePassed.compareTo(thresholds.get(0)) <= 0) {
        return Optional.empty();
    }

    for (int i = 0; i < thresholds.size(); i++) {
        if (timePassed.compareTo(thresholds.get(i)) <= 0) {
            return Optional.of(thresholds.get(i - 1));
        }
    }

    return Optional.of(thresholds.get(thresholds.size() - 1));
}

Там! Помимо его захватывающей чистоты, у него есть еще один плюс: Его можно очень легко протестировать на устройстве . Вам не нужны маски или внешние часы, чтобы протестировать эту часть кода.

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

Так что же остается?

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

static boolean shouldSendNotification(Instant jobStart, Instant deviceOffline, Instant lastNotification, List thresholds) {
    Optional lastPassedThreshold = calculateLastPassedThreshold(deviceOffline, jobStart, thresholds);

    if (!lastPassedThreshold.isPresent()) {
        return false;
    }

    if (lastNotification.isBefore(deviceOffline)) {
        return true;
    }

    Optional lastPassedThresholdNotifiedAbout = calculateLastPassedThreshold(deviceOffline, lastNotification, thresholds);

    return !lastPassedThreshold.equals(lastPassedThresholdNotifiedAbout);
}

Или, более кратко:

static boolean shouldSendNotification(Instant jobStart, Instant deviceOffline, Instant lastNotification, List thresholds) {
    Optional lastPassedThreshold = calculateLastPassedThreshold(deviceOffline, jobStart, thresholds);

    return lastPassedThreshold.isPresent() && (lastNotification.isBefore(deviceOffline) || !lastPassedThreshold.equals(calculateLastPassedThreshold(deviceOffline, lastNotification, thresholds)));
}

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

static boolean shouldSendNotification(Instant jobStart, Instant deviceOffline, List thresholds) {
    return calculateLastPassedThreshold(deviceOffline, jobStart, thresholds).isPresent();
}

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

public void run() {
    Instant jobStart = Instant.now();

    offlineDevices.entrySet().stream()
            .filter(offlineDevice -> pushNotificationService
                    .getLastOfflineNotificationInstant(offlineDevice.getKey())
                    .map(instant -> shouldSendNotification(jobStart, offlineDevice.getValue(), instant, thresholds))
                    .orElseGet(() -> shouldSendNotification(jobStart, offlineDevice.getValue(), thresholds))
            )
            .forEach(offlineDevice -> pushNotificationService.sendOfflineNotification(offlineDevice.getKey()));
}

Мы граничим с iamverysmart – территория здесь. Не всем нравится этот стиль кодирования, и, возможно, его сложнее анализировать, чем более традиционный стиль. Так что самое меньшее, что мы можем сделать, – это задокументировать наши подразделения.

Документирование Нашего Решения

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

Документация в нашем случае наиболее очевидно представлена в виде Javadoc, например:

/**
 * Checks whether a notification should be sent by determining which threshold has been passed last for the
 * calculated amount of time passed between the device going offline and the job running.
 *
 * @param jobStart         The instant the job calling this function was started.
 * @param deviceOffline    The instant the device went offline.
 * @param lastNotification The instant the last notification was sent.
 * @param thresholds       The list of notification thresholds.
 * @return True if the notification should be sent, false if not.
 */
static boolean shouldSendNotification(Instant jobStart, Instant deviceOffline, Instant lastNotification, List thresholds) {
    // ...
}

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

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

Запоздалые мысли

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

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

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

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

Прислушивайтесь к своим инстинктам, сообщайте о своих намерениях, слушайте коллег и учитесь друг у друга: создание отличного программного обеспечения – это постоянная (командная) работа.

Исходный код

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

Обратная связь

Я хотел бы услышать ваши отзывы в форме запросов на удаление , вопросов или комментариев! Здесь происходит несколько приятных дискуссий:

Или вы можете оставить свои комментарии ниже.

Оригинал: “https://dev.to/pietvandongen/pure-bliss-with-pure-functions-in-java-1mba”