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

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

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

Как определить чистый код

Дядя Боб сходил к именитым разработчикам и спросил у них, что они подразумевают, когда говорят про чистый код. Разработчики дали разные ответы. 

  • Например, Бьёрн Страуструп, изобретатель C++, говорит, что чистый код должен быть эффективным, он должен делать что-то одно. 

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

  • Майкл Физерс, автор книги «Эффективная работа с legacy-кодом», считает, что чистый код должен выглядеть так, будто он написан человеком, которому не все равно. 

  • Гради Буч, знаменитый разработчик, сейчас он работает в IBM, уверен, что чистый код должен читаться, как хорошо написанная проза. Простой код должен быть простым и понятным или однозначным. 

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

1. Названия

1.1 Понятные имена

Если название требует комментария, значит, это название не отражает нашего намерения

Например, у нас есть класс DaoCacheConfigProperties, у него есть две переменные — alive и elements. Из названий не совсем понятно, за что отвечают эти переменные. Если их нужно прокомментировать, то, скорее всего, этим переменным дано неправильное имя. 

@ConfigurationProperties(prefix = "company.cache.dao")
class DaoCacheConfigProperties {
    lateinit var alive: Integer // alive cache time in days
    lateinit var elements: Integer // maximum elements in cache
}

alive — количество в днях, сколько должен жить этот кэш, а elements — максимальное количество, сколько должно храниться в кэше это значение. Их можно было бы назвать по-другому, перенести комментарии в название переменной, например, timeToLiveInDays или maximumSavedElements.

@ConfigurationProperties(prefix = "company.cache.dao")
class DaoCacheConfigProperties {
    lateinit var timeToLiveInDays: Integer
    lateinit var maximumSavedElements: Integer
}

Сюда же я бы вынес комментарии про Magic Number (про них есть только в книжке). Это о том, что все числа, которые непонятны (а это большинство чисел), нужно выносить в описательные имена, то есть тут непонятно, что такое пять.

assertSame(5, doSome())
fun doSome(): Int {
    TODO()
}

Но есть исключения: например, 0, 1 и 1024 легко понятны. Например, понятно, что 1024 — количество бит, 60*60*24 — это сутки, то есть для таких чисел не обязательно использовать описательные названия.

1.2 Избегайте дезинформации

Итак, у нас есть какая-то функция, например, getMonth().

fun getMonth(shortened: Boolean): String {
    return if (shortened) {
        LocalDateTime.now().month.name.substring(0, 3)
    } else {
        LocalDateTime.now().month.name
    }
}

Если мы заглянем в реализацию, то по ней видно, что мы возвращаем на самом деле не объект month, а имя этого месяца, каким-то образом обрезанное. Эта функция должна называться не getMonth(), аgetMonthName() или getShortenedMonthName(). Иначе ее реализация не соответствует ее названию, то есть это название дезинформирует. 

fun getMonthName(shortened: Boolean): String {
    return if (shortened) {
        LocalDateTime.now().month.name.substring(0, 3)
    } else {
        LocalDateTime.now().month.name
    }
}

1.3 Изменяйте имена, не давайте им протухнуть

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

fun getParisMonthName(shortened: Boolean): String {
    return if (shortened) {
        LocalDateTime.now().atZone(ZoneId.of("Europe/Paris")).month.name.substring(0, 3)
    } else {
        LocalDateTime.now().atZone(ZoneId.of("Europe/Paris")).month.name
    }
}

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

1.4 Имена должны быть просты в произношении

Переменные нам даны для того, чтобы описывать свое намерение, и для коммуникации между собой. Скажем, мы где-нибудь в переговорке или в Zoom садимся, обсуждаем, кто и что написал. Функциями, названия которых сложно произносить, сложно коммуницировать.

Например, как сказать: «Вызови функцию getYYYY»? Язык не поворачивается же. Такие примеры есть в том же самом SQL, когда по стандарту к каждой таблице применяется alias. Эти alias чаще всего бывают труднопроизносимыми. Например, select * from virtual_technical_merchants_processors можно прочитать, а alias становится vtpm, что прочитать невозможно.

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

1.5 Избегайте схем кодирования

IDE уже знает о типах кодирования, но раньше было время, когда не было IDE, и приходилось прибавлять каждой переменной название. Например, что она булева (bvalue) или svalue, обозначая, что это string.

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

interface iRunnable

Или RunnableInterface

interface RunnableInterface

Аналогично с реализациями. Например, RunnableImpl тоже не добавляет никакой информации.

interface Runnable
class RunnableImpl : Runnable
class Work : Runnable

Или, например, с переменными: wordString — и так понятно, что это string — или sWord.

val wordString = "description"
val sWord = "description"

1.6 Части речи в коде

Начнем с классов и переменных: они должны быть существительными.

Нужно избегать таких слов, как Manager/Prosessor/Info/Data, так как они не дают никакой конкретики. А самая жесть, если класс называется Manager Prosessor или Info Data, совершенно непонятно, о чем они. 

Итак, классы и переменные должны быть существительными. Например, есть класс Car - он должен быть существительным.

class Car(val color: Color, val fuelType: Set<FuelType>) {
    val status = DriveStatus.STOP
    
    fun isHybrid(): Boolean {
        TODO()
    }
    
    fun move() {
        TODO()
    }
    
    fun stop() {
        TODO()
    }
}

Булевы методы внутри этого класса должны быть предикатами и булевы переменные тоже должны быть предикатами, например, isHybrid.

val isHybrid = car.isHybrid()

Значения Enum должны быть прилагательными или наречиями, потому что они всегда описывают либо состояния, либо какую-то характеристику объекта, то есть это Color, FuelType, DriveStatus — это все прилагательные.

enum class Color {
    RED, BLUE
}

enum class FuelType {
    ELECTRICITY, GASOLINE
}

enum class DriveStatus {
    STOP, ACCELERATE, BRAKE, RIDE
}

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

if (car.isHybrid() && fuelStation.isElectric()) {
    car.stop()
}

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

Тут есть интересный пример неестественной речи. Например, когда у нас есть функция set(), которая возвращает какое-то значение. То есть она одновременно и устанавливает это значение, и возвращает результат.

if (set("some-value")) {
    TODO()
}

fun set(value: String): Boolean {
    TODO()
}

В соответствии с этим правилом, если перевести это условие на русский язык, то оно будет звучать так: «Если установить запись - делаем что-то» — вроде бы звучит не особо по-русски и не переводится в привычную речь.

Чтобы сделать это условие естественной речью, нам нужно разделить метод на set(), который кидает ошибку в случае неуспеха, и метод isSet(). Таким образом можно написать какое-нибудь предложение, которое может прочитать человек: «Установить запись. Если запись установилась, то сделать что-то еще». 

1.7 Правила длины названий в зависимости от размеров используемого скоупа

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

Например, у нас есть код с большим количеством строк. Так же у нас есть переменная rootCompanyElement. Эта переменная используется условно в большом скоупе (можно было еще больше код написать, чтобы она использовалась выше экрана).

var docFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
var docBuilder: DocumentBuilder = docFactory.newDocumentBuilder()

val document = docBuilder.newDocument();

val rootCompanyElement = document.createElement("company");
document.appendChild(rootCompanyElement);

/** Много кода */

document.createElement("staff");
rootCompanyElement.appendChild(document.createElement("staff"));
val childCompanies = setOf("Company 1", "Company 2")
for (company in childCompanies) {
    rootCompanyElement.appendChild(document.createElement(company))
}

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

Если скоуп, в котором используется, переменная company, очень маленький, то переменную достаточно назвать одной буквой. Например, буквой c, а не company, и тогда, мы не потеряем фокус внимания, так как мы смотрим на эти три строчки кода и видим, где она инициируется одновременно с тем же, где она используется. Другими словами это нормально, использовать переменную с названием “c” в маленьком скоупе.

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

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

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

class Server {
    fun serve(socket: Socket) {
        try {
            tryProcessInstructions(socket)
        } catch (exception: Throwable) {
            TODO()
        } finally {
            closeServiceInSeparateThread();
        }
    }
    
    private fun tryProcessInstructions(socket: Socket) {
        TODO()
    }
    
    private fun closeServiceInSeparateThread() {
        TODO()
    }
    
    private class SocketTimeoutConnectionController
}

Если мы по всему приложению используем этот Server.serve(), то желательно, чтобы он назывался как можно короче. 

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

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

Тут надо остановиться на исключении. Исключение для этого правила применяется для наследования. Есть интерфейс Account, и у него есть наследник SavingAccount.

interface Account
class SavingAccount : Account

Каждый раз делая наследника, мы добавляем в название прилагательное, для того, чтобы расширить его или сделать от него instance. Чем больше цепочка наследования тем длиннее имена. Все мы помним названия, которые часто можно увидеть в Spring, из 30-40 символов, которые невозможно запомнить. За такими названиями надо внимательно следить.

1.8 Избегайте отрицательных условий

Отрицательные условия сложнее в понимании, чем положительные. Таким образом, старайтесь формулировать положительные условия. Например, у нас есть класс Kafka, у него есть метод notNeedToSend().

if(kafka.notNeedToSend()) {
    TODO()
}

Метод needToSend() легче читать, чем notNeedToSend(). Плюс у нас всегда появляется вероятность того, что в коде notNeedToSend() будет использовано с отрицанием.

if(!kafka.notNeedToSend()) {
    TODO()
}

Если попытаться прочитать, то получится: если не kafka.notNeedToSend() — отрицание на отрицании — то что-нибудь сделать. Когда ты это читаешь, заходят шарики за ролики (сложно понять). 

2. Функции

2.1 Функции должны быть маленькими

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

fun createCompanyProfileDocument(company: Company) {
val docFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
val docBuilder: DocumentBuilder = docFactory.newDocumentBuilder()
val document = docBuilder.newDocument();

val rootCompanyElement = document.createElement(company.name)
document.appendChild(rootCompanyElement)

val staffElement = document.createElement("staff")
for (employee in company.employees) {
    val childElement = document.createElement(employee)
    staffElement.appendChild(childElement)
}
rootCompanyElement.appendChild(staffElement)

/** Здесь закончилось добавление сотрудников в родительскую компанию */

for (childCompany in company.childCompanies) {
    val companyElement = document.createElement(childCompany.name)
    val staffElement = document.createElement("staff");
    for(employee in childCompany.employees) {
        /** В функциях не должно быть много отступов */
        val childElement = document.createElement(employee)
        staffElement.appendChild(childElement)
    }
    companyElement.appendChild(staffElement)
    rootCompanyElement.appendChild(companyElement)
}

if (company.turnover &gt; config.amountBig) {
    val attentionElement = document.createElement("ATTENTION")
    rootCompanyElement.appendChild(attentionElement)
}

/** ... */

}

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

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

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

Чтобы соответствовать правилу о маленьких функция можно эту функцию отрефакторить примерно в такой вид:

fun createCompanyProfileDocument(company: Company) {
    val document = createNewDocument()
    /** Указатель один - создание root */
    val rootCompanyElement = createCompanyNameAsRootElement(document, company)
    /* Указатель два - добавление работников в root */
    createStuffElement(document, company, rootCompanyElement)
    /* Указатель три - отчета по дочерним компаниям */
    createChildCompaniesElements(company, rootCompanyElement, document)
    /* Указатель четыре - помечаем компанию как ту, на которую надо обращать внимание */
    createAttentionMarkerElement(document, company, rootCompanyElement)
}

fun createNewDocument(): Document {
    val docFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
    val docBuilder: DocumentBuilder = docFactory.newDocumentBuilder()
    val document = docBuilder.newDocument();
    return document
}

fun createCompanyNameAsRootElement(document: Document, company: Company): Element {
    val rootCompanyElement = document.createElement(company.name)
    document.appendChild(rootCompanyElement);
    return rootCompanyElement
}

fun createStuffElement(document: Document, company: Company, rootCompanyElement: Element) {
    val staffElement = document.createElement("staff")
    for (employee in company.employees) {
        val employeeElement = document.createElement(employee)
        staffElement.appendChild(employeeElement)
    }
    rootCompanyElement.appendChild(staffElement)
}

fun createChildCompaniesElements(company: Company, rootCompanyElement: Element, document: Document) {
    for (childCompany in company.childCompanies) {
        val companyElement = document.createElement(childCompany.name)
        createStuffElement(document, childCompany, companyElement)
        rootCompanyElement.appendChild(companyElement)
    }
}

fun createAttentionMarkerElement(document: Document, company: Company, rootCompanyElement: Element) {
    if (company.turnover > config.amountBig) {
        val attentionElement = document.createElement("ATTENTION")
        rootCompanyElement.appendChild(attentionElement)
    }
}

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

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

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

Третье - отступов в маленькой функции будет немного. В первой версии у нас было максимально два отступа и в них можно было запутаться. А в этой функции у нас один отступ и только один for().

Четвертый - в результате рефакторинга и выноса маленьких функций мы можем переиспользовать какие-то функции. Мне удалось, например, переиспользовать функцию createStuffElement(). Это функция, которая добавляет по конкретной компании информацию по ее сотрудникам. Я пользуюсь ей для родительской компании и для дочерних.

Но даже когда функция достаточно маленькая - все равно новая функция за счет названия может добавить читабельности. Можно, например, вынести условие для простановки маркера “ATTENTION” в говорящую функцию isBigClient(). Таким образом, код становится более понятным. Прочитать можем это так: если клиент большой, то добавляем маркер “ATTENTION” к нему.

fun createAdditionalMarkerElement(document: Document, company: Company, rootCompanyElement: Element) {
    if (isBigClient(company)) {
        val attentionElement = document.createElement("ATTENTION")
        rootCompanyElement.appendChild(attentionElement)
    }
}

fun isBigClient(company: Company) = company.turnover > config.amountBig

В длинных функциях часто скрываются классы

Один из способов найти все классы в вашей системе — найти длинные функции. У нас есть похожая функция, которая называется generateTaxReport(). Она печатает отчет о налогах по физическому лицу. И опять очень большая:

fun generateTaxReport(person: Person) {
    val docFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
    val docBuilder: DocumentBuilder = docFactory.newDocumentBuilder()
    val document = docBuilder.newDocument();
    
    val rootPersonElement = document.createElement(person.inn)
    rootPersonElement.setAttribute("name", person.name)
    document.appendChild(rootPersonElement)
    
    /** Здесь кроется класс EstateTaxCalculator */
    val estateRootElement = document.createElement("estates")
    var estateTax = 0.00
    var fullEstatePrice = 0.00
    for (estate in person.estates) {
        val estateElement = document.createElement("estate")
        estateElement.setAttribute("price", estate.commercialPrice.toString())
        fullEstatePrice += estate.commercialPrice
        estateTax += when(estate.type) {
            EstateType.COMMERCIAL -&gt; estate.commercialPrice * taxForCommercialEstate
            EstateType.PERSONAL -&gt; estate.commercialPrice * taxForPersonEstate
        }
        estateRootElement.appendChild(estateElement)
    }
    if(fullEstatePrice &gt; highPriceForAllEstates) {
        estateTax += estateTax * highPriceEstatesRatio
    }
    estateRootElement.setAttribute("estateTax", estateTax.toString())
    rootPersonElement.appendChild(estateRootElement)
    
    /** Здесь кроется класс IncomesTaxCalculator */
    val incomesRootElement = document.createElement("incomes")
    var fullIncome = 0.00
    var selfPayedTax = 0.00
    for(income in person.incomes) {
        fullIncome += income.amount
        val incomeElement = document.createElement("estate")
        if(income.selfPayed) {
            selfPayedTax += income.amount * income.taxPercent
            incomeElement.setAttribute("isSelfPayed", "yes")
        } else {
            incomeElement.setAttribute("isSelfPayed", "no")
        }
        incomesRootElement.appendChild(incomeElement)
    }
    if(fullIncome &gt; richManIncome) {
        fullIncome += fullIncome * richManIncome
    }
    incomesRootElement.setAttribute("incomeTax", fullIncome.toString())
    rootPersonElement.appendChild(incomesRootElement)
}

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

Внутри функции generateTaxReport() у нас явно кроется класс, который можно назвать EstateTaxCalculator. Это класс, который считает налог на недвижимость.

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

Часто в таких больших функциях у нас кроются классы.

P.S. Оставляя функции большими, вы сохраняете себе время, но отнимаете его у других. А может, и у себя в будущем.

2.2 Функция должна делать только одно действие

Что значит фраза «функция должна делать только одно действие?». Дядя Боб это переформулировал так:

Если функция работает более чем с одним уровнем абстракции, значит, функция делает больше, чем одно действие. 

Рассмотрим функцию auth() (проведение платежа).

fun auth(payment: Payment, gatewayService: GatewayService) {
    val authResult = gatewayService.callAuth(payment.amount)
    if(authResult.rcCode == "00" && authResult.authCode !== null) {
        /** Сборка транзакции в успешную */
        payment.setStatus("ok")
        payment.saveExtra("auth_code", authResult.authCode)
    } else if(authResult.rcCode == "00" && authResult.authCode == null) {
        payment.setStatus("error")
        reverseDAO.create(payment.id, payment.amount)
    } else {
        payment.setStatus("declined")
        payment.saveExtra("rc", authResult.rcCode)
    }
}

Она идет в gateway (реализация интерфейса платежного шлюза), затем вызывает авторизацию. Дальше уже работа идет с платежом: меняется его статус и к нему добавляются Extra (дополнительные характеристики платежа). Если в ответе нет кода авторизации - то откатываем транзакцию. 

Эта функция выглядит так, как будто бы мы в ней совместили два уровня бизнес-логики: работу с внешним сервисом, когда мы вызываем gatewayService.callAuth(), и работу с базой данных. То есть сделать транзакцию оплаченной - один уровень бизнес-логики, а выставить транзакции статус “OK”, поставить ей одну Extra, вторую Extra, третью Extra — более низкий уровень бизнес-логики, они не должны совмещаться в одной функции. 

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

fun auth(payment: Payment, gatewayService: GatewayService) {
    val authResult = gatewayService.callAuth(payment.amount)
    if(isSuccess(authResult)) {
        setSuccessPayment(payment, authResult)
    } else if(isWrongBuiltSuccess(authResult)) {
        revertWrongConfigured(payment)
    } else {
        setDeclinedPayment(payment, authResult)
    }
}

fun isSuccess(authResult: ResponseDto): Boolean {
    return authResult.rcCode == "00" && authResult.authCode !== null
}

fun isWrongBuiltSuccess(authResult: ResponseDto): Boolean {
    return authResult.rcCode == "00" && authResult.authCode == null
}

fun setSuccessPayment(payment: Payment, authResult: ResponseDto) {
    payment.setStatus("ok")
    payment.saveExtra("auth_code", authResult.authCode)
}

fun revertWrongConfigured(payment: Payment) {
    payment.setStatus("error")
    reverseDAO.create(payment.id, payment.amount)
}

fun setDeclinedPayment(payment: Payment, authResult: ResponseDto) {
    payment.setStatus("declined")
    payment.saveExtra("rc", authResult.rcCode)
}

Мы вызываем на стороннем сервисе auth(), дальше у нас вынесены более низкие функции (проверка наличия rcCode и authCode, проставление статуса). Тогда эта функция не совмещает разные бизнес-логики в себе.

В такой функции без разных уровней бизнес-логики легко понять, что происходит: делаем авторизацию (запрос на оплату)gatewayService.callAuth(), и если результат isSuccess() успешный, то делаем транзакцию оплаченной setSuccessPayment(). Если транзакция успешная, но успех неправильно сгенерирован isWrongBuiltSuccess(), то есть, нет authCode в ответе, то откатываем транзакцию revertWrongConfigured().

Дальше я хотел бы рассказать про прием, который применяет дядя Боб, он называется “Extract till you drop”. Он выносит код из любой функции до тех пор, пока нечего будет выносить. То есть, например, функцию isSuccess() можно разделить на две функции.

fun isSuccess(authResult: ResponseDto) = isSuccessByRcCode(authResult) && authCodeIsPresented(authResult)
fun isSuccessByRcCode(authResult: ResponseDto) = authResult.rcCode == "00"
fun authCodeIsPresented(authResult: ResponseDto) = authResult.authCode !== null

В следующей части поста разберем структуру функций.

Комментарии (23)


  1. x2v0
    14.04.2023 08:49
    +2

    Когда я был "начинающим программистом" у нас в команде были свои "Coding Conventions".

    Не вдаваясь в детали, их можно было сформулировать, как = "твой код не должен отличаться от кода написанного другим членом команды".

    И хотя никаких подробных инструкций, как писть код не было, но очень скоро я стал писать "чистый код", подобный описываему в статье.


    1. askolo4ek
      14.04.2023 08:49
      +1

      У меня была похожая история, только бывали неприятные моменты, когда открываешь ПР, приходит человек на code review, и только в этот момент ты узнаешь о каких-то правилах. Которые, между прочим, ещё и могли меняться время от времени или выдумывались прямо на ходу


    1. sepetov
      14.04.2023 08:49
      +4

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


  1. ChessMax
    14.04.2023 08:49

    Например, Ирвин Якобс, изобретатель C++, говорит, что чистый код должен быть эффективным, он должен делать что-то одно.

    Вроде бы автором С++ всегда был Бьёрн Страуструп? Или я чего-то не знаю?


    1. lexus1990 Автор
      14.04.2023 08:49
      +1

      Все верно


  1. MaratCdek
    14.04.2023 08:49

    Можно ввести в Яндексе
    Чистый код site:habr.com


    1. lexus1990 Автор
      14.04.2023 08:49
      +2

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

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

      Кроме того, обычно конспекты делают по книге. Тут конспект сделан по лекциям на англ языке. Различается структура разделов и некоторые правила.


      1. gmtd
        14.04.2023 08:49

        Согласен, вполне структурированное и полезное чтиво

        Единственное,

        Enum должны быть прилагательными, потому что они всегда описывают либо состояния, либо какую-то характеристику объекта, то есть это Color, FuelType, DriveStatus — это все прилагательные.

        Разве это прилагательные?


        1. lexus1990 Автор
          14.04.2023 08:49

          Наверно, в переводе с английского да. Цвет - Зеленый. Тип топлива - бензиновый. Статус движения - едущий.

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

          Может быть у вас есть лучшие варианты и я поправлю статью?


          1. gmtd
            14.04.2023 08:49
            +1

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

            Во-вторых, различить имя enum и его члены.

            Имя - существительное
            Члены - определения этого существительного (не обязательно прилагательные, могут быть наречия например)


          1. lexus1990 Автор
            14.04.2023 08:49
            +1

            Спасибо! Поправил в статье


  1. Myxach
    14.04.2023 08:49
    +3

    Если значение класса или метода поменялось, то меняйте название

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


    1. lexus1990 Автор
      14.04.2023 08:49

      Можете подробнее пояснить? Тут речь только про название. Зачем ломать интерфейс?


      1. ryazanov_13
        14.04.2023 08:49

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

        Т.е. в данном примере, если посмотреть сигнатуру метода он ничего не принимает и возвращает строку. Как назвать интерфейс класса этого метода? Чё он делает абстрагируясь от реализации? Ты его дёргаешь - он тебе какую-то строку возвращает, причём, причем не непонятно какую, ты её в него не клал. Я бы назвал этот интерфейс каким-то строко-генератором. А реализацию генератором имени строки или типо того.

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

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

        Как вывод, IDE отличная штука, но не нужно завязывать архитектуру на возможности IDE.


      1. Myxach
        14.04.2023 08:49

        Ты меняешь название=>меняется его сигнатура=>если он публичный, то ломается интерфейс его

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


        1. lexus1990 Автор
          14.04.2023 08:49

          Если вы имеете ввиду только публичный api - то да. Но тут есть версионирование api

          Тут в основном речь идет про код внутри проекта. Переименование публичного метода и класса обычно происходит с автоматическим рефакторингом во всех местах использования с помощью IDE


          1. Myxach
            14.04.2023 08:49

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


            1. lexus1990 Автор
              14.04.2023 08:49

              Тут я с вами не соглашусь. Достаточно иметь trunk based development или не иметь большой монорепы - и тогда конфликтов скорее всего либо не будет, либо будет незначительно мало.

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


              1. Myxach
                14.04.2023 08:49

                Я Кстати нигде не говорил про смену название класса. В Пункте был четко предоставлен пример с методом


                1. lexus1990 Автор
                  14.04.2023 08:49

                  С переименованием метода абсолютно такая же ситуация


  1. Helltraitor
    14.04.2023 08:49
    +1

    Но есть исключения: например, 0, 1 и 1024 легко понятны. Например, понятно, что 1024 — количество бит, 60*60*24 — это сутки, то есть для таких чисел не обязательно использовать описательные названия.

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

    В хороших языках дополнительные типы вроде типов для времени будут очень дешевыми, если не бесплатными (Rust, C++)


    1. lexus1990 Автор
      14.04.2023 08:49

      Абсолютно согласен на счет Duration. Пример можно было подобрать получше, так как в нашем случае за заполнение объекта класса конфиг отвечает Spring и он умеет работать с Duration


  1. KongEnGe
    14.04.2023 08:49

    Сейчас у нас есть IDE, которые позволяют понять, какого типа у нас переменная, или подсказать, какой класс является интерфейсом, а какой реализацией.

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