Мої найкращі практики – загальні
1. Коментарі — прокляття і благословення
Хороший коментар — це мистецтво. Влучний — справжній дар, та часто — зайвий тягар.
Самодокументований код не означає «жодних коментарів». Код має якомога краще сам виражати, як щось працює; коментарі доповнюють його поясненням чому, неочевидними граничними умовами та контрактом API.
Далі — кілька прикладів самодокументованого коду, а також хороших і поганих коментарів.
Поширений аргумент: коментарі швидко застарівають. Насправді ця проблема так само стосується назв змінних і методів!
Ой — ніколи так не робіть:
int age = 32; // set the value of the age integer to 32
Самодокументований код: влучні назви змінних — половина успіху
Погано:
var dateTime = DateTimeUtil.getNow();
Добре:
var now = DateTimeUtil.getNow();
Ще один приклад:
Погано:
User user = this.getBruce();
Погано:
// testing user without permission
User user = this.getBruce();
Добре:
User userWithoutPermission = this.getBruce();
Самодокументований код: влучні назви методів
Погано:
public String sanitizeInput(String input) {
// replace underscore with spaces
return input.replaceAll("_", " ");
}
Добре:
public String replaceUnderscoreWithSpaces(String input) {
return input.replaceAll("_", " ");
}
Самодокументований код: приватні методи
Погано: метод змушує читача одночасно розбиратися з валідацією, обчисленням і бізнес-правилом.
public Invoice createInvoice(Order order, Customer customer) {
if (order.items().isEmpty()) {
throw new IllegalArgumentException("Order needs at least one item");
}
BigDecimal subtotal = order.items().stream()
.map(item -> item.unitPrice().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal discount = customer.isPremium()
? subtotal.multiply(new BigDecimal("0.10"))
: BigDecimal.ZERO;
return new Invoice(order.id(), subtotal, discount, subtotal.subtract(discount));
}
Добре: публічний метод розповідає, як усе відбувається. Приватні stateless-методи називають окремі наміри й роблять свої залежності видимими через параметри та значення, що повертаються.
public Invoice createInvoice(Order order, Customer customer) {
requireItems(order);
BigDecimal subtotal = calculateSubtotal(order.items());
BigDecimal discount = loyaltyDiscountFor(customer, subtotal);
return invoiceFor(order, subtotal, discount);
}
private static void requireItems(Order order) {
if (order.items().isEmpty()) {
throw new IllegalArgumentException("Order needs at least one item");
}
}
private static BigDecimal calculateSubtotal(List<OrderItem> items) {
return items.stream()
.map(item -> item.unitPrice().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private static BigDecimal loyaltyDiscountFor(Customer customer, BigDecimal subtotal) {
return customer.isPremium()
? subtotal.multiply(new BigDecimal("0.10"))
: BigDecimal.ZERO;
}
private static Invoice invoiceFor(
Order order,
BigDecimal subtotal,
BigDecimal discount
) {
return new Invoice(order.id(), subtotal, discount, subtotal.subtract(discount));
}
Не кожен рядок потребує окремого методу. Виносити варто цілісну думку, яку можна точно назвати. Назви на кшталт processData() чи handleStuff() натомість нічого не дають.
Емпіричне правило: коментуйте, чому щось відбувається, а не що відбувається
Тут зрозуміло, що відбувається:
public Duration retryDelay(int retryCount) {
long seconds = Math.min(1L << retryCount, 60);
return Duration.ofSeconds(seconds);
}
Але чому це відбувається?
Краще:
public Duration retryDelay(int retryCount) {
// Cap the delay so an outage never blocks queued jobs for more than one minute.
long seconds = Math.min(1L << retryCount, 60);
return Duration.ofSeconds(seconds);
}
Ще один приклад із реального життя. Тут показано методи, які показують і приховують Ajax-спінер завантаження. Проте методів чотири, і чому саме так, з першого погляду не зовсім зрозуміло:
function showAjaxLoader() {
$('.layout-ajax-loader').children().first().show();
}
function hideAjaxLoader() {
$('.layout-ajax-loader').children().first().hide();
}
function suppressAjaxLoader() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = 'none';
}
function releaseAjaxLoader() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = '';
}
Усі методи здаються дуже схожими й так чи інакше змінюють видимість через CSS. Короткий коментар одразу прояснює, для чого кожен із них:
showAjaxLoader: function() {
$('.layout-ajax-loader').children().first().show();
},
hideAjaxLoader: function() {
$('.layout-ajax-loader').children().first().hide();
},
// take the control from primefaces and don't allow the spinner to show, no matter what
suppressAjaxLoader: function() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = 'none';
},
// reset the state after hiding, to give the control back to primefaces
releaseAjaxLoader: function() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = '';
},
Ще один приклад:
Погано:
/**
* Sets the tool tip text.
*
* @param text the text of the tool tip
*/
public void setToolTipText(String text) {}
Добре:
/**
* Registers the text to display in a tool tip. The text
* displays when the cursor lingers over the component.
*
* @param text the string to display. If the text is null,
* the tool tip is turned off for this component.
*/
public void setToolTipText(String text) {}
Обговорення на воркшопі 2021-06-21: усі розробники погодилися, що Javadoc загалом варто уникати (кошмар із параметрами, постійно застаріває, незручно підтримувати) і застосовувати лише тоді, коли параметр справді потребує пояснення або коли інакше довелося б читати весь довгий метод.
Виняток із емпіричного правила: коментуйте, що відбувається, — тут це доречно
Коли це помітно полегшує читання, бо інакше код незрозумілий:
let isAlwaysAllowedKey = (
e.which === 8 || /* BACKSPACE */
e.which === 35 || /* END */
e.which === 36 || /* HOME */
e.which === 37 || /* LEFT */
e.which === 38 || /* UP */
e.which === 39 || /* RIGHT*/
e.which === 40 || /* DOWN */
e.which === 46 || /* DEL*/
e.ctrlKey === true && e.which === 65 || /* CTRL + A */
e.ctrlKey === true && e.which === 88 || /* CTRL + X */
e.ctrlKey === true && e.which === 67 || /* CTRL + C */
e.ctrlKey === true && e.which === 90 /* CTRL + Z */
)
Коментарі — лайка в коді ядра Linux
І насамкінець кумедний факт про ядро Linux — кількість лайливих слів у його вихідному коді:
https://www.vidarholen.net/contents/wordcount/#fuck*,shit*,damn*,idiot*,retard*
Приємного написання коментарів!
2. Приватні методи у великих програмних процедурах небезпечні
Увага! У сучасному об’єктно-орієнтованому світі завжди багато приватних методів. Зазвичай це не проблема. Вона з’являється тоді, коли ці методи звертаються до полів, тобто до стану, і містяться у великих процедурах. Такі допоміжні методи мають бути приватними та stateless. Інакше їхній недолік у тому, що окремий виклик часто завдає клопоту. Розробникам спаде на думку викликати такий допоміжний метод напряму. Неприпустимі стани — одна з головних причин багів. Цікава стаття на цю тему про деякі висловлювання John Carmack (id Software, Doom) і про те, що в застосунках, де код має бути особливо стабільним (наприклад, у космічних польотах), підпрограми часто заборонені, що робить код детермінованішим: http://number-none.com/blow/john_carmack_on_inlined_code.html
Ця історія лише частково стосується сучасного stateless-світу Java/Spring, але все одно дає добру поживу для роздумів.