بهترین روش‌های من – عمومی

۱. کامنت‌ها – نعمت و نقمت

کامنت خوب نوشتن یک هنر است. اگر سنجیده باشد موهبت است، اما اغلب هم بیهوده.

کد خودمستندساز به معنای «بدون کامنت» نیست. کد باید تا حد ممکن خودش چگونگی را بیان کند؛ کامنت‌ها چرایی، شرایط مرزی غیربدیهی و قرارداد یک 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("_", " ");
}

خودمستندسازی: متدهای private

بد: متد خواننده را مجبور می‌کند اعتبارسنجی، محاسبه و قاعده‌ی کسب‌وکار را هم‌زمان رمزگشایی کند.

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));
}

خوب: متد public روند کار را روایت می‌کند. متدهای private و بی‌حالت (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) {}

بحث در کارگاه ۲۱ ژوئن ۲۰۲۱: همه‌ی توسعه‌دهندگان هم‌نظر بودند که به‌طور کلی باید از 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*

از نوشتن کامنت لذت ببرید!

۲. متدهای private در روال‌های بزرگ برنامه خطرناک‌اند

توجه! در دنیای شیءگرای امروز همیشه متدهای private زیادی وجود دارد. این معمولاً مشکلی نیست. مشکل از جایی شروع می‌شود که این متدها به فیلدها، یعنی به State، دسترسی پیدا کنند و درون روال‌های بزرگ باشند. چنین زیرمتدهایی باید private و بی‌حالت (stateless) باشند. وگرنه این عیب را دارند که اگر به‌تنهایی فراخوانی شوند، اغلب دردسرساز می‌شوند. توسعه‌دهندگان به فکر فراخوانی مستقیم زیرمتد می‌افتند. Stateهای نامعتبر یکی از دلایل اصلی باگ‌ها هستند. مقاله‌ای جالب در این باره، درباره‌ی برخی گفته‌های John Carmack (id Software، Doom) و اینکه در برنامه‌هایی که کدشان باید بسیار پایدار باشد (مثلاً پرواز فضایی) زیرروال‌ها اغلب ممنوع‌اند، که کد را قطعی‌تر می‌کند: http://number-none.com/blow/john_carmack_on_inlined_code.html

این حکایت فقط تا حدی به دنیای مدرن و بی‌حالت Java/Spring قابل تعمیم است، اما با این حال تلنگر خوبی برای فکر کردن است.