Disclaimer 1: I am not and do not claim to be an expert on any of the topics you are about to read about. Do not consider this a comprehensive guide, instead look at this as the fledgling opinions of a fledgling developer.
Now, with that out of the way.
- What is "Clean" code?
It was this question that first sparked this journey into the realm of coding paradigms and their kaleidoscopic facets and it is this question that we will keep coming back to.
A simple google search of that question yields ~ 2 billion (1,940,000,000 to be exact) search results. So clearly I'm not the only person to think on this, theres even an aptly named book on the topic Clean Code. However I came across a set of acronymns that give us a good starting point.
KISS: (Keep It Simple Stupid) The philosophy of rendering down a design to its least complicated form. Ask yourself: 'Can this be written in a simpler way?'
DRY: (Don't Repeat Yourself) The Mantra of Senior Developers and Coding Instructors every where. Ask yourself: 'Can this be abstracted and reused?'
YAGNI: (You Ain't Gunna Need It) This one is a bit more for larger scale projects than what technomancers in training (like myself) might come across. Ask yourself: 'Does this functionality bring me closer to my MVP?'
With these we have a few guide posts when writing our code, unfortunetly, we now have a few more questions. So keep those in the back of your mind, because. . .
"Mr/Ms blog writer person," I hear you ask, "What is a Paradigm exactly?"
Well I'm not going to get into the finer points of it but for our purposes I'll be using this very simple definition:
A Paradigm is a collection of beliefs and thought patterns, a guiding philosophy if you will
There a Many Coding Paradigms, in this context we will only be going over two; Functional Programming and Object Oriented Programming.
From the Wiki page
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm in which function definitions are trees of expressions. . .
And with that we now have two new questions; first, what is a function? Second, what is an expression
A Simple Example
// in the math.js file
const add = (a, b) => a + b
in the example the line as a whole is a function (never mind that this is technically an arrow function expression ...because naming is hard) while everything to the right of the arrow (=>) is an expression. This also fits nicely into a few of the core tenets of Functional Programming. But first a quote:
Functional Programming imposes discipline on mutating state
The first principle of functional programming we are going to look at is the idea of 'Pure' functions. A pure function is a one that is, predictable, does not mutate any data that it takes in, and that only returns a single value; in our example above our function is pure. The purity of a function is paramount* in writing functional code, so much so that all of the other principles we will be covering rely on or intersect with it.
Pure functions and their predictability allow for a certain level of transparency in the code, for example:
A Simple Example v2
// in the math.js file const add = (a, b) => a + b const add = (1, 2) => 1 + 2
In our modified example the values we are passing into the function and the expression they form are clear, and a single result is returned from the expression resolving and as such is side effect free.
A Simple Example v3
// in the math.js file const add = (a, b) => a + b const subtract = (a, b) => a - b const multiply = (a, b) => a * b const divide = (a, b) => a / b
Don't they just look so wonderful and 'clean'. . .
The third and final principle we will be covering is that of higher-order functions. Higher-order functions allow for functions to return other functions, as well as take in first-class functions as arguments. This is where the magic starts to happen with Functional Programming, check out this example:
A Simple Example v4
// in the math.js file const add = (a, b) => a + b const subtract = (a, b) => a - b const multiply = (a, b) => a * b const divide = (a, b) => a / b const equation = (a, b) => subtract(multiply(add(a, b), divide(a, b)), add(multiply(add(a,b), divide(a, b)))) equation(add(1, 2), subtract(5, 4))
Well reader? Is this pure? Is this perhaps even 'clean'?
With these principles in mind a pattern begins to emerge, but lets pause here to address a core principle of functional programming, namely the concept of Immutablity, a concept that can be easy to express and talk about in theory, but complicated to implement.
Now that we have a base understanding of functional programming lets move one to the other paradigm we will be looking at The Notorious O.O.P
From the wiki
Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods).
In Object Orientation the idea of the 'Object' reigns supreme (I know, shocking right?) but what do we mean when we talk about 'Objects', well, an object at its core is simply a data structure that is able to house data(values) and methods(functions). However this data structure is capabale of something special, but more on that later, first let's unpack this relationship between Objects, data, and methods. So lets talk about Encapsulation.
Encapsulation is the principle of scoping data(values), and methods(functions) to a particular object, often(but not always) defined as a 'class'. While this may or may not be THE core priniciple of OOP it will be essential later on, so add it to the pile of things to keep in mind as you read on. By encapsulating our code in classes we are able to seperate out the concerns of each object, meaning that data(values) and methods(functions) are grouped together by their relevence to each other. Lets revisit our example, (I will be switching to ruby for these examples, but the concepts are more important than the particular syntax)
A Simple Example v5
# in the math.rb file class Math def initialize num_a = 1 num_b = 2 num_c = 3 num_d = 4 num_e = 5 num_f = 6 end def add(a, b) a + b end def subtract(a, b) a - b end def multiply(a, b) a * b end def divide(a, b) a / b end add(num_a, num_b) subtract(num_a, num_b) multiply(num_a, num_b) divide(num_a, num_b) end
Here we see encapsulation in action. This class of math is fully self contained. We have a Math class(object) containing all of the Math methods(functions) as well as all the data(values) that are needed to perform the appropriate operations, very clean, much organized, such Object, etc. As a quick aside, classes are often(but not always) given their own file in the code base, this is a convention that can be applied or ignored as needed (as with most, if not all conventions in coding). It is important that as written this code can only be executed inside of itself, no other code within the larger (purely hypothetical) code base we are working with, so if we had multiple classes within a single file they would each be encapsulated with there own scope. So, is this code 'clean'?
Of course if we have code that closed off from the rest of our code base, that obviously makes having complex programs that do more than one thing a bit tricky. This is where our second principle comes into play, the concept of Inheritance. With Inheritance we can designate a parent and child relationship between two classes allowing the child to utilize and of the data(values) and methods(functions) of the parent. But lets get a little more abstract with our example
A Simple(?) Example v6
# in the math.rb file class Math < Numbers def add(a, b) a + b end def subtract(a, b) a - b end def multiply(a, b) a * b end def divide(a, b) a / b end add(@num_a, @num_b) subtract(@num_a, @num_b) multiply(@num_a, @num_b) divide(@num_a, @num_b) end class Numbers def initialize @num_a = 1 @num_b = 2 @num_c = 3 @num_d = 4 @num_e = 5 @num_f = 6 end end
In the example here, the Math class(object) is inheriting the values of @num_a, @num_b, etc. with the use of inheritance we are able to separate our program into larger concerns beyond itself allowing for classes(objects) to remain focused on their particular concern while still having access to and operating on code outside of their own scope. With this our program is capable of increased depth of functionality while maintaining 'cleanliness'.
By utilizing these concepts we are able to use metaprogramming and abstract out our code(sorry no particular example for this one) in an effort to make our programs as light weight as possible without sacrificing depth of functionality or readability.
*Insert sick neat meme here *
By writing methods(functions) with an eye towards purity we are able to create complex procedures that produce complex outputs that are clean and predictable,