Tales of the JavaScript Monk: The Discipline of Good Code
Prologue:
I thought about going full Dr. Strangelove with the title for this post: "The JavaScript Monk: Or How I Learned to Stop Showing Off and Love Boring Code." There were just so many things wrong with that title I couldn't use it with a clean conscience. First, it's not boring code that I love. It's clean, declarative, obvious code that I love, which can often be pulled off in very clever and interesting ways. Second, who am I kidding? I do like to show off every now and again; as long as I am able to think of some reasonable defense for my cleverness. So, really there was no reason for me to use my Dr. Strangelove title. Don't know why I even mention it.
Have you ever seen a piece of code that expressed it's complexity in such a wonderfully simple way it just seemed like that was the way that code was meant to be written? There would, in fact, be no other way to write it. Yet, however, most code we look at is garbage; a collection of spaghetti and vomit spewed forth during some red-bull-induced frenzy of panic under the shadow of an approaching deadline. The nice code seems so much easier to write, but, alas, that doesn't appear to be the case.
Chapter 1: A Path Well Traveled
Among the many things that make web development interesting is the amount of professional diversity among web developers. Many of us didn't come to web development through the usual roads people take to software engineering. I, as it so happens, have a degree in fine arts. This road isn't so unusual. Many web developers find their way to application development using design as a gateway. Even beyond that, many more people are drawn to web development specifically by an interest in developing user interfaces. We want to build something cool, nice-looking and engaging for our users.
Because of our platform of choice, we use JavaScript to implement these user interfaces. The first significant application I wrote was in JavaScript. There are many things to love about JavaScript. The bar for entry is very low. It is incredibly easy to write only a few lines of code and have something running in the browser. There is no set up. No compiler. No configuration. I have a web browser and a text editor. I can get something running. JavaScript is also incredibly forgiving. If you write something, the browser will try like hell to run it. I don't have to use semi-colons. I haven't even heard of strict mode yet, so I don't even have to declare variables before using them. I declare this thing here and I can use it anywhere and everywhere. Global scope is awesome; not that I know what global scope is at this point. Again making it easier for new programmers to pick up and play with. As a new comer it is important to get this instant feedback, to not be intimidated by the bar for entry and to not feel quickly discouraged. I don't know what I'm doing yet, but I'm at least able to run something. I click this and this other thing appears. Awesome. I did that without knowing anything. JavaScript offers this.
JavaScript is also incredibly expressive, making it fun and engaging even after you've become a confident programmer. You can do amazingly cool things by extending native types, manipulating function context and leveraging first-class functions. So, you don't like the prototype chain? Come up with your own inheritance mechanism. This flexibility makes it an inviting playground; and creative people love playing.
Not only does JavaScript have a low bar for entry, web development in general has a low bar for entry. You can start by building simple static web pages. Almost anyone can learn to do this in an afternoon of futzing around on their computer. Of course we can go from that to single-page web applications that are as complex as anything you would find on a native platform. Then there is all the levels of complexity between these two extremes that make web development an inviting place for people new to application development. We can take baby steps as our skills improve. And it seems no matter how far we have come there is always a new step to take.
Chapter 2: Free Love and Atomic Bombs
I think I'm in love. Web development is exciting and fast-moving. It's hard to keep up with the new APIs browsers are supporting, much less the ever-increasing number of JavaScript libraries and frameworks. Yes, it's fun to be a web developer at this moment. We are not burdened by the legacy that weighs on other languages and platforms. We are able to constantly embrace new technologies or even new programming paradigms. The platform we develop on is evolving, changing beneath our feet. We are not compiling to a specified version or architecture while the company management is too scared to let us compile to the new version of the thing so we can use the new stuff. We have to move to keep up with our platform. Last week I had to do this thing this way, but this week Chrome is supporting it natively so I can get 10x performance gains by using the native API. Even in old code bases we are able to add new features that take advantage of new technologies without updating the surrounding code. This constant newness is a prime reason web developers enjoy what they do. It's just always more fun to play with the new shiny.
With Great Power Comes... Oh, I Forget
Something that is relatively new to the JavaScript community is the computational complexity we are now packing into our applications. When we were working with jQuery plugins pushing some mess of code then rewriting it a month later was fine. When your application is tens of thousands of lines of code, containing dozens of connected modules, handling a huge amount of business logic that used to be owned by some Java engineer on the backend, you have to concern yourself with maintainability and extensibility. We should now be taking the responsibility of planning for a 2-3 year lifetime for our applications. The time and monetary commitment to our applications now don't allow us to be in a constant state of rewriting the features we've already coded. We should be able to quickly add features to our application as they are needed, reliably fix bugs when they are found and adopt existing code bases when we switch jobs, teams or projects. Yes, boys and girls, it's not all about building new stuff now. More and more our jobs are going to defined by taking the responsibility of other people's code bases and planning for the future of our own code bases. Web developers aren't just flinging carrots at the kid's table anymore. We are application engineers responsible for keeping large code bases and significant business logic in a good state.
It's at this moment that JavaScript and our community start to betray us. All of the features that make JavaScript so easy to learn and use are the same features that empower us to be lazy and undisciplined. The recognition of this problem is almost as old as the language itself. One of the first books suggested to people learning JavaScript is Douglas Crockford's "The Good Parts." This book describes a discipline; the discipline that our code will, in general, be better if we stick to using a subset of the language. This is just one discipline that JavaScript developers may find useful. It's important when writing software to always have an idea of why you are doing what you are doing, not just in terms of the logic behind the lines of code you write, but the formatting you use to write them and the libraries and frameworks you choose to help you.
The immaturity and diversity of our community cause us to try and resolve the same problems Java engineers solved 10 years ago, or C++ engineers solved 20 years ago, or C engineers 30 years ago... and so on. There is a long history of software development. We should be open to learning and growing from it. When you set out to be a web developer, JavaScript engineer, front end engineer, whatever job title you most associate with, it is important to understand you are a software developer, a profession with a much longer history than web developer. You should understand the broader principles of good software, not just follow some tutorial on the hottest JavaScript framework. Why is that framework good? What problems does it solve? Do yourself a favor and learn other languages and platforms. You will bring what you learn back to the next problem you solve in JavaScript.
Gross Hack #5
Any developer, regardless of platform or language, will be better if they approach their craft with a sense of discipline. However, when writing JavaScript you have nothing else to guide you. You don't have static types. You can throw values up to the global scope at any time. You can modify the behavior of native types. You can change the context functions operate in. At times these may seem like great things that make your job easy, but without a careful discipline they make JavaScript an inviting place to solve problems with the hackiest solutions and to break from convention without reason. Perhaps making your job easy today, but leaving your application weaker, harder to maintain and just waiting to explode with a bug you and your team will have little hope in tracking down.
I have a deadline, or I have a bug in prod, either way something needs to be fixed or built right now. I have this one value in this module, but now I want to use it in this other module. I don't see how to wire this through. I could sit here and take the time to think this through and do it right. No, I need this done. Well, I could share it globally. No, no, I won't be that bad. I'll create this intermediary store that has nothing to do with our application architecture but what the hell; I'll just do it and put a todo to fix it later. Undefined is not a function? What the hell do you mean undefined is not a function? This has been working in prod for months. There's no way this is my fault. Let me see what's going on here. That shouldn't be undefined. How is that ever undefined? I don't have time to track that down now. I'll wrap it in a conditional and leave a todo comment to fix it later.
JavaScript doesn't do anything to make you think before you start writing your application or implementing that new feature. It doesn't even force your code to make sense. That function expects a number, you give it a string... whatever. That function doesn't expect a number. You expect it to get a number. This other function takes four parameters, but you only pass it two. It'll try to run anyway. You have to make the decision to sit down and plan how best to proceed. Stop and think. A lot of people start an app by code sketching. That is just sitting down and writing code with only the most high-level understanding of what needs to be done. It's just those code sketches become apps that are eventually declared to be finished, pushed to prod and may accidentally work... for a while... until we need to dig into the code to extend or fix it.
Another form of code sketch comes from our obsession with the new shiny. Many applications begin with an arbitrary decision to build an application in a given framework just because it's the favorite framework of the loudest developer, or, even worse, there is no expert on the team, the developers on the project just want to try it out. There are many good frameworks. All of them solve problems in different ways and have different strengths and weaknesses. The decision to write an application in a specified framework should be made with thought and planning. The chosen framework should solve the problems important to the given project, not satisfy the curiosity of the developers. All the developers should be comfortable picking up the framework and there should be a developer able to take on the role of architect and guide the project. It won't leave anyone in a good spot six months down the line when you're dealing with code written by a bunch of developers who didn't know how to use the framework. We thought that was the best way to build it. We were wrong.
Chapter 3: Mystical Knowledge isn't all that Mystical
Writing complex applications in JavaScript is like preparing to run a marathon by cutting off your right foot. We have this wonderful playground, with all this new shiny and we should be excited. We also have a bunch of production bugs and developers on our team who keep trying to fix those bugs by adding lines of code to previously beautiful single-purpose functions that now do six things, touch global state and are border-line untestable. That's not to mention the handful of spots in our code that we missed updating code to account for the new properties on this one object. Oh, and we still have testing as a todo. Well then, it's time we thought about what rules/limitations we're willing to accept to make this whole process more reliable.
Types? You're in the Wrong Language
Just the thought of type-checking makes many web developers cringe. I actually witnessed one JavaScript developer start to gag when he tried to utter the words. Hey man, we don't need none of your boxed-in ideas about types. Let the variables be what they want to be. Once we start doing all the crap other languages do, things just won't be fun anymore. This is fine for small applications, but there is real value to type-checking in large applications that are maintained over a long period of time by a team of developers. Aversion to type-checking is at best a laziness that says I would rather deal with bugs later than fix what a compiler tells me is wrong right now. At worst I'm an idiot that doesn't think about types at all.
It's something the JavaScript community is more open to now as we have more experience with large applications. In frameworks, like React props or Ember.Data, we see runtime type-checking. We also have tools like Flow or a number of compile-to-JS languages that offer type-checking. There is value in knowing if you change a type you don't have to wait for a unit test or a runtime error to catch that your application can't handle the new type. You've got some refactoring to do. Type-checking lets you know that your application is wired properly. All functions are getting the types they expect; all variables are being assigned to values that make sense for their purpose. These are all bugs that will appear in your code when writing JavaScript.
Types also make it easier to communicate with other people about your code. Instead of saying this function takes a hash with these keys, or just generally saying it takes an Object, you have a concrete type. Everyone on your team can look at that type. It's a contract put forth by the code. This function will work with this type. A contract that is enforced at compile time; not some loose suggestion that can turn into a production bug a month from now. The larger your code base the more valuable some level type-certainty is.
So Yeah, We Don't Have That
How do we compensate? First, we have to unit test our wiring. We have to make sure functions work together, not just in isolation. It's so much easier to write unit tests in isolation, but we have no guarantee arguments are being passed properly. We have no guarantee that a function is actually getting numbers instead of strings. We have no guarantee function A is passing all the arguments it needs to function B.
Second, we can strive to keep our functions referentially transparent. This means our functions only operate on the arguments they are given. It doesn't rely on or modify any outside state. Our functions take a specified set of arguments, operates on them and returns a result. That is it. This is also called writing pure functions or stateless functions. Before you write stateful functions exhaust your options for writing stateless functions. This means don't fix a problem by just throwing some code into an existing function. Think about where the new code should go. Is it performing a new task? Does it alter the flow of what was happening before? Don't be afraid to refactor if necessary. Also, as you push state out of functions, centralize state to as few places as possible. This reduces the number of places you can introduce a stateful bug. If you're updating state in many places in your app it becomes likely you'll introduce a conflict over which spot should be updating the state at any given time. It also complicates finding a bug should one arise. If you strive to keep the bulk of your code base stateless you will witness huge long-term benefits. This will make your code simpler, easier to reason about and easier to test. You can think about your functions as the values they return instead of as the actions they perform.
Third, don't abuse the lack of static types. It is likely a code smell if a single variable is used to represent multiple types. If you have functions that are using a variable to represent multiple types this probably means a function is handling multiple computations and should be refactored into different functions for each of those computations. Why is that variable representing multiple types? Is it necessary for it represent multiple types? Probably not. Just because we don't have static types does not mean we shouldn't think about our types. Giving a little thought to how you are handling your types will help you to think more critically about how you are building your application and make it easier for you to reason about your application as it grows.
Let all the Things be Simple
Something else you are bound to run into when building large applications are developers who like showing off how clever they are. You may even realize that you are this developer. That you get a real kick out of writing things in the most clever way possible. Yes, it's cool you were able to write that as a one-liner; but I wouldn't know what it was doing if you weren't here to explain it to me.
Working with expert developers is fun and rewarding. Striving to be an expert developer should be the goal of all web developers. However, don't confuse being clever with being an expert developer. As an expert developer one of your responsibilities, if not your key responsibility, is to keep your code maintainable and extensible. Keeping code simple and declarative is key to maintainability. Remember, you write code to be read by other developers. Everything else being equal, don't do something the most clever way if it is not the most clear way. We don't need to write the most concise code. There are many good JavaScript minifiers and optimizers. When writing code, our first concern should be that it is correct. Beyond that, make it as clear and easy to understand as you can. Not only will other developers appreciate it, you will appreciate it when you return to it later.
No One is Going to do Any of This
When developing a large application you will have multiple engineers laying their nasty little fingers all over the code. It is important that each of them write consistent code. Have your team adopt a style guide. If you don't have one, many potential JavaScript style guides are online. This isn't your weekend project. This is a project we all have to work in. It makes things easier to read if we're all writing code that looks the same. Double quotes or single? Semi-colons? Curly braces required for conditionals? Spaces or tabs? It doesn't matter, everyone is going to be miserable. All developers think the way they write code is the best and don't see the point in a style guide. We're not writing code for ourselves anymore. This is a large app with many developers. We're writing for everyone. It will make things easier when we're reading through the code to not be interpreting the favorite formatting of a dozen different people.
That looks weird. Why is this function doing that? What idiot wrote this? I sure wish whatever moron wrote this would've left a comment to explain this crap. Git blame. I wrote this? There's no way I wrote this. I don't remember this at all. Wow, what the hell was I thinking? Document your code. It can be good practice to document your code before writing your code. Describe in simple, step-by-step, language what something should do. This will often help you better understand what you're doing before you've got some crap code running. You'll notice holes and redundancies in your logic when you're trying to document it. At any rate, get in the habit of documenting your code as you write it. It's fresh in your mind. You know what you're doing. Liberally comment any code that may be even the least bit opaque. What may seem clear now may not seem clear a month from now, or may not be clear to another developer. Just a brief description will help everyone. The probability of returning to document it later is very low. Documentation and unit testing are how we define a contract between our code and other developers, including our future selves, about what our code should do.
Conclusion: So, That's It?
The theme then is JavaScript is just so easy to sit down and start hacking on it becomes very difficult for many developers to resist the urge. Yes, it is fun to sit down and just hack on an idea. No, no one has ever said unit testing or documentation are fun. Sitting and planning my code for an hour when I could just throw some working crap together in five minutes is not the easiest thing in the world to do. However, these are the things that will make you and your team more productive over the life of an application. This thing is nearly 100,000 lines of code. We're not just going to rewrite it next week. There are going to be new developers that come on a year from now that are going to need to jump into this. Do you want to sit and explain to them every function and what it should be doing? Do you want another developer here who's adding code to update application state deep in some module that no one has touched in months? The fun is hacking on JavaScript. The discipline is doing all the things now that will save you time six months from now so you can do some more cool hacking instead of figuring out exactly what the hell some undocumented/stateful function is supposed to do and why it isn't doing it now.
Staff UI Engineer at Aurora - Crafting tools make vehicles drive themselves
8 年No idea what to say after reading this. Too many truths being said here. I'll tell you my thoughts face to face tomorrow, as I sit right behind you now. All I can say here is that this is the most enjoyable 15 minutes I have spent reading some computer programming crap in a long time, you bastard bearded hippie! Now, let's fix the damn Pac-Man machine at the office, please.
Tech Lead, Mentor, Accessibility Advocate, Occasional Teller of Dad Jokes
8 年Great post Kevin.