- Правила оформления кода
Программист большую часть времени занимается не написанием кода, а чтением и изучением уже написанной кодовой базы. Поэтому оформление кода это не менее важная часть процесса его написания.
Хорошо написанный и оформленный код может рассказать о программе большую часть еще до ее запуска.
Также можно столкнуться с работами, где люди пришедшие в Java
с других языков, тянут свои привычки.
Но надо понимать, что и в Java
есть свои правила, которым надо следовать.
Поэтому следование некоторым простым правилам является обязательным требованием к разработчикам.
Зачастую, даже если есть выбор: написать чуть более производительный код или гораздо более читаемый, то стоит сделать выбор в сторону более читаемого.
Преждевременные оптимизации - корень всех зол.
(c) Дональд Кнут.
Классы принято называть именами существительными, начинающимися с прописной буквы.
При наименовании классов и интерфейсов в Java
придерживаются стиля написания CamelCase.
Это стиль написания составных слов, при которому несколько слов пишутся слитно без пробелов, при этом каждое слово внутри фразы пишется с прописной буквы.
Например:
public class HelloWorld {}
public class Hasher {}
public class FileUtils {}
Запомните, что имя класса, перечисления или интерфейса всегда пишется с заглавной буквы!
Помните, что имя класса - это то, с чего начинается использование любого кода в 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 {}
Что в принципе согласуется с правилом о наименовании классов, участвующих в наследовании.
При наименовании переменных в Java
придерживаются стиля написания lowerCamelCase.
Данный стиль написания говорит о том, что имена переменных (если это не константы) пишутся слитно, где все слова, кроме первого, начинаются с прописной буквы.
Например:
private int maxSize = 10;
private Object objectClassInPackage;
Имена констант же принято писать заглавными буквами, разделяя слова знаком нижнего подчеркивания _
.
public static final int MAX_VALUE = 64; // Хорошо!
private static final String name = "NAME_1"; // Плохо!
Переменные предназначены для хранения состояния. Они могут быть локальными, принадлежать объекту класса или быть статическими, т.е принадлежать непосредственно классу.
Название переменной обязано отражать то, зачем она нужна и какое состояние в себе хранит. Обязано отражать свою суть.
Это значит, что у вас не должно быть кода, похожего на подобный:
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
в целом довольно многословный язык, добавлять еще свои пять копеек в эту копилку не стоит, тем более, что использовать эту переменную вы будете только в области цикла.
Помните, чем короче область использования переменной - тем короче ее имя.
Совсем другое дело, если ваша переменная является свойством класса:
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
.
Обязательно задумывайтесь о названии переменных - это если не половина, то треть обеспечения будущей поддержки, развития и переиспользования вашего кода.
Теперь, когда мы разобрали как выбрать имя для переменной, пришло время поговорить о том, как влияет место объявления на читаемость кода.
То, где вы объявили переменную также может повлиять на читаемость вашего кода.
Давайте посмотрим на следующий пример:
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
}
Объявление переменной, хранящей индекс массива происходит непосредственно в месте использования.
Таким образом логика будет объявлена компактно и вникнуть в нее будет проще.
Названия методов должны быть глаголами, первая буква должна быть строчной, первые буквы внутренних слов — заглавные.
При наименовании методов в Java
придерживаются стиля написания lowerCamelCase.
Например:
public int getPort();
public String toLowerCase();
Так как метод - это некоторое поведение класса, то требования к именованию методов строже.
Из того, что метод - это поведение объекта, следует, что в названии метода нужно использовать глаголы, которые как можно более точно и полно описывают то, что выполняет метод.
Помните, что метод обязан передавать суть своей работы в названии.
Худшее, что можно сделать при задании имени метода - это дать имя, которое не соответствует действию.
Например:
//название метода не отображает его суть - плохо
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
тип.
Переменные типа boolean
имеют только два состояния: true
или false
.
Чтобы подчеркнуть это при именовании таких переменных и методов, их возвращающих, имеет смысл использовать префиксы is
или has
.
boolean isInitialized;
boolean hasNext();
Благодаря этому, использование таких имен выглядит в коде довольно органично и понятно:
while(hasNext()) {
// some code
}
Это улучшает читабельность вашего кода, при этом, если вы встречаете переменную с таким префиксом - в 99% случаев это будет именно boolean
переменная.
Имена пакетов и подпакетов должны быть существительными в единственном числе в нижнем регистре, слова разделяются подчёркиваниями.
Например, program_installer
.
Помните, что пакеты - это дополнительная возможность сгруппировать ваш код, поэтому пакеты должны содержать только те классы, которые логически могут быть там.
Если у вас существует пакет parser
, то объявлять там класс Entity
, отвечающий за некоторую модель в вашем приложении - не логично. Модель принадлежит проекту, а значит не может быть в пакете, где классы отвечают только за парсинг.
Имена пакетов дают дополнительную информацию о том, что за классы лежат внутри и какие задачи они призваны решать.
Например:
package org.apache.kafka.streams.errors;
Содержит все кастомные классы-исключения, которые могут произойти при работе с kafka-стримами.
Очень важным моментом стоит выделить еще и оформление логических выражений 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
-ы фигурными скобками {}
.
Еще одним важным аспектом, который многие, особенно по началу, игнорируют является то, что пишут огромную лапшу, которая идет непрерывным водопадом от начала до конца монитора, без каких-то логических резделений.
Как например тут:
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-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());
}
}
Поэтому старайтесь не дробить излишне свой код - так вы можете сделать его гораздо менее читабельным даже с хорошим неймингом.
Комментарии — это пояснения к исходному тексту программы, находящиеся непосредственно внутри комментируемого кода.
В Java
существует две возможности добавить комменатрии к коду.
-
Строчный комментарий, начинающийся с
//
.// Строчный
-
Блочный комментарий.
/* * Блочный комментарий */
В идеале надо стремиться к такому коду, который не нуждается в комментировании.
Однако такое не всегда выходит, в дополнении к этому, часто бывает так, что код выполняет запутанную бизнес-логику.
И такие места, я считаю, нужно комментировать и объяснять.
Самой большой ошибкой будет злоупотребление комментариями в коде. Так как это серьезно захламляет код, при этом не добавляя ничего нового, например:
// 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;
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
переменными и методами!
Если вы написали код, то отвечайте за то, что написали.
Если вы автор какого-то метода, то именно вы в ответе за поведение метода и за то, что его контракт выполняется верно.
Иначе вы просто вводите в заблуждение, а это требует дополнительное время на изучение вашего кода и силы на то, чтобы держать в голове, что вот тот метод ведет себя не так, как написано.
Такие ситуации вызывают только раздражение по отношению к автору кода.
Помните, что чем шире модификатор доступа к вашему коду(чем он доуступнее для использования из других частей) - тем большее влияние оказывает наименование на его применение.