DEV Community

The Art of Clean Code: A Practical Guide to Writing Maintainable JavaScript

Nozibul Islam on November 17, 2024

The Art of Clean Code: A Practical Guide to Writing Maintainable JavaScript. Introduction: Writing clean code is more than an...
Collapse
 
brense profile image
Rense Bakker
function getRandomNumber(randomGenerator = Math.random) {
    return randomGenerator();
}
Enter fullscreen mode Exit fullscreen mode

This is of course something you should never do... Function wrappers are evil. Just call Math.random() directly.

Maybe what you mean to say is something along the lines of: "use some form of dependency injection to improve testability."

Collapse
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

This was an example of creating something that is testable. In this case, he's using Math.random since it's a great example of non-deterministic code which is horrible for unit testing. You don't want unit tests that fail sometimes and pass others, nor do you want meaningless ones. I think it might have been more obvious if the function was doing more than simply wrapping Math.random, but beyond that, it was an excellent example of doing di well (although, I'd personally have probably put it into a special object literal that I'd have called di or something that would make it clear and obvious that this is what it's for).

It probably would have been even better to show an example with an import, as this is a great way to handle di with imports. The default is the import, but it can easily be overriden with the injection.

The only thing that might be even better would be to create a closure around it so you don't need to continuously reassign the value. Or, if you must use classes (not my recommendation in JS, actually), then use a singleton or similarly scoped class.

Collapse
 
wass_supp_6e189b8fb3b6d16 profile image
Wass Supp

Ok Very good - I was really struggling why he had chosen such a for lack of better language random function to simply be returned. Difficulty again I may be missing something that should IMO never be called good. Maybe a clearer explanation in the op. Logical but if you know its not an issue. Great work though

Thread Thread
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

Look at my response to @brense, in that I suggest doing something in the function that handles a specific scenario that might occur with the random function.

If you have that, it would better reflect the reason for di (which I usually prefer over import mocking).

Thread Thread
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

The core purpose of Dependency Injection (DI) is to handle specific scenarios that might occur with random function generation. For instance, if a random number falls outside a desired range, you could re-generate it or handle exceptions. Such logic would demonstrate a more meaningful use of DI.

Thread Thread
 
brense profile image
Rense Bakker

If you need Math.random to return a specific number for one of your test cases, you just do:
Math.random = vi.fn(() => 0.1)

Dependency injection can be useful and can simplify testing in certain cases, but this is not one of them.

Collapse
 
brense profile image
Rense Bakker

Yes, that's why I added the second paragraph of what I thought they were trying to explain... Imho the code examples are really not clear or in this case just wrong...

I disagree with your statement to add unit test specific code to the function... If your function behaves different in unit tests, compared to production, the unit test is irrelevant.

Thread Thread
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

I have to disagree with the irrelevant part.

There are plenty on times when you have to test that code that "could" happen in production is properly tested.

Let's imagine that the code that is using the random function does something in particular if the value returned is less than .01.

This is something that "could" happen in production.

If you test the Math.random result directly in tests, it may randomly produce that value approximately once every hundred times. That means your unit test will fail most of the time and pass sometimes. That's a useless test. You need to be able to pass in a scenario where the unit test can check that the function works correctly when the value produced by Math.random is less than .01 by explicitly giving it a value that is less than that. Similar patterns are required for testing time based methods and more. You can't have tests that do a particular thing on Monday only work one day of the week.

I think we can both agree, the provided example isn't as good because it doesn't showcase something like this and so, we're simply testing Math.random itself, which, I agree is useless to test. Also, if we're attempting to show that it actually is returning random values, we would likely need a different kind of test than a unit test.

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

The primary goal of Dependency Injection (DI) is to make code more testable. For non-deterministic functions like Math.random(), injection allows for more predictable and repeatable tests. In the future, more advanced DI can be achieved using imports or closures, making the code more flexible and easier to test.

Collapse
 
jvnm_dev profile image
Jason Van Malder

What about mocks?

Thread Thread
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

Yes, mocks are another alternative, however, they don't have as much longevity.

Let me provide an example.

Node used to use common js, now it's primarily using esm. And we are starting to see other tools like Deno that work differently still.

If we used di like this, much less code would need changing when moving from one paradigm to another. Whereas if we were utilizing one of the many mocking solutions that mock the globals or requires/imports, we introduce way more variables. I think you still have to do this sometimes, and di does not necessarily preclude mocking, it mainly tries to get away from the more magic aspects of mocking that aren't always so obvious and that often rely more on the runtime being used.

Thread Thread
 
brense profile image
Rense Bakker

I think this all boils down to: the right tool for the job. Sometimes you want dependency injection so you can test functions that may act different depending on which context they are executed in.

function getDbClient(){
  // ... some logic to switch databases or clients based on env vars for example ...
  return { type: 'postgres' }
}

function doSomething(someParameter, injectDbClient = getDbClient){
  const dbClient = injectDbClient()
  if(dbClient.type === 'postgres'){
    return 'do sql stuff'
  } else {
    return 'do something else'
  }
}

// test the default code branch
it('should return sql stuff', () => {
  const result = doSomething('123')
  expect(result).toEqual('do sql stuff')
})

// test specifically what happens if we inject the mongodb client
it('should return something else', () => {
  const mockInjectMongoDbClient = () => ({ type: 'mongodb' })
  const result = doSomething('456', mockInjectMongoDbClient)
  expect(result).toEqual('do something else')
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Your comment is completely incorrect. This function is an excellent example of dependency injection.

function getRandomNumber(randomGenerator = Math.random) {
    return randomGenerator();
}
Enter fullscreen mode Exit fullscreen mode

Advantages of this approach:

  • Highly beneficial for testing
  • Allows passing mock random generators during tests
  • Makes the function more flexible
  • Simplifies function testing

Example:

// During testing
function fixedRandomGenerator() {
    return 0.5;
}

const result = getRandomNumber(fixedRandomGenerator);
Enter fullscreen mode Exit fullscreen mode

This is an efficient design pattern that promotes:

  • Testability
  • Modularity
  • Separation of concerns

The default parameter Math.random ensures normal operation, while allowing custom generators when needed - a standard dependency injection technique in JavaScript.

Collapse
 
brense profile image
Rense Bakker • Edited
// Using dependency injection to create a fake code branch.
it('should return 1', () => {
  const mockRandomNumberGenerator = vi.fn(() => 1)
  const result = getRandomNumber(mockRandomNumberGenerator)
  expect(result).toBe(1)
})

// No functional difference by just mocking Math.random directly.
it('should return 1', () => {
  Math.random = vi.fn(() => 1)
  const result = getRandomNumber()
  expect(result).toBe(1)
})
Enter fullscreen mode Exit fullscreen mode

Both these tests are nonsense because getRandomNumber() doesnt do anything except wrap Math.random… Math.random is already properly tested by the EcmaScript people.

Yes you can use dependency injection for testing where it makes sense. However, your example is poorly chosen and doesnt make sense 🤷 as per my original comment: you should never do what you did, but perhaps you meant to say that you can use dependency injection to simplify testing in certain cases.

Collapse
 
armando_ota_c7226077d1236 profile image
Armando Ota

I agree .. all this hidden "magic" is so fubared after you come back on a project after a while ...

Collapse
 
thomasbnt profile image
Thomas Bnt

Hello ! Don't hesitate to put colors on your codeblock like this example for have to have a better understanding of your code 😎

console.log('Hello world!');
Enter fullscreen mode Exit fullscreen mode

Example of how to add colors and syntax in codeblocks

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

wow great, thank you so much.

Collapse
 
drew_e29d5b0152adc2 profile image
Drew Riley

These are all really great examples ( obviously #3 as an example of dependency injection rather than an actual way to generate a number) of clean code that is not slow code. But keep in mind the term clean code often is synonymous with slow code. Don't abstract just for the sake of it.

Collapse
 
ram_taylorabigadol_421ad profile image
Ram Taylor Abigadol

Great article!,
I love the focus on readability it is by far once of the most underrated issues today as javascript is so flexible you can do anything in n^2 ways.
I also love that you did give room for the question of readability vs performance, most article choose one or the other.
The best thing I can say for this article is that I think it should be a basis for code review processes.
so shkran.

Collapse
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

This is one of the best articles on clean coding I've seen for JavaScript in a long time. There's a couple areas that could probably be described in a bit more detail so that the intent isn't so easily misinterpreted, but overall this is really good.

I've been writing JavaScript since the year it came out. Over the years I've discovered things that work and things that don't. The things you have here are in the group of things that work (when applied properly).

You even included one that always seems to throw people off (the one under scalability where you are using an object literal in place of a switch or multiple if/else blocks). I love that one. Once people understand it, it's very elegant and, in my experience, frequently faster than the alternatives.

I think there are even more things that can really improve the JS experience, but so far, this hits most of the major things. Kudos.

Collapse
 
gopikrishna19 profile image
Gopikrishna Sathyamurthy • Edited

Everything looks great! I follow clean coding/coder principles as well. Great job 🤜!!

However...

No 3. Testability example? Um.. uh... No easy way to say this 😵‍💫: Nope. I was reading in a mobile, the code was cut off, and I thought you were sending in a seed for predictability, but sending in a function that replaces the only job of the system under test? Nope. Can you give a better example, please? Like, Math.random() takes a seed, send that as argument from test file.

I understand this is only a 3 line example, but IMHO, the example, to the untrained mind, can present a whole list opportunities to get it wrong.

Testing is not hard, but people can make it hard. A weak test is no better than no test. Any code is testable. You wrote the code, you have the scenario in mind, set that scenario up and see how your code behaves. Repeat for each scenario. Finally, automate on each commit.

What I would suggest for a testability example is what you have in the explanation for No 3: Break it down. Modularize for testability. Cleaner dependency injection for predictability, like stubbing Math.random through external means and run the system under test.

Better yet, TDD. If you can't test it, you don't have to write code for it. That's how you get inherently test-able-ed clean code.

Happy coding ✨!

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

tnx.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

For configuration, my best TypeScript work so far (v3 in beta): wj-config

It excels in large and complex projects, but can also be used easily in simpler ones. The best part is that it works identically in NodeJS projects as well as browser projects, so it covers all of your JavaScript needs.

Collapse
 
skhmt profile image
Mike 🐈‍⬛

For #4, i'd argue a switch is even better than both other options. The first example with a ton of if statements is clunky and the exact reason the switch exists. The second example needlessly splits up code that makes it less readable and doesn't catch errors like the case not existing.

Collapse
 
piso_wifi_ad5a80a17a8d183 profile image
pisowifi • Edited

This sounds like an excellent resource for anyone looking to improve their coding skills in JavaScript! Writing clean, maintainable code is crucial for long-term project success. Some key principles that might be covered include:

Readable Code: Use clear variable names, consistent formatting, and proper indentation.
Modular Design: Break code into smaller, reusable functions or modules.
10.0.0.0.110.0.0.1
Avoiding Complexity: Write simple logic and avoid deeply nested structures.
Documentation: Add comments where necessary to clarify intent.

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

thank you.

Collapse
 
itamartati profile image
Itamar Tati

Great Article bro, Enjoyed reading, hope to see more from you!

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Thank you, brother! I really appreciate your feedback. I'll try to bring even better content in the future, Insha'Allah! 😊

Collapse
 
aniruddhaadak profile image
ANIRUDDHA ADAK

just wow ♥️.

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

tnx.

Collapse
 
ezekiel_77 profile image
Ezekiel

I think at point 2. The provided solution will still need a if-else chain especially when we need to attend to those specific errors from those asynchronous functions in the try clause

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

I agree with you. tnx

Collapse
 
programmerraja profile image
Boopathi

Thanks for sharing :) great work

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

welcome...

Collapse
 
marwan_rabi_2924d5c8578fa profile image
Marwan Rabi

we need a python version

Collapse
 
goran_shekerov_b498ca5e5a profile image
Goran Shekerov

I logged into dev.to just to give thumbs up on your expressivnes. Like!

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

wow. thank you so much brother.

Collapse
 
harshwaghmare profile image
Harsh Waghmare

The good code for Scalability,4, seems wrong

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Thank you, I checked again, everything seems to be correct.

Collapse
 
procoders profile image
ProCoders

Wow! Thank you!

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

welcome.

Collapse
 
sportz_fyy_1b730dfe74508f profile image
Sportz Fyy

The Art of Clean Code is a practical guide to writing maintainable JavaScript, focusing on principles and practices to make code more readable, easier to debug, and simpler to maintain over time. The guide follows SOLID principles, such as the Single Responsibility Principle, Open/Close Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. It also embraces modern JavaScript syntax, using ES6+ features for cleaner syntax, and using descriptive names and clear structures.

The guide emphasizes the importance of keeping functions short and focused, avoiding code duplication, using lining and formatting tools, writing comprehensive tests, handling errors gracefully, and using modules to encapsulate functionality. It also emphasizes the importance of documenting code, writing comments sparingly but effectively, and maintaining a proper README for projects.

Key takeaways from the guide include the importance of clean code, writing with the future in mind, and incorporating tools, best practices, and regular refactoring to keep the JavaScript codebase clean and efficient. By adopting these principles and practices, developers can master the art of writing maintainable JavaScript that is not only functional but also enjoyable to maintain and scale.