Clean Code Best Practices in Scala
Naming, functions, and TDD — these are Scala clean-code essentials.
Join the DZone community and get the full member experience.
Join For FreeThis article was first published on the Knoldus blog.
If you are reading this blog, that's most likely due to two reasons: You are a new Scala programmer, and/or, you want to become a better Scala programmer.
Even bad code can function. But if the code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and resources are lost due to poorly written code. But it doesn’t have to be that way.
So, there might be a question popping in your head: what is clean code?
Clean code always looks like it was written by someone who cares. Clean code means the logic is straightforward and makes it hard for bugs to hide, which means that they can be read and identified by a developer other than the original author.
So, there are some basic Scala clean-code rules that we will discuss in this post. Let's get into it!
1) Meaningful Names
It is easy to say that a name should indicate its intent. Choosing names can take time, but these names will save more time in the long run. Everyone who reads your code will be happier, including you. Properly naming code can improve consistency, clarity, and code integration. The name of the function or class should answer all of the big questions. Suppose your variable name is:
val x = 10 // This variable name reveals nothing
The name “x” doesn’t reveal anything. Therefore:
Use Intention When Revealing the Name
We should choose a name that specifies what is being measured and the unit of that measurement.
val elapsedTimeinDays // This reveals what is being measured and unit of measurement
Programmers must avoid leaving false cues that obscure the meaning of the code. We should avoid words entrenched in meanings that vary from our intended meaning. For example, hp
, aix
, and sco
would be a poor variable name. So, avoid misleading names, which could create confusion:
def getAccount ()
def getAccounts()
def getAccountInfo()
Use a Pronounceable Name
Also, you should use a pronounceable name. For example, a name like genymdhms
means (generate date, year, months, day, hours, minutes, seconds), so we can’t walk around and say “gen why emm dee aich emm ess,” which is very hard to discuss. This could, instead, be written as: generateTimeStamp
.
You also don't need to prefix member variables with m_ anymore
. Your classes and functions should be small enough that you don’t need them. And you should be using an editing environment that highlights or colorizes members to make them more distinct. Prefixes become an unseen clutter and a marker of older code:
class Part {
var m_dsc = "manager";
def setName(name:String){
m_dsc = name;
}
}
class Part{
var description:String = "Manager";
def setDescription(description:String) {
this.description = description;
}
}
Classes and objects should have a noun or noun phases name, like Customer or wiki page. Additionally, avoid names like Manager, Data, and Info. A class name should not be a verb.
Methods, however, should have a verb or verb phrase as the name, like post payment, deletePage, or save. Accessors, mutators, and predicates should be named based on their value and prefixed with getting, set, etc. Pick one word for one abstract concept and stick with it. For instance, it’s confusing to have fetched, retrieve, and get as equivalent methods of different classes.
Here is an example of a method:
def genymdhms(t: Any): Timestamp = {
val d1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS")
val d2 = date1.parse(token_exp.toString)
new Timestamp(parsed2.getTime)
}
Clean code would look like:
def getCalendarTimeStamp(token_exp: Any): Timestamp = {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS")
val parsedDate = dateFormat.parse(token_exp.toString)
new Timestamp(parsedDate.getTime)
}
2) Functions
“Functions should DO ONLY ONE THING. And they should do it well."
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than small. So, this means that your function should not be large enough to hold nested structures. Therefore, the indent level of a function should not be greater than one or two. This technique makes it easier to read, understand, and digest.
Your function can take also the minimum number of the argument, which is zero. Such functions do not depend on any input, so there’s always going to be some trivial output from it. However, it will be easy to test.
Next comes one (monadic), which is based on functional programming rules, followed closely by two (dyadic). Three arguments (triadic) should be avoided wherever possible. More than three (polyadic) requires very special justification, and even then, they shouldn’t be used anyway.
However, in case the number of arguments are more than three, we should group them into a meaningful object, like so:
def registerUser(name: String, password: String, email: String,address:String,zip:Long): String = {
implicit val session = AutoSession
dBConnection.createConnectiontoDB()
val token = UUID.randomUUID().toString
import java.util.Calendar
val calendar = Calendar.getInstance
val token_gen = new Timestamp(calendar.getTime.getTime)
calendar.add(Calendar.MINUTE, 30)
val token_exp = new Timestamp(calendar.getTime.getTime)
withSQL {
insert.into(UserData).values(name, password, email, address,zip,token, token_gen, token_exp)
}.update().apply()
token
}
This can be written as:
case class User(name: String, password: String, email: String,address:String,zip:Long)
def registerUser(user:User): String //
3) Test-Driven Development (TDD)
The primary goal of TDD is to make the code clear, simple, and bug-free. In a TDD approach, first, the test is developed to specify and validate what the code will do.
How does TDD work?
- Write a test
- Make it run
- Change the code to make it right, i.e. Refactor.
- Repeat the process
By now, everyone knows that TDD asks us to write unit tests before we write production code. But that rule is just the tip of the iceberg. Consider the following three laws:
First Law: You may not write production code until you have written a failing unit test.
Second Law: You may not write more of a unit test that is sufficient to fail, and not compiling is failing.
Third Law: You may not write more production code that is sufficient to pass the currently failing test.
Finally, there should be a single concept per test and only one assertion per test, which is said to be good practice.
Here is an example:
"document is empty" should {
"not be able to convert a document into an entity" in {
val result = UserDataDao.documentToEntity(Document())
assert(result.isFailure)
}
}
References
- Clean Code by Robert C. Martin
Published at DZone with permission of Shubham Dangare. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments