Clean code
Some advice for keeping your code clean and manageable
Long functions
When a function becomes too long, extract parts of it into separate functions.
Example
function DrawEverything(canvas) {
// lots of code
// ...
// ...
// ...
}
Becomes:
function DrawEverything(canvas) {
drawBackground(canvas);
drawEnemies(canvas);
drawPlayer(canvas);
drawProjectiles(canvas);
}
Long classes
Same as long functions, try to break them up.
Too many arguments
If you have a function with many arguments, create a class to group the related values into a single package.
Example
function printDocument(
paperSize,
marginLeft,
marginRight,
marginTop,
marginBotton,
numCopies,
resolution,
text,
fontColor,
fontSize,
fontFamily,
lineSpacing,
paragraphIndentation
) {
// do the printing
}
Becomes:
class Margin {
constructor (left, right, top bottom) {
this.left = left;
this.right = right;
this.top = top;
this.bottom = bottom;
}
}
class PrintParameters {
constructor (paperSize, margin, numCopies, resolution) {
this.paperSize = paperSize;
this.margin = margin;
this.numCopies = numCopies;
this.resolution = resolution;
}
}
class Font {
constructor (color, size, family) {
this.color = color;
this.size = size;
this.family = family;
}
}
class DocumentFormat {
constructor (font, lineSpacing, paragraphIndentation) {
this.font = font;
this.lineSpacing = lineSpacing;
this.paragraphIndentation = paragraphIndentation;
}
}
class TextDocument {
constructor (text, format) {
this.text = text;
this.format = format;
}
}
function printDocument(printParameters, textDocument) {
// do the printing
}
Too many attributes
Similar to the previous issue, but instead of having too many function arguments, you have too many properties in a class.
When a class has too many properties, group them into new classes.
Data duplication
Avoid having multiple variables holding copies of the same value. If certain value can be trivially computed from existing variables, it's usually better to provide a function for computing it instead of storing it in a variable.
Example
class Player {
constructor(health) {
this.health = health;
this.isDead = false;
}
receiveDamage(amount) {
this.health -= amount;
if (this.health < 0) {
this.health = 0;
}
if (this.health == 0) {
this.isDead = true;
}
}
heal(amount) {
this.health += amount;
if (this.health > 0) {
this.isDead = false;
}
}
}
Becomes:
class Player {
constructor (health) {
this.health = health;
}
receiveDamage(amount) {
this.health -= amount;
if (this.health < 0) {
this.health = 0;
}
}
heal(amount) {
this.health += amount;
}
getIsDead ( ) {
return this.health <= 0;
}
}
Note
- Second variant is shorter, because there is no need to update
this.isDead
every time health changes. - In the second variant, it is impossible for the value to be incorrect as long as the function
isDead( )
is correct. In the first variant, forgetting to updatethis.isDead
(or updating it incorrectly) will introduce a bug which can be difficult to find.
Code duplication in classes
If you have multiple classes with similar methods and/or attributes, consider extracting the common part into a base class and using inheritance.
Example
class Vector2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Player {
constructor(name){
this.position = new Vector2(0,0);
this.name = name;
}
move(offset){
this.position.x += offset.x;
this.position.y += offset.y;
}
// Player-related methods
// ...
// ...
}
class Skeleton {
constructor(){
this.position = new Vector2(0,0);
this.name = "Skeleton";
}
move(offset){
this.position.x += offset.x;
this.position.y += offset.y;
console.log("rattling of bones echoes through the dungeon");
}
// Skeleton-related methods
// ...
// ...
}
class Slime {
constructor(){
this.position = new Vector2(0,0);
this.name = "Slime";
}
move(offset){
this.position.x += offset.x;
this.position.y += offset.y;
}
// Slime-related methods
// ...
// ...
}
Becomes:
class Vector2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Character {
constructor(name){
this.position = new Vector2(0,0);
this.name = name;
}
move(offset){
this.position.x += offset.x;
this.position.y += offset.y;
}
}
class Player extends Character {
constructor(name){
super(name);
}
// Player-related methods
// ...
// ...
}
class Skeleton extends Character {
constructor(){
super("Skeleton");
}
move(offset){
super.move(offset);
console.log("rattling of bones echoes through the dungeon");
}
// Skeleton-related methods
// ...
// ...
}
class Slime extends Character {
constructor(){
super("Slime");
}
// Slime-related methods
// ...
// ...
}
Code duplication in functions
If you have multiple functions with similar code, consider extracting the common part into a separate function.
Example
function castFireball() {
console.log("preparing to cast..."); // accidental inconsistency
console.log("reading the spell scroll: Fireball...");
console.log("a massive sphere of flame flies towards the enemy!");
}
function castHeal() {
console.log("preparing to cast a spell..."); // accidental inconsistency
console.log("reading the spell scroll: Heal...");
console.log("you summon healing energies, which make you feel better");
}
Becomes:
function readSpell(name) {
console.log("preparing to cast a spell...");
console.log(`reading the spell scroll: ${name}...`);
}
function castFireball() {
readSpell("Fireball");
console.log("a massive sphere of flame flies towards the enemy!");
}
function castHeal() {
readSpell("Heal");
console.log("you summon healing energies, which make you feel better");
}
Note
In the first version you could modify one of the functions and forget to modify the other, introducing an inconsistency.
The second version protects you from this.
Panic early
If you have a feeling that something might go wrong in your code, implement a sanity check to receive an immediate warning. Javascript is particularly notorious for pretending like everything is fine and continuing the execution with corrupt data (undefined, etc).
MDN link: throw statement.
Example
class Player {
constructor (health) {
this.health = health;
}
receiveDamage(amount) {
this.health -= amount;
if (this.health < 0) {
this.health = 0;
}
}
heal(amount) {
this.health += amount;
}
getIsDead ( ) {
if (this.health < 0) {
throw "health should never be negative!";
// something clearly went wrong, no reason to continue execution!
}
return this.health == 0;
}
}
Magic numbers
When using numeric constants (for example, ), create named constants instead of writing the values directly in your code.
Example
class Circle {
constructor(radius){
this.radius = radius;
}
getCircumference(){
return this.radius * 2.0 * 3.1415;
}
getArea(){
return 3.14 * (this.radius ** 2);
}
}
Becomes:
const PI = 3.1415;
class Circle {
constructor(radius){
this.radius = radius;
}
getCircumference(){
return this.radius * 2.0 * PI;
}
getArea(){
return PI * (this.radius ** 2);
}
}
Note
In the first variant different precision was used for in different places. This can create confusion.
Creating a constant ensures the value is always the same throughout the code.