JavaScript — чистый код
Написание чистого кода — это то, что делают на автомате профессиональные разработчики.
В этой статье мы обсудим основные понятия принципа чистоты кода, как называть и использовать переменные и функции, а также рассмотрим некоторые конкретные примеры применения “чистого кода” в javascript.
Что такое Чистый код и с чем его едят?
Понятие Чистый код значит, что вы пишете код для повторного использования собой и коллегами, а уже потом для машин. Ваш код должен быть легок в понимании.
Если каждая рутина, которую Вы читаете кажется Вам более менее предсказуема — вы работаете с хорошим кодом.
JavaScript. Чистый код. Best Practices
Сейчас, когда мы имеем общее понятие, что от нас требуется, перейдем к более конкретным моментам.
Как называть переменные?
Имя переменной должно указывать на то, зачем она нужна. Не переживайте если Ваши названия будут не совсем короткими.
Если вы будете этому следовать — Ваши переменные можно будет отыскать в коде, что очень поможет в рефакторинге или когда вы просто будете сами что-то искать.
// DON'T
let d
let elapsed
const ages = arr.map((i) => i.age)
// DO
let daysSinceModification
const agesOfUsers = users.map((user) => user.age)
Кроме того не перегружайте свои переменные.
// DON'T
let nameString
let theUsers
// DO
let name
let users
Название Ваших переменных должно легко произноситься, так как для человека это будет требовать меньше времени на обработку. Когда вы делаете код-ревью со своими коллегами, на подобные названия будет легче ссылаться.
// DON'T
let fName, lName
let cntr
let full = false
if (cart.size > 100) {
full = true
}
// DO
let firstName, lastName
let counter
const MAX_CART_SIZE = 100
// ...
const isFull = cart.size > MAX_CART_SIZE
Как писать функции?
Ваши функции должны делать только 1 вещь только на одном уровне абстракции, но делать её хорошо!
// DON'T
function getUserRouteHandler (req, res) {
const { userId } = req.params
// inline SQL query
knex('user')
.where({ id: userId })
.first()
.then((user) => res.json(user))
}
// DO
// User model (eg. models/user.js)
const tableName = 'user'
const User = {
getOne (userId) {
return knex(tableName)
.where({ id: userId })
.first()
}
}
// route handler (eg. server/routes/user/get.js)
function getUserRouteHandler (req, res) {
const { userId } = req.params
User.getOne(userId)
.then((user) => res.json(user))
}
- Используйте более длинные, но понятные названия.
Имя функции должно описывать ее назначение. Название существительной лучше делать глаголом или фразой.
// DON'T
/**
* Invite a new user with its email address
* @param {String} user email address
*/
function inv (user) { /* implementation */ }
// DO
function inviteUser (emailAddress) { /* implementation */ }
- Избегайте длинного списка аргументов.
Лучше использовать обьект, чем слишком длинный список аргументов.
// DON'T
function getRegisteredUsers (fields, include, fromDate, toDate) { /* implementation */ }
getRegisteredUsers(['firstName', 'lastName', 'email'], ['invitedUsers'], '2016-09-26', '2016-12-13')
// DO
function getRegisteredUsers ({ fields, include, fromDate, toDate }) { /* implementation */ }
getRegisteredUsers({
fields: ['firstName', 'lastName', 'email'],
include: ['invitedUsers'],
fromDate: '2016-09-26',
toDate: '2016-12-13'
})
- Уменьшайте побочные эффекты
Пишите чистые функции без побочных эффектов, пока это возможно. Они реально упрощают использование и тестирование.
// DON'T
function addItemToCart (cart, item, quantity = 1) {
const alreadyInCart = cart.get(item.id) || 0
cart.set(item.id, alreadyInCart + quantity)
return cart
}
// DO
// not modifying the original cart
function addItemToCart (cart, item, quantity = 1) {
const cartCopy = new Map(cart)
const alreadyInCart = cartCopy.get(item.id) || 0
cartCopy.set(item.id, alreadyInCart + quantity)
return cartCopy
}
// or by invert the method location
// you can expect that the original object will be mutated
// addItemToCart(cart, item, quantity) -> cart.addItem(item, quantity)
const cart = new Map()
Object.assign(cart, {
addItem (item, quantity = 1) {
const alreadyInCart = this.get(item.id) || 0
this.set(item.id, alreadyInCart + quantity)
return this
}
})
- Организовывайте свои функции в файле в соответствии с правилам перехода.
Функция самого высокого уровня должна быть сверху. Это значительно облегчает прочтение кода.
// DON'T
// "I need the full name for something..."
function getFullName (user) {
return `${user.firstName} ${user.lastName}`
}
function renderEmailTemplate (user) {
// "oh, here"
const fullName = getFullName(user)
return `Dear ${fullName}, ...`
}
// DO
function renderEmailTemplate (user) {
// "I need the full name of the user"
const fullName = getFullName(user)
return `Dear ${fullName}, ...`
}
// "I use this for the email template rendering"
function getFullName (user) {
return `${user.firstName} ${user.lastName}`
}
- Запрос или изменения.
Функции должны иметь простую возможность модификовываться или отвечать на запросы, но не одновременно.
Все любят писать JavaScript по-разному, что делать?
Поскольку JavaScript является динамическим и слабо типизированным, он особенно подвержен ошибкам программиста.
Используйте правила проекта или компании по написанию и форматированию кода.
Чем жестче правила, тем меньше усилий будет направлено на то, чтобы исправить плохое форматирование. Правила должны охватывать такие вещи, как согласованное присвоение имен, размер отступов, размещение пробелов и даже точки с запятой.
Как писать красивый асинхронный код?
Вместо написания вложенных callbacks, вы можете использовать Promises доступные в node js.
// AVOID
asyncFunc1((err, result1) => {
asyncFunc2(result1, (err, result2) => {
asyncFunc3(result2, (err, result3) => {
console.lor(result3)
})
})
})
// PREFER
asyncFuncPromise1()
.then(asyncFuncPromise2)
.then(asyncFuncPromise3)
.then((result) => console.log(result))
.catch((err) => console.error(err))
Большинство библиотек, которые имеют интерфейсы callbacks и Promises, предпочитают использовать последний. Вы даже можете конвертировать API callbacks, чтоб использовать Promises, обернув их, используя пакеты, такие как es6-promisify.
// AVOID
const fs = require('fs')
function readJSON (filePath, callback) {
fs.readFile(filePath, (err, data) => {
if (err) {
return callback(err)
}
try {
callback(null, JSON.parse(data))
} catch (ex) {
callback(ex)
}
})
}
readJSON('./package.json', (err, pkg) => { console.log(err, pkg) })
// PREFER
const fs = require('fs')
const promisify = require('es6-promisify')
const readFile = promisify(fs.readFile)
function readJSON (filePath) {
return readFile(filePath)
.then((data) => JSON.parse(data))
}
readJSON('./package.json')
.then((pkg) => console.log(pkg))
.catch((err) => console.error(err))
Следующим шагом будет использование async/await (≥ Node 7) или генераторов с co (≥ Node 4) чтоб добиться синхронных потоков управления для вашего асинхронного кода.
const request = require('request-promise-native')
function getExtractFromWikipedia (title) {
return request({
uri: 'https://en.wikipedia.org/w/api.php',
qs: {
titles: title,
action: 'query',
format: 'json',
prop: 'extracts',
exintro: true,
explaintext: true
},
method: 'GET',
json: true
})
.then((body) => Object.keys(body.query.pages).map((key) => body.query.pages[key].extract))
.then((extracts) => extracts[0])
.catch((err) => {
console.error('getExtractFromWikipedia() error:', err)
throw err
})
}
// PREFER
async function getExtractFromWikipedia (title) {
let body
try {
body = await request({ /* same parameters as above */ })
} catch (err) {
console.error('getExtractFromWikipedia() error:', err)
throw err
}
const extracts = Object.keys(body.query.pages).map((key) => body.query.pages[key].extract)
return extracts[0]
}
// or
const co = require('co')
const getExtractFromWikipedia = co.wrap(function * (title) {
let body
try {
body = yield request({ /* same parameters as above */ })
} catch (err) {
console.error('getExtractFromWikipedia() error:', err)
throw err
}
const extracts = Object.keys(body.query.pages).map((key) => body.query.pages[key].extract)
return extracts[0]
})
getExtractFromWikipedia('Robert Cecil Martin')
.then((robert) => console.log(robert))
Источник: Blog risingstack.