Skip to content

Latest commit

 

History

History
650 lines (443 loc) · 34.6 KB

code_style.md

File metadata and controls

650 lines (443 loc) · 34.6 KB

Правила оформления кода

1.1. Введение

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

Хорошо написанный и оформленный код может рассказать о программе большую часть еще до ее запуска.

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

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

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

Преждевременные оптимизации - корень всех зол.

(c) Дональд Кнут.

1.2. Наименования классов и интерфейсов

1.2.1. Стиль

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

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

Например:

public class HelloWorld {}
public class Hasher {}
public class FileUtils {}

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

1.2.2. Рекомендации

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

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

public class XmlHelper {}
public final class FileUtils {}

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

При этом, если класс будет использоваться именно как Utils, то резонно вообще запретить ему участвовать в наследовании, объявив его final - законченным классом.

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

public interface Parser {
    // some code
}

public class JsonParser implements Parser {}

public class Criteria {
    // some code
}

public class SelectCriteria extends Criteria {}

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

public class Figure {}
public class Triangle extends Figure {}

Имена классов-исключений должны заканчиваться на Exception:

public class InvalidUserException extends Excepton {}
public class UserException extends RuntimeExcepton {}

Что в принципе согласуется с правилом о наименовании классов, участвующих в наследовании.

1.3. Наименования переменных

1.3.1. Стиль

При наименовании переменных в Java придерживаются стиля написания lowerCamelCase.

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

Например:

private int maxSize = 10;
private Object objectClassInPackage;

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

public static final int MAX_VALUE = 64; // Хорошо!
private static final String name = "NAME_1"; // Плохо!

1.3.2. Рекомендации

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

Название переменной обязано отражать то, зачем она нужна и какое состояние в себе хранит. Обязано отражать свою суть.

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

private int a = 1;

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

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

Если переменая объявляется в пределах метода или цикла, то давать подробнейшее имя не имеет смысла.

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

for(int indexOfElementInArray = 0; indexOfElementInArray < array.len; indexOfElementInArray++) {
    System.out.println(array[indexOfElementInArray]);
    //some logic
}

Более того, это вносит путаницу, хаос и катастрофически съедает все место на экране ноутбука.

Гораздо более лучше сократить это до:

for(int index = 0; index < array.len; index++) {
    System.out.println(array[index]);
    //some logic
}

или вообще заменить index на i:

for(int i = 0; index < array.len; i++) {
    System.out.println(array[i]);
    //some logic
}

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

Помните, чем короче область использования переменной - тем короче ее имя.

1.3.2.1. Свойства класса

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

public class ConfigViewModel {
    private Map<String, String> propertyMap = new HashMap<String, String>();
    private String filterConfig;
}

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

Про модификаторы доступа тесно связаны с полиморфизмом.

Поэтому вы довольно подробно должны описать такое свойство класса.

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

Например:

public class Сonnection {
    private int port;
    private String host;
}

Тут совершенно излишне уже писать, что это connectionPort, например. Так как из названия класса ясно, что свойство относится именно к connection.

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

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

1.3.3. Место объявления переменной

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

Давайте посмотрим на следующий пример:

public void print(int[] array) {
    int i = 0;
    // some code
    // and another code
    // still code

    while(i != array.size) {
        System.out.println(array[i]);
        i++;
    }
}

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

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

Поэтому старайтесь придерживаться правила: локальные переменные желательно объявлять ближе к месту использования.

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

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

Лучше объявить переменную там, где вы начинаете с ней работать, чем ближе - тем лучше.

for(int i = 0; index < array.len; i++) {
    System.out.println(array[i]);
    //some logic
}

Объявление переменной, хранящей индекс массива происходит непосредственно в месте использования.

Таким образом логика будет объявлена компактно и вникнуть в нее будет проще.

1.4. Наименования методов

1.4.1. Стиль

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

Например:

public int getPort();
public String toLowerCase();

1.4.2. Рекомендации

Так как метод - это некоторое поведение класса, то требования к именованию методов строже.

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

Помните, что метод обязан передавать суть своей работы в названии.

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

Например:

//название метода не отображает его суть - плохо
public void getHost() {
}

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

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

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

List<String> getHosts();

Плохим стилем считается использование символов подчеркивания, цифр и знаков пунктуации:

//подчеркивание лишнее - плохо
public void send_request() {}

//начинается с большой буквы - плохо
public void SendRequest() {}

//название метода не отображает его суть - плохо
public void methodOne() {}

Хорошим вариантом будет, например:

public boolean sendRequest() {}
public boolean sendRequest() {}
public boolean send() {}

В зависимости от того, как и что делает метод.

Имена методов, выполняющих чтение/изменение значений полей класса, должны начинаться со слов get и set.

Эти методы называются еще get-ы и set-ы, и тесно связаны с инкапсуляцией.

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

public String toString() {}

Имена методов, которые создают и возвращают созданный объект, желательно должны начинаться с create:

public Record createRecord() {}

Отдельной строкой стоит сказать про именование методов и переменных, возвращаемых boolean тип.

1.5. Boolean

Переменные типа booleanимеют только два состояния: true или false.

Чтобы подчеркнуть это при именовании таких переменных и методов, их возвращающих, имеет смысл использовать префиксы is или has.

boolean isInitialized;
boolean hasNext();

Благодаря этому, использование таких имен выглядит в коде довольно органично и понятно:

while(hasNext()) {
    // some code
}

Это улучшает читабельность вашего кода, при этом, если вы встречаете переменную с таким префиксом - в 99% случаев это будет именно boolean переменная.

1.6. Наименования пакетов

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

Например, program_installer.

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

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

Имена пакетов дают дополнительную информацию о том, что за классы лежат внутри и какие задачи они призваны решать.

Например:

package org.apache.kafka.streams.errors;

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

1.7. Общие рекомендации

1.7.1. Обрамление логических выражений фигурными скобками

Очень важным моментом стоит выделить еще и оформление логических выражений if/else.

Несмотря на то, что язык позволяет писать в виде:

if(num == 1) System.out.println("Hello");
else System.out.println("Hi");

Писать так я крайне не рекомендую.

Лично я предпочитаю вариант более явный:

if(num == 1) {
    System.out.println("Hello");
} else {
    System.out.println("Hi");
}

На мой взгляд, это понятнее, хотя это, конечно зависит от человека.

Но плюсы такой записи логических выражений на моих вкусовых предпочтениях не заканчиваются.

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

В качестве иллюстрации приведу такой пример:

if(num == 1) System.out.println("Hello");
else System.out.println("Hi");
System.out.println("Else case!")

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

Во избежание таких сюрпризов обрамляйте if-ы фигурными скобками {}.

1.7.2. Выделение логических блоков кода

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

Как например тут:

        Assert.assertEquals(0, postRepository.findAll().size());
        Assert.assertEquals(0, commentRepository.findAll().size());
        Assert.assertEquals(0, tagRepository.findAll().size());
        Post post = new Post("Title", "Description", "Content");
        Comment comment1 = new Comment("Content of Comment1");
        Comment comment2 = new Comment("Content of Comment2");
        Comment comment3 = new Comment("Content of Comment3");
        post.getComments().add(comment1);
        post.getComments().add(comment2);
        post.getComments().add(comment3);
        postRepository.save(post);
        Assert.assertEquals(1, postRepository.findAll().size());
        Assert.assertEquals(3, commentRepository.findAll().size());
        Assert.assertEquals(0, tagRepository.findAll().size());

        Optional<Post> persist = postRepository.getById(1L);
        Assert.assertTrue(persist.isPresent());
        Post savedPost = persist.get();
        Assert.assertEquals(3, savedPost.getComments().size());
        Assert.assertEquals(Sets.newHashSet(comment1, comment2, comment3), savedPost.getComments());

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

Явно выделяются блоки проверки репозиториев, инициализации тестовых данных, сохранения и новой проверки:

        Assert.assertEquals(0, postRepository.findAll().size());
        Assert.assertEquals(0, commentRepository.findAll().size());
        Assert.assertEquals(0, tagRepository.findAll().size());

        Post post = new Post("Title", "Description", "Content");
        Comment comment1 = new Comment("Content of Comment1");
        Comment comment2 = new Comment("Content of Comment2");
        Comment comment3 = new Comment("Content of Comment3");

        post.getComments().add(comment1);
        post.getComments().add(comment2);
        post.getComments().add(comment3);

        postRepository.save(post);

        Assert.assertEquals(1, postRepository.findAll().size());
        Assert.assertEquals(3, commentRepository.findAll().size());
        Assert.assertEquals(0, tagRepository.findAll().size());

        Optional<Post> persist = postRepository.getById(1L);
        Assert.assertTrue(persist.isPresent());

        Post savedPost = persist.get();

        Assert.assertEquals(3, savedPost.getComments().size());
        Assert.assertEquals(Sets.newHashSet(comment1, comment2, comment3), savedPost.getComments());

Всего несколько разделителей строк, а получается гораздо читабельнее.

1.7.3. Дробление кода

В попытке повысить читаемость кода не надо уходить в крайность.

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

Чаще всего это неоправданно и выглядит нелогично:

class Example {
    
    public List<String> forUpdate(List<Node> nodes) {
        return nodes.stream().filter(node -> isForUpdate(node)).map(Node::getName).collect(Collectors.toList());
    }

    // Метод больше нигде не используется, кроме как в forUpdate и закрыт как private
    private boolean isForUpdate(Node node) {
        return !node.getName().isEmpty() && node.getValue().equals("Up!"); 
    }
}

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

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

class NodeParser {
    public List<String> parse(List<Node> nodes) {
        return nodes.stream()
                .filter(node -> !node.getName().isEmpty() && node.getValue().equals("Up!"))
                .map(Node::getName)
                .collect(Collectors.toList());
    }
}

Поэтому старайтесь не дробить излишне свой код - так вы можете сделать его гораздо менее читабельным даже с хорошим неймингом.

1.8. Комментарии

Комментарии — это пояснения к исходному тексту программы, находящиеся непосредственно внутри комментируемого кода.

В Java существует две возможности добавить комменатрии к коду.

  1. Строчный комментарий, начинающийся с //.

    // Строчный
  2. Блочный комментарий.

    /*
    * Блочный комментарий
    */

В идеале надо стремиться к такому коду, который не нуждается в комментировании.

Однако такое не всегда выходит, в дополнении к этому, часто бывает так, что код выполняет запутанную бизнес-логику.

И такие места, я считаю, нужно комментировать и объяснять.

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

// url and password
private String url;
private String password;

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

public class DatabaseConfig {
    private String url;
    private String password;
} 

Код не нуждается в комментировании, более того, оно только усложнит читаемость.

Существует даже совет, что если у вас есть время на комментирование кода - потратьте его на рефакторинг.

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

Например, с помощью комментариев можно объяснить группировку объявлений полей:

    // left view for sources
    private VBox leftPane;
    private CheckBox selectAll;
    private ListView<SearchViewModel.SearchSource> lstCompanies;
    private Button initConfBtn;

    // right view for results
    private WebView resultView;

1.8.1. JavaDoc

Javadoc — стандарт для документирования классов Java.

Пример:

/**
 * Returns an Image object that can then be painted on the screen. 
 * The url argument must specify an absolute {@link URL}. The name
 * argument is a specifier that is relative to the url argument. 
 * <p>
 * This method always returns immediately, whether or not the 
 * image exists. When this applet attempts to draw the image on
 * the screen, the data will be loaded. The graphics primitives 
 * that draw the image will incrementally paint on the screen. 
 *
 * @param  url  an absolute URL giving the base location of the image
 * @param  name the location of the image, relative to the url argument
 * @return      the image at the specified URL
 * @see         Image
 */
 public Image getImage(URL url, String name) {
        try {
            return getImage(new URL(url, name));
        } catch (MalformedURLException e) {
            return null;
        }
 }

Все, что начинается с @ называется дескриптором.

Например, @see дескриптор - это ссылка на другое место в документации.

Подробнее про Javadoc

Заключение

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

Поэтому старайтесь максимально ответственно подходить к этому моменту, думайте над названиями классов, методов, пакетов и переменных.

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

И не забывайте про префиксы is, has при работе с boolean переменными и методами!

Если вы написали код, то отвечайте за то, что написали.

Если вы автор какого-то метода, то именно вы в ответе за поведение метода и за то, что его контракт выполняется верно.

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

Такие ситуации вызывают только раздражение по отношению к автору кода.

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