Ошибки начинающих разработчиков майнкрафт плагинов и как их не допускать

Руководство Ошибки начинающих разработчиков майнкрафт плагинов и как их не допускать

Поддерживаемые версии
  1. Отсутствует
Рад вновь сесть за микрофон.

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

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


⦁ Пункт 1 - Некорректная работа с конфигурацией.

Это наверное является основной ошибкой начинающих разработчиков в принципе.

В этом пункте можно выделить 2 побочных подпункта.

А) Получение значений конфигурации в рантайме

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

Пример плохого кода:
Java:
Bukkit.getScheduller().runTaskTimer(plugin, () -> {
    Player player = Bukkit.getPlayer(nickname);
    player.sendMessage(plugin.getConfig().getString("message"));
}, 200L, 200L);

Почему это плохо?
По тому, что каждый раз, когда мы обращаемся к конфигу таким образом - он получает данные из LinkedHashMap (смотрите org.bukkit.configuration.MemorySection), что накладывает дополнительный процент нагрузки на сервер.
ВАЖНО СКАЗАТЬ, что даже если нагрузка от этого может казаться (хотя фактически она и есть) незначительной и менять код нет необходимости - это является крайне неоправданной затратой ресурсов, а также в случае, если вашему плагину будет необходимо получать значения из конфига более часто - это уже может вылиться из незначительной в весьма большую нагрузку.

Решение:
Вместо того, чтобы постоянно обращаться к конфигу и получать оттуда значения - мы можем записать это значение заранее и после использовать его в дальнейшем.
Java:
String message = plugin.getConfig().getString("message");
Bukkit.getScheduller().runTaskTimer(plugin, () -> {
    Player player = Bukkit.getPlayer(nickname);
    player.sendMessage(message);
}, 200L, 200L);

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

Пример до:
Java:
// Сетап
public String usage, nopermission, cooldown;
public void setupConfigValues(FileConfiguration config) {
        usage = config.getString("usage"));
        nopermission = Util.colorize(config.getString("nopermission"));
        cooldown = Util.colorize(config.getString("cooldown"));
    }
// много кода спустя
player.sendMessage(usage);

С использованием record:
Java:
public ConfigValues configValues;
public void setupConfigValues(FileConfiguration config) {
        configValues = new ConfigValues(
                Util.colorize(config.getString("usage")),
                Util.colorize(config.getString("nopermission")),
                Util.colorize(config.getString("cooldown")),
        );
    }

public record ConfigValues(
          String usage,
          String nopermission,
          String cooldown
    ) {}

// много кода спустя
player.sendMessage(configValues.usage())

Думаю я дал достаточно информации

Б) Неэффективное получение данных из конфига.

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

Ситуация:
У нас есть следующая конфигурация:
YAML:
section:
  value-one: "valueone"
  value-two: "valuetwo"
  value-three: "valuethree"
И нам необходимо взять оттуда значения

Пример плохого кода:
Java:
String one = config.getString("section.value-one");
String two = config.getString("section.value-two");
String three = config.getString("section.value-three");

Почему это плохо?
Ответ прост. Поскольку мы уже знаем, что при получении данных из конфига таким образом у нас идёт их получение из хешмапы - нам стоит посмотреть ещё на один аспект. Сначала этот метод получит секцию конфигурации 'section' и только потом получит значение оттуда. Таким образом каждый раз хеш высчитывается дважды, так ещё и итерация к этому прибавляется.
Итого суммарно 6 раз мы вычисляем хеш, что, согласитесь, не очень, особенно если у вас большой конфиг со множеством значений, где кол-во вычислений может быть больше в разы.

Решение:
Вместо того, чтобы получать необходимые нам значения из конфига вышеописанным образом - мы сначала будем получать секцию конфигурации, откуда уже будем брать наши значения.
Java:
ConfigurationSection section = configuration.getConfigurationSection("section");
String one = section.getString("value-one");
String two = section.getString("value-two");
String two = section.getString("value-three");
Этот подход будет в разы эффективнее, чем предыдущий.


⦁ Пункт 2 - Страх перед локальными переменными/константами и прочем.

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

И в целом назначения у них обоих относительно понятные - читаемость, удобство, НО, есть и у них такая черта - ваш код может стать производительнее за счёт их использования.

Ситуация 1:
Нам необходимо сделать метод для колоризации сообщений в hex формате.

Пример плохого кода:
Java:
public static String hex(String message) {
    Pattern pattern = Pattern.compile("&#([A-Fa-f0-9]{6})");
    Matcher matcher = pattern.matcher(message);
    StringBuffer buffer = new StringBuffer();
    while (matcher.find()) {
      String color = matcher.group(1);
      StringBuilder replacement = new StringBuilder("§x");
      for (char c : color.toCharArray())
        replacement.append('§').append(c);
      matcher.appendReplacement(buffer, replacement.toString());
    }
    matcher.appendTail(buffer);
    return buffer.toString().replace("&", "§");
}
Почему это плохо?
Чтож, на самом деле тут много чего, однако нас интересует следующая строчка:
Pattern pattern = Pattern.compile("&#([A-Fa-f0-9]{6})");
Как не сложно догадаться - она компилирует паттерн, который используется для хекса. Однако нюанс заключается в том, что компилирование паттернов - не быстрое дело, а происходит это при КАЖДОМ вызове метода. Таким образом, если при помощи данного метода его автор решит перекрасить 200 сообщений - паттерн будет скомпилирован 200 раз!

Решение:
Вместо того, чтобы компилировать паттерн в методе каждый раз когда он вызывается - мы можем вынести паттерн во вне, сделав его константой:
Java:
private static final Pattern PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})"); // обычно константы пишутся в апперкейсе
public static String hex(String message) {
    Matcher matcher = PATTERN.matcher(message);
    StringBuffer buffer = new StringBuffer();
    while (matcher.find()) {
      String color = matcher.group(1);
      StringBuilder replacement = new StringBuilder("§x");
      for (char c : color.toCharArray())
        replacement.append('§').append(c);
      matcher.appendReplacement(buffer, replacement.toString());
    }
    matcher.appendTail(buffer);
    return buffer.toString().replace('&', '§');
}

Примечание: НЕ КОПИРУЙТЕ ЭТОТ КОД В КАЧЕСТВЕ МЕТОДА КОЛОРИЗАЦИИ ДЛЯ СВОИХ ПЛАГИНОВ! Он неэффективен. Мы доберёмся до оптимального варианта позже.

Ситуация 2:
Нам необходимо отправить оповещение всем игрокам на сервере.

Пример плохого кода:
Java:
public void sendBroadcast(Player sender, String broadcastFormat, String message) {
    for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
        onlinePlayer.sendMessage(broadcastFormat.replace("{sender}", sender.getName()).replace("{message}", message));
    }
}

Почему это плохо?
Как и раньше - проблема остаётся. В данном примере внутри цикла for игроку отправляется сообщение, в котором заменяется {sender} на ник игрока, а также {message} на сообщение.
Суть проблемы заключается в том, что при каждой отправке сообщения в нём будут заменяться его части, что опять же - весьма неэффективно. Особенно в больших циклах.

Решение:
Перед циком мы можем создать локальную переменную в виде строки, в которой заранее заменим весь необходимый текст на наш, таким образом сделав это единожды, избежав многократных операций внутри цикла.
Java:
public void sendBroadcast(Player sender, String broadcastFormat, String message) {
    String finalBroadcast = broadcastFormat.replace("{sender}", sender.getName()).replace("{message}", message);
    for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
        onlinePlayer.sendMessage(finalBroadcast);
    }
}


⦁ Пункт 3 - Проблемы с коллекциями.

В данном пункте также можно выделить несколько подпунктов.

А) Некорректное использование коллекций как таковое.

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

На данный момент нас интересует List и Set.

List - упорядоченный список из всех элементов, которые будут туда занесены.
Set - просто список элментов без какой либо упорядоченности (на самом деле она там есть, но она там своя, не суть) и без возможности наличия там дубликатов

List нам нужно использовать в случаях когда:
1) Нам нужен порядок элементов.
2) Нам нужно получать значения по индексу (по тому, в каком месте в листе находится определенный элемент).
3) У нас могут быть дубликаты.

Set нам нужно использовать в случаях когда:
1) Нам не важна последовательность элементов.
2) Когда нам не нужно получать значения по индексу.
3) Когда нам не нужны дубликаты.

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

Ситуация:
Нам необходимо сделать список запрещённых блоков, которые игроки не смогут ставить

Пример плохого кода:
Java:
// Берём и создаём условный лист, взяв его из конфига
List<String> blocks = getConfig().getStringList("banned-blocks");

@EventHandler
public void onPlace(BlockPlaceEvent event) {
    Material material = event.getBlock().getType();
    if (blocks.contains(material.toString())) {
        event.setCancelled(true);
    }
}
Почему это плохо?
Ответ заключается в принципе работе метода contains у List-а. Чтобы узнать, содержится ли указанный материал в списке - метод будет перебирать его весь, от начала и до конца, пока не найдёт необходимый элемент в листе и не вернёт ответ.
И если лист содержит в себе множество элементов - то для проверки на то, содержится в нём какой либо элемент может занимать много времени.

Решение:
Использовать сет, в котором алгоритм, проверяющий на то, находится ли в нём элемент работает иначе и быстрее.
Java:
// Берём и создаём условный лист, взяв его из конфига
Set<String> blocks = new HashSet<>(getConfig().getStringList("banned-blocks"));

@EventHandler
public void onPlace(BlockPlaceEvent event) {
    Material material = event.getBlock().getType();
    if (blocks.contains(material.toString())) {
        event.setCancelled(true);
    }
}

Б) Утечки памяти из за невнимательности или незнания.

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

Без лишних слов я скажу главное, что вам нужно будет НАВСЕГДА запомнить.
Никогда не храните в коллекции объект Player.

Почему? Ответ прост. Каждый раз при перезаходе в игру объект игрока (тоесть этот самый Player) создаётся по новой. Таким образом если вы попытаетесь сохранить игрока в лист и потом сравнить игрока в листе с тем же казалось бы игроком, но после перезахода - они будут разными.
В чём же проблема? Откуда берется утечка? Всё просто. В джаве у нас есть сборщик мусора, что наверное для вас не секрет. И когда игрок перезаходит в обычных условиях - старый объект игрока удаляется сборщиком мусора через время, поскольку ссылок на него (опять же в обычных условиях) более не остаётся. Однако, если вдруг данный объект игрока занесён в какую либо коллекцию - ссылка на него остаётся и сборщик мусора игнорирует данный объект при сборке по очевидным причинам (не ну мало ли он ещё нужен кому-то). Отсюда и возникает утечка, когда один игрок, перезайдя и будучи занесённым в коллекцию 10 раз создаст 10 объектов игрока соответственно, которые в свою очередь весят немало.

Как избежать такой проблемы? Всё просто - вместо того, чтобы заносить в коллекцию игроков как Player вам необходимо заносить туда либо ник игрока, либо его UUID, а после получать игрока по необходимости через Bukkit#getPlayer

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

В) Неиспользование лучших коллекций.

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

В первую очередь очень хочу сказать про EnumSet и EnumMap.
Если вам необходимо создать список к примеру материалов - не поленитесь и создайте не обычный HashSet<>() с енамами, а специализированный EnumSet.

Пример:
Java:
private Set<Material> createMaterialSet(List<String> stringList) {
        Set<Material> materialSet = EnumSet.noneOf(Material.class);
        for (String s : stringList) {
            Material mat = Material.matchMaterial(s);
            if (mat != null) {
                materialSet.add(mat);
            }
        }
        return materialSet;
    }

Теперь же затронем сторонние библиотеки. В майнкрафте, в самом ядре (а на данный момент я имею в виду Paper, поскольку считаю, что ничто иное просто не должно использоваться в современном мире) присутствуют такие библиотеки как
You must be logged in to see this link. и You must be logged in to see this link., при помощи которых вы можете весьма значительно улучшить свой код.

Так в Google Guava нас интересуют такие коллекции как ImmutableList и ImmutableSet.
В первую очередь их преимущество в том, что в них нельзя ничего положить или удалить после того как они были созданы, что делает их (условно честно говоря) безопасными, если вы опасаетесь за то, что что-то может переписать имеющиеся в них элементы.
Однако прежде всего - они полезны в случае, когда нам нужно проходиться через них циклом (for). В таком случае они показывают себя в разы быстрее, а потому в случае, если вам необходима коллекция, которую вы создадите единожды, не будете изменять и вам необходимо проходиться по ней циклом - советую использовать одну из этих.

В fastutil же представлены коллекции направленные на более быструю и эффективрую работу с примитивами, такими как int, double, long и т.п.
Если вам необходимо создать List Set или Map, который будет внутри себя содержать Integer и т.п. обёртки, которые могли бы быть примитивами - рекомендую использовать соответствующие IntList, IntSet или Int2ObjectMap-ы.



⦁ Пункт 4 - Асинхрон/конкурентность и что там должно и не должно быть.

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

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

Вот те случаи когда асинхрон нужен:
1) При работе с базами данных, поскольку обработка запросов занимает много времени. Это безусловный принцип работы с базами, который вам необходимо знать.
2) При работе с вебхуками (подключения к сайтам), по той же причине, которая была с базами данных - скорость. В ряде популярных плагинов проверка обновлений не была вынесена из основного потока, что приводило к невероятно медленному запуску.
3) Частая работа с файлами (сохранение в первую очередь).
4) Переодически выполняемые задачи по типу: Регулярной отправки сообщений, тайтлов, звуков, партиклов. Не смотря на то, что это занимает достаточно мало времени - если у нас есть возможность делать это вне основного потока - то почему бы и нет? В пример можно привести те же самые авто-сообщения.

Теперь, когда мы это поняли - стоит перейти к конкурентности.

Тут будет затронут только 1 её компонент, а именно - "потокобезопасность". В кавычках, поскольку не все не знают, что это такое и зачем оно есть.
Коротко, в чём проблема - при работе с чем-то из разных потоках может возникнуть так называемая гонка данных, когда один поток делает с объектом одно, а другой поток другое. Пример - один поток добавляет в наш лист элемент, а другой удаляет в это же время, в следствие чего возникает ошибка, кто что и куда должен добавить и что удалить.
И чтобы вот таких ситуаций не было в java существует такая штука, как потокобезопасные коллекции, в числе которых:
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
(и ещё ряд других)
Однако! Их необходимо использовать только в том случае, когда у вас гарантировано идут одновременные операции с ними из разных потоков.
Их категорически не рекомендуется использовать в иных случаях, когда у вас такое не происходит, как то любят делать множество современных... ИИшек.
Пример правильного использования:
Java:
private final ConcurrentMap<String, Integer> perPlayerBalance = new ConcurrentHashMap<>();
// Одно действие в одном потоке
public void addBalanceAsync(String playerName, int amount) {
    Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
        perPlayerBalance.compute(playerName, (key, value) -> value == null ? amount : value + amount);
    });
}
// Другое действие которое уже в другом потоке
public void removeBalanceAsync(String playerName, int amount) {
    Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
        perPlayerBalance.compute(playerName, (key, value) -> {
            if (value == null) return null;
            int newValue = value - amount;
            return newValue <= 0 ? null : newValue;
        });
    });
}
Пример неправильного использования:
Java:
// В данном случае ConcurrentMap не нужен
private final ConcurrentMap<String, Integer> playerDeaths = new ConcurrentHashMap<>();

@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
    Player player = event.getEntity();
    String name = player.getName();
    playerDeaths.compute(name, (key, value) -> value == null ? 1 : value + 1); // Делается в главном потоке (из ивента смерти)
}

@EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) {
    Player player = event.getPlayer();
    String name = player.getName();
    int deaths = playerDeaths.getOrDefault(name, 0);
    player.sendMessage("Вы умирали " + deaths + " раз(а)"); // Делается в главном потоке (из ивента респавна)
}


⦁ Пункт 5 - Принцип DRY и ради бога - следуйте ему.

И вновь восславим яндекс.
DRY (Dont Repeat Yorself) - это принцип разработки программного обеспечения, который гласит:
"Не повторяй себя".

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

Пример плохого кода:
Java:
@EventHandler
public void onItemDrop(PlayerDropItemEvent e) {
  if (!event.getPlayer().isOp() && !event.getPlayer().hasPermission("bypass.perm")) {
      event.setCancelled(true);
  }
}

@EventHandler
public void onItemPickup(PlayerPickupItemEvent e) {
  if (!event.getPlayer().isOp() && !event.getPlayer().hasPermission("bypass.perm")) {
      event.setCancelled(true);
  }
}

Исправленный код:
Java:
@EventHandler
public void onItemDrop(PlayerDropItemEvent e) {
  if (!canPickup(event.getPlayer())) {
      event.setCancelled(true);
  }
}

@EventHandler
public void onItemPickup(PlayerPickupItemEvent e) {
  if (!canPickup(event.getPlayer())) {
      event.setCancelled(true);
  }
}

provate boolean canPickup(Player player) {
  return player.isOp() || player.hasPermission("bypass.perm")
}

Либо, доже лучший аналог:
Java:
@EventHandler
public void onItemDrop(PlayerDropItemEvent e) {
      processCheck(e.getPlayer(), e);
}

@EventHandler
public void onItemPickup(PlayerPickupItemEvent e) {
      processCheck(e.getPlayer(), e);
}

private void processCheck(Player p, Cancellable e) {
  if (p.isOp() || p.hasPermission("bypass.perm")) {
        e.setCancelled(true);
    }
}


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


⦁ Пункт 6 - Мелкие придирки к бесящим вещам.

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

А) Синглтоны будь они не ладны.

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

Пример того, как делать не надо:
Java:
public class Main implements JavaPlugin {

    private static Main instance;

    public void onEnable() {
          instance = this;
          getCommand("command").setExecutor(new CommandClass());
    }

    public static Main getInstance() {
        return this.instance;
    }
}
Java:
public class CommandClass implements CommandExecutor {
   public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
      sender.sendMessage(Main.getInstance().getConfig().getString("message"));
   }
}

Почему это плохо?
Чтож, как уже говорилось выше - принцип DRY. Писать каждый раз Main.getInstance() - неэффективно, делает код более громоздким и неудобным в чтении.

Что лучше использовать:
Java:
public class Main implements JavaPlugin {

    public void onEnable() {
        getCommand("command").setExecutor(new CommandClass(this)); // Создаём новый командный класс вот тут
    }

}
Java:
public class CommandClass implements CommandExecutor {
   private final Main plugin;
   public CommandClass(Main plugin) {
       this.plugin = plugin; // инициализируем в конструкторе наш плагин, вместо того, чтобы получать его
   }
   public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
      sender.sendMessage(plugin.getConfig().getString("message")); // используем тут то, что мы инициализировали в конструкторе
   }
}

Б) HEX цвета и их парсинг.

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

А взамен я могу предложить следующий код:
Java:
private static final Pattern HEX_PATTERN = Pattern.compile("&#([a-fA-F\\d]{6})");

    public static String colorize(String message) {
        if (message == null || message.isEmpty()) {
            return message;
        }
        final Matcher matcher = HEX_PATTERN.matcher(message);
        final StringBuilder builder = new StringBuilder(message.length() + 32);
        while (matcher.find()) {
            String group = matcher.group(1);
            matcher.appendReplacement(builder,
                    COLOR_CHAR + "x" +
                            COLOR_CHAR + group.charAt(0) +
                            COLOR_CHAR + group.charAt(1) +
                            COLOR_CHAR + group.charAt(2) +
                            COLOR_CHAR + group.charAt(3) +
                            COLOR_CHAR + group.charAt(4) +
                            COLOR_CHAR + group.charAt(5));
        }
        message = matcher.appendTail(builder).toString();
        return translateAlternateColorCodes('&', message);
    }

    public static final char COLOR_CHAR = '§';

    public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) {
        final char[] b = textToTranslate.toCharArray();

        for (int i = 0, length = b.length - 1; i < length; ++i) {
            if (b[i] == altColorChar && isValidColorCharacter(b[i + 1])) {
                b[i++] = COLOR_CHAR;
                b[i] |= 0x20;
            }
        }

        return new String(b);
    }

 
private static boolean isValidColorCharacter(char c) {
        return switch (c) {
            case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D',
                 'E', 'F', 'r', 'R', 'k', 'K', 'l', 'L', 'm', 'M', 'n', 'N', 'o', 'O', 'x', 'X' -> true;
            default -> false;
        };
    }

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

В) И ещё немножко про DRY и локальные переменные.

Я слишком часто видел то, что будет показано в примере ниже, а по этому я был просто обязан об этом сказать.

Пример плохого кода:
Java:
getCommand("command").setExecutor(new CommandClassAndTabCompleteClassAlso(this));
getCommand("command").setTabCompleter(new CommandClassAndTabCompleteClassAlso(this));

Почему это плохо?
Чтож, во первых мы 2жды дублируем код и 2жды создаём новый объект класса. Казалось бы зачем?

Исправленный код:
Java:
PluginCommand command = getCommand("command");
CommandClassAndTabCompleteClassAlso commandClass = new CommandClassAndTabCompleteClassAlso(this);
command.setExecutor(commandClass);
command.setTabCompleter(commandClass);

В) Игнорирование игронирования.

На самом деле достаточно глупая ошибка, но тем не менее.
Если ваш ивент не подразумевает то, что его действия должны выполняться при его отмене - используйте @EventHandler(ignoreCancelled = true), во избежание лишних действий в неподходящий момент.
Никому не нужно, чтобы плагин на, ну допустим, разлетающиеся во все стороны партиклы при ударе игрока другим игроком работал в зоне, где не работает PVP. Всегда держите это в уме.

Д) System.out.println

Нет серьёзно. Даже ядра новых версий будут оповещать вас о том, что использование сисаута - это не то, что нужно делать.

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

Что использовать вместо? Безусловно логгер плагина через plugin.getLogger().info, который будет выводить имя плагина перед сообщением лога. Удобно? Вполне.
Также стоит упомянуть и plugin.getSlf4Logger и его дополненный аналог из современных версий Paper plugin.getComponentLogger, которые могут выступить лучшими альтернативами, а в особенности ComponentLogger, который полностью совместим с новой системой цветов, чего не скажешь о простом getLogger-е.

Е) Отсутствие исходников

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

Ж) Излишнее следование подсказкам от Idea. (А ё кстати не существует хихи хаха)

Многие начинающие разработчики за частую весьма бездумно следуют подсказкам, которые им высвечивает intellij idea. В целом, эти подсказки полезны и им нужно следовать, однако вы должны знать, что вы делаете.

Примеры плохого кода:
Java:
// 1 плохой пример
String[] stringArray = Objects.requireNonNull(config.getString("value")).split(";");
// 2 плохой пример
ConfigurationSection section = config.getConfigurationSection("section");
assert allowedBookCharsNotify != null;
boolean bool = section.getBoolean("bool");

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

И) Написание всего кода в едином классе или использование огромных методов.

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

К) Не добавление таймаута к URL запросам

Одна из критических ошибок, что может привести к катастрофическим проблемам.
Запомните
Если вы делаете URL запрос куда либо - вы обязаны добавить ему таймаут.
По умолчанию открытый UrlConnection через openConnection и подключённый через connect не имеет каких либо таймаутов.
Это означает, что в теории он может банально зависнуть в состоянии суперпозиции, в лучшем случае - заняв поток собой до момента рестарта, а в худшем - убить весь сервер, если запрос был сделан из главного потока.

Пример того, как нужно делать:
Java:
// Предыдущий код
URL url = new URL("ваша.ссылка");
URLConnection connection = url.openConnection();
connection.setConnectTimeout(5000); // Устанавливаем максимальное время для коннекта
connection.setReadTimeout(10000); // Устанавливаем максимальное время для чтения
connection.connect;
// ваш остальной код


⦁ Заключение

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

Если же у вас есть подобные примеры стандартных ошибок, которые совершают новички - милости прошу в обсуждения, добавлю сюда наиболее важное из предложенного.
Автор
Overwrite
Просмотры
8 578
Первый выпуск
Обновление
Оценка
4.86 звёзд 14 оценок

Другие ресурсы пользователя Overwrite

Поделиться ресурсом

Последние обновления

  1. Дополнения

    Дополнен раздел про асинхрон - добавлена информация про потокобезопасные коллеции (и где их не...
  2. Дополнения

    Пункт и) в придирках с информацией о том, что не нужно писать всё в главном классе
  3. Дополнения

    Пункт Ж в придирках - про подсказки от idea Небольшие исправления ошибок в форматировании кода...

Последние рецензии

*(и мне всё равно, что тут нет Markdown)*

# Мишань, всё херня, давай по-новой.

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

При этом советы из статьи не то, что бы плохие. Но подача очень хромает.

## 1. Некорректная работа с конфигурацией
### а) Код из примера не идентичен по функционалу
Переменная `message` из "правильного" кода будет взята единожды и переиспользоваться даже после выполнения перезагрузки конфига `plugin.reloadConfig()` - то есть игрок будет всё время видеть одно и то же сообщение. Если действительно хотим показать что-то правильное, нужно расширять скоуп. Что-нибудь вроде:
```java
public class MyClass {
private String message;

public MyClass(Plugin plugin) {
reload(plugin);
Bukkit.getScheduller().runTaskTimer(plugin, () -> {
Player player = Bukkit.getPlayer(nickname);
player.sendMessage(message);
}, 200L, 200L);
}

public void reload(Plugin plugin) {
this.message = plugin.getConfig().getString("message");
}
}
```
В следствии, при вызове `MyClass#reload` у нас наконец будет обновляться соответствующее сообщение.

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

### б) Неэффективность преувеличена
Когда как для часто-вызываемых блоков кода рассказ про производительность имеет смысл, здесь его уже почти нет. Единоразовая перезагрузка конфига не может уронить ТПС сервера, если только происходит что-то совсем не так.

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

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

## 2. Страх перед локальными переменными/константами и прочим
Тут уже получше...
### Ситуация 1) ...но у новичка нет контекста, что должно быть константой
Например, есть потоко-зависимые классы, которые таковыми не кажутся. Пускай какой-нибудь `SimpleDateFormat` уже считается крайне устаревшим, но вот какой-нибудь `new DecimalFormat("#.##")` выглядит как хороший кандидат... а потом оказывается, что у него есть внутренний буфер, который может упороться при выполнении в потоках.

Также можно предположить, что при работе с SQL какой-нибудь `PreparedStatement` можно сохранить - ведь даже название на это будто намекает - но пойдёшь лесом на втором же прогоне.

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

### Ситуация 2) ...но пример слишком конкретный
Как и в первом, рассматривается очень определённый кейс, из чего можно только подчеркнуть, что общие операция со строками следует делать сразу. По факту же, стоит расширить описание - мол, это касается всех подобных кейсов. Собственно, у таких значений даже название есть - инвариант цикла - т.е. то, что не меняется из итерации в итерацию. Тут же следует сделать оговорку про мутабельность:
```java
List<String> bestPeople = new ArrayList<>(getAdminNames());
for (Player player : Bukkit.getOnlinePlayers()) {
bestPeople.add(player.getName());
player.sendMessage("В список лучших людей на сервере входят: " + bestPeople); // С каждой итерацией будут добавляться новые игроки
}
```

## 3. Проблемы с коллекциями
Тоже неплохо...
### А) ...но стоит подправить пример
Тут чем-то похоже на проблему из самого первого пункта. Я *очень* часто видел, как люди создают новый `HashSet` прямо перед работой с ним. Следует помнить, что создание такого объекта, как `HashSet`, тоже небесплатно, и, по возможности, это следует делать заранее.
### Б) ...но стоит, по-моему, расширить
Хранение `Player` - далеко не единственная причина утечек. Одна из самых популярных у новичков - хранение мутабельных объектов в `HashSet` и в ключах `HashMap`. Коллекция запоминает старый хэш, а поиск/удаление идёт изменённому - объект становится недоступен, но остаётся в памяти. Такое часто видел с объектами типа `Location`.

Конечно, если всё делать аккуратно, как и в случае в `Player`, проблем можно избежать. Но лучше просто избегать таких ситуаций. Например, с локацией можно оформить свой рекорд:
```java
public record MyLocation(String world, int x, int y, int z) {
public MyLocation(Location loc) {
this(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
}
```
### В) ...и всё
Могу лишь придраться, что для `ImmutableList`/`ImmutableSet` в ванильной джаве уже есть свои аналоги.

## 4. Асинхрон/конкурентность и что там должно и не должно быть
Очень странные примеры. Из них кажется, будто для работы с `ConcurrentHashMap` нужно обязательно работать в асинхроне - в реальности же, в однопоточном режиме производительность `CHM` сопоставима со стандартным `HashMap`. Лучше уж сделать по примеру про использование `HashMap` vs `CHM`, и по примеру для вызова долгих или тяжёлых операций, вроде обращения по сети, нормальная компутация, или работа с файловой системой.

## 5. Принцип DRY и ради бога - следуйте ему.
Ну тут правда. Хотелось бы только блоки логики побольше, чтоб реально показать разницу.

## 6. Мелкие придирки к бесящим вещам
Тут просто оставлю пару комментариев.
### А) Dependency Injection - круто
Единственное - про громоздкость можно поспорить. При правильной архитектуре и это решаемо, но это уже совсем другая история.
### Б) Хе-хе-хе, мой старый код
Наверно, один из лучших вариантов для баланса между производительностью и поддерживаемостью кода. Но примерно на том же уровне можно сделать всё и без регулярок, и одним методом.

# Моё заключение
1. Самое важное - нужны правки примеров (а ещё `getScheduler()` с одной `l`).
2. Некоторые темы плохо раскрыты.
3. Слишком большой упор на производительность - пишешь верно, но новичка так можно довести до того, что тот постоянно будет заниматься преждевременной ~~эяк~~оптимизацией.
4. Если реально заниматься оптимизацией - без тестов многое подтвердить просто невозможно, хорошо бы чего про проверки через JMH и spark тогда.
5. Как финальный совет, можно было бы попросить новичков забить на Spigot API и перейти на Paper API. Потихоньку оно будет становиться всё более несовместимым, а фич там намного больше.
отличная статья, для новичков - самое то. Единственный минус статьи - отсутствие котлина
Отличаня статья для новичков! Хоть я и сам знал все эти принципы, думаю, что тем кто только начал создавать плагины нужно выучить эти правила как молитву.
почему бы не 5 звезд, видно человек выделил время, постарался, респект
Мне понравилось, подробно и по фактам расписано все, для себя взял пару хороших привычек, будем привыкать XD. А так я думал все пишут так (правильно), а исходя из статьи странных кадров достаточно много :D
прикольно объяснил, пойду переписывать теперь свой rProfiles
Эммм нуууу крутооо пасибааа (надо 50 символов набрать просто), а если серьёзно, всё вполне качественно и хорошо написано.
я тоже тебе поставлю, че уж
(просто текст, чтобы набрать 50 символов)
Overwrite
Overwrite
легчайшая для величайшего
от величайшему равного
Хороший гайд для новичков в сфере разработки майнкрафт плагинов, таких гайдов сейчас мало)
Видно, автор постарался над данной статьёй и потратил больше количество времени, сил на его создание и оформление. Очень рад, что есть креативные люди на форуме. Да и плагины тоже интересные
Назад
Сверху Снизу