Programming JavaScript Applications By Eric Elliott - Revisited
A few years ago, I read the "early release" edition of Programming JavaScript Applications by Eric Elliott. At the time, the book was only a fraction complete, so there was very little to review (shame on O'Reilly's early release program - what a diservice it does). Now, three years later, I've revisited the completed book and, let me say, this book is rather fantastic. While the title might lead you believe that it's solely about client-side applications, this book is actually a full-stack exploration of application architecture, touching on everything from JavaScript fundamentals to modularity to security to RESTful API development.
|
|
|
||
|
|
|||
|
|
|
I don't necessarily agree with everything that was said in this book; but, it is definitely a book that I would recommend to JavaScript developers of all levels. At 435 pages (in iBooks), the book has incredible breadth. And yet, at the same time, it still manages to go into good depth on certain topics like functions (in all their diverse glory), object instantiation, Express, security, and RESTful API development.
Obviously, the focus of the book is JavaScript. And, since almost all development these days involves some kind of JavaScript (client-side, server-side, embedded code, internet of things (IoT), etc.) this book is certainly relevant. But, even if your server-side language isn't JavaScript, this book is still great; while all of the back-end discussions are had in the context of JavaScript, very little of it is unique to JavaScript. Meaning, security, message queues, feature toggling, internationalization, logging, security, API development, security, event-driven programming - this is relevant to everyone that is building web applications today.
Now, I happen to revisit this book after Eric and I had an interesting discussion on my blog post about the use of method-chaining with the revealing module pattern. So, for me, the topics of inheritance and object instantiation where of particular interest. Clearly, Eric has some strong feelings about the way some of us do objects in JavaScript:
Programmers with a background in other languages are like immigrants to JavaScript. They often code with an accent - preconceived notions about how to solve problems. For example, programmers with a background in classical OO tend to have a hard time letting constructors go. Using constructor functions is a clear and strong accent, because they are completely unnecessary in JavaScript. They are a waste of time and energy. (Page 91)
I'm not a classically trained programmer. Nor, do I have much confidence when it comes to things like object oriented programming (OOP). So, I rather enjoy it when I'm challenged to question and examine the way I do things. In JavaScript, I happen to like constructor functions. For me, they provide a great way to set up the state of an object in a way that the calling context doesn't have to know about.
In the book, Eric presents his Stampit library as a way to combine the "best of" features surrounding object relationships in order to create an approach that easily allows for encapsulation, prototypal inheritance, and privileged methods. I've spent a few hours reading through the Stampit source code (you should check it out) and it does some very interesting and clever things. But, I'm still having a hard time thinking about creating objects without constructors.
Take, for example, trying to create a Stream that also exposes a Deferred value. Without a constructor function, who creates that deferred value that's associated with the stream? Not the developer consuming the stream - we definitely don't want the consumer to worry about the object properties at that level. So, maybe one of the "enclose" functions then (part of Stampit)? Of course, if we start using the enclose functions to set up state, aren't we just using a constructor function and calling it a different name?
I suppose that we could also create a factory function that encapsulates the use of Stampit. That way, the factory function could create the Deferred value and then inject it into the instance as an override to the .create() method (which will override the default, prototype state).
But, even if we go that route, I think we still come up missing a little something. Setting up the Deferred value is one thing; but, we also need to set up event handlers that wire stream events into promise events (ie, translating emitted events into Deferred resolution and rejection). Who does that? The factory function?
Mixing multiple objects together is definitely a hard thing - just look at the hoops Node.js has to jump through to create a Duplex stream (that inherits from Readable and Writable); I understand the problem that Eric is trying to solve. But, perhaps my preconceived notions about JavaScript are too engrained for the answer to be obvious. That said, the real beauty here is that Eric has truly gotten me to step back and think more deeply - more holistically - about how I architect things in my own JavaScript applications. And that is critical when it comes to evolving our skills.
Before I wrap up, I wanted to share a few other passages, from Programming JavaScript Applications, that I found thought-provoking:
On the "let" keyword in ES6. I personally do not understand the "let" keyword at all. It feels like it's trying to solve a problem that I don't have. As such, seeing this tip was very comforting:
TIP: The desire to use block scope can be a good code smell that indicates that it may be time to break a function into smaller pieces in order to encourage readability, organization, and code reuse. It's a good idea to keep functions small. (Page 47)
On handling the node_modules folder. This passage will, without a doubt, be highly controversial. But, my gut feeling is that Eric is correct when he says:
Since you'll be deploying this app, you should consider all of the code it uses to be part of the app, including its dependencies. You don't want those dependency versions shifting around under your feet during your deploy step. The less uncertainty and moving parts you have in a deploy step the better. For that reason, you're going to want to check in your node_modules directory (don't add it to .gitignore). (Page 158)
For me, there are two big reasons why this makes sense:
- I might have to go in and patch some code in a vendor library. Whether it be buggy or just lacking something that I need, I would have to be able to check-in the vendor code in order to get it deployed in production.
- I would think this gives us a great opportunity to de-dupe dependencies (though, huge caveat, I haven't yet written any production Node.js applications ... yet).
On handling [some] exceptions in server-side JavaScript:
Let it crash. Processes crash. Like all things, your servers's runtime will expire. Don't sweat it. Log the error, shut down the server, and launch a new instance. You can use Node's cluster module, forever (a Node module available on npm), or a wide range of other server monitor utilities to detect crashes and repair the service in order to keep things running smoothly, even in the face of unexpected exceptions. (Page 221)
Coming from a ColdFusion / J2E background, Node.js feels like a laughably brittle environment. As such, it's good to see that seasoned developers can embrace this nature rather than fear it. Now, I don't think Eric means that all errors should bubble up into exceptions; but, rather, that an application shouldn't be expected to gracefully recover from every single error that it encounters.
There's really way more in this book than I could cover in a single review. It has a good deal of stuff in here that I wish I had known about (or known to think about) when I started building more complex web applications. I would say that it is definitely worth the read.
Reader Comments
Thanks for this review, I've added it to my queue.
"Coming from a ColdFusion / J2E background, Node.js feels like a laughably brittle environment."
Big plus one. The first time I discovered that a bug in a Node.js app could kill the whole thing I was really surprised. :)
@Ray,
Ok cool, I'm glad I'm not the only one. When I saw that, I thought I was taking crazy pills :D Though, I guess it forces you to cover all your bases ... after your app crashes a bunch of times in production.
Great review. "I'm not a classically trained programmer. " This sentence really resonated with me. My programming experience can probably best be described as a big, huge melting pot. When I first started out, I studied some programming courses in college and was exposed to object-oriented programming, programming logic, a few languages, data structures, software development and engineering.
@Raymond Camden & Ben - I have experienced this (with the seemingly brittle environment) developing @ times using php.
As I was reading your review, some thoughts that went through my head re: the object orientation discussion as it pertains to my particular experience with ColdFusion. When I was first taught ColdFusion, I was taught cfml, of course. It was later that I was exposed to methods, cfc's, object orientation using ColdFusion, and mvc, to name a few. I was told when I was being taught these that ColdFusion didn't start out as an object-oriented language with cfml, but this methodology of programming using some of these constructs evolved, turning it into a powerful object oriented language that could be very useful in accomplishing the tasks we as programmers are asked to complete while developing various applications used throughout the web. Assuming this is true, is it that JavaScript (like ColdFusion) was not originally written with object-orientation in mind, but there are now these constructs which are allowing it to be a good solid object-oriented language?
Just a few thoughts swimming through my head as I read your review. Thank you. It does sound like it'd be a good book to check out and add to my list.
@Raymond,
Crashing is not unique to JavaScript. It's quite easy to crash C and C++ programs.
Java sometimes crashes due to unavailable resources or bugs in the JVM.
Because of these limitations, engineers designing for robust systems write code that can recover by restarting the application.
In mission critical designs (autopilot systems, for example), engineers write software with redundant instances and fast boots (sometimes called microboot architecture). There's a philosophy of engineering such systems called "crash only software."
For the record I've seen JS instance uptimes of more than a year, and you can achieve that too, if you're careful about code quality.
@Annak,
"is it that JavaScript (like ColdFusion) was not originally written with object-orientation in mind, but there are now these constructs which are allowing it to be a good solid object-oriented language?"
No.
In fact, it's more true that languages built on classes are more class-oriented than object-oriented, because classes are the primary element of relationship designs.
JavaScript could be described as more object-oriented because objects are the primary elements of relationship designs.
In classical OO languages, classes inherit from other classes and object instances are created with class constructors.
In prototypal languages, objects inherit from other objects, and object instances are created using exemplar objects.
A class is a blueprint.
A prototype is an object instance.
Prototypal OO is more powerful and more flexible than classical OO.
See "Common Misconceptions About Inheritance in JavaScript": https://medium.com/javascript-scene/common-misconceptions-about-inheritance-in-javascript-d5d9bab29b0a
Ben,
First, thank you for this new review. I'm glad you liked the book! I just have a few comments you may want to consider. =)
---
RE: constructor functions - factory functions have all of the capabilities of constructor functions.
"For me, they [constructor functions] provide a great way to set up the state of an object in a way that the calling context doesn't have to know about."
Factory functions can do everything a constructor function can do.
Stampit returns a factory function. You can move logic you would have put in your constructor functions into an `.enclose()` function.
"aren't we just using a constructor function and calling it a different name?"
No. Constructor functions have several disadvantages, but the most awkward from my point of view is that the `new` requirement can make it hard to refactor from a constructor implementation to a factory implementation: a refactor that is common enough to make it into Martin Fowler's "Refactoring" book. I believe that the `new` requirement is a violation of the open/closed principle (open for extension, closed to breaking changes).
See also: Common Misconceptions About Inheritance in JavaScript: https://medium.com/javascript-scene/common-misconceptions-about-inheritance-in-javascript-d5d9bab29b0a
---
Your question RE: "create a Stream that also exposes a Deferred value" is a little flawed for two reasons:
1. A normal factory function can do that.
2. With Stampit, an `.enclose()` function can do it.
3. The idea itself is a little awkward.
1 and 2 are self-explanatory as they're both direct answers to the question. Basically, a factory corresponds 1:1 with a constructor. They're both responsible for instantiating and possibly initializing an object. Anything you can do in a constructor, you can do in a factory. The difference is that factories expose a less awkward API.
3. A promise is equivalent to a reactive stream, except that instead of emitting possibly many values, it can only emit one (or an error). The stream itself could (and typically does) tell you when it's done emitting messages, so there's really no reason to expose a thenable promise. For much more detail on this topic, see the gtor: A General Theory of Reactivity. https://github.com/kriskowal/gtor
---
RE: `node_modules` - It's been a few years since I wrote that passage, and I have a lot more experience with Node now. I no longer check in `node_modules` because it creates too much noise in pull requests. Instead, I've refined our process for production release candidates. Now I recommend that you build an immutable release candidate on a virtual machine cluster, run `npm install` as part of that process, run all your pre-deploy tests, and after all the tests have passed, promote the release candidate cluster to production by swapping out the machines on the production load balancers.
This way, you go to production without running `npm install` again, so you know that the code in production is exactly the same code that passed all of the release validation tests.
I'm over-simplifying things, of course, but I've found that's the most reliable release process with the fewest opportunities for failure.
---
RE: "let it crash" - While it's possible that crashing may be less frequent in other languages, there is no such thing as a programming language where crashes are not a possibility. All web services -- regardless of the language they're written in -- should be built with crash-recovery in mind.
"Let it crash" is more prominent advice in Node because it's easy to make a type-related mistake like trying to access a property on an undefined variable -- but that does not mean that you don't have to think about it in Java or C++, or whatever your favorite language is.
Every service that needs high availability needs to be written with redundancy and crash recovery in mind. NASA doesn't use JavaScript, but they do plan for system failures.
@Eric,
I think I'm still a little stuck on where you would move certain functionality to. On your statement:
>> Stampit returns a factory function. You can move logic you would have put in your constructor functions into an `.enclose()` function.
Imagine that I need to set state based on a constructor function argument. Not store it directly, but set state based on it. If I am understanding the Stampit code properly, I would have to achieve that by calling the .create() method with an empty-object argument:
.create( { }, someArg, otherArg )
.... since the first argument is expected to overwrite state and arguments 2-N get passed, in order, to the various .enclose() functions. I think this will work, but it feels like I am trying to shoe-horn something together.
I think it gets even a bit more awkward if the .enclose() method is conditionally creating a public variable (ie, this-scoped). At that point, the very name of the enclose method feels awkward.
Re: streams + promises, I understand that they are different kinds of things. The intent of exposing a promise on a stream would be to allow for more uniform consumption in the calling context, where you might be using a whole promise-base workflow. That said, it was more to demonstrate a case where the internal bindings of an object were not simple value assignments. So, in my other post, where I am exploring Streams + Promises, I have constructor logic that looks like this:
this.once( "content", this._deferred.resolve );
this.on( "error", this._deferred.reject );
Again, I *could* put this in an "enclose" function; but that seems like an awkward place for it.
Maybe I just need to sit down an play with stampit a bit. Right now, I'm basing this all on just my review of the docs and source code. Things tend to make more sense when you sit down with it. I'll try to re-create my Stream + Promise exploration with Stampit in mind.
@Eric Elliott,
Thanks for responding/answering questions. I can appreciate your mentioning of how all code in all languages have the "ability" to crash, or tendency to do so. I've found this happening quite often within the .net framework w/ C#.net, ASP.NET, and VB.NET. It's a pretty frequent occurrence, so I didn't think it was specific to JavaScript. It does seem that some languages tend to do it more so than others. Thanks for the insight.
Why not just avoid wild cards in your dependencies rather than check the dependencies into a repo. This should keep your code set constant. Certainly having wild cards that allow the dependencies to change should be avoided.
@Don,
Because your dependencies usually also have dependencies, and those dependencies can specify ranges, so what works now may not work if a sub-dependency publishes a breaking change to npm between your acceptance tests and your deployment.
@Ben,
"Imagine that I need to set state based on a constructor function argument. Not store it directly, but set state based on it. If I am understanding the Stampit code properly, I would have to achieve that by calling the .create() method with an empty-object argument:"
I don't recommend doing that. This is my favorite solution:
https://gist.github.com/ericelliott/3c7e1c130b11c79f65b8
Just passing data as arguments to a constructor function doesn't work when you need to compose multiple stamps -- which is the primary reason you'd want to use Stampit.
For regular factory functions, you do it just like you'd do it with a constructor function, except that you have more options to change your implementation in the future.
"I think it gets even a bit more awkward if the .enclose() method is conditionally creating a public variable (ie, this-scoped). At that point, the very name of the enclose method feels awkward."
`.enclose()` is named `.enclose()` because it creates a closure for your stamp. If you need to conditionally create a public variable, that implies that there's some private state or logic that determines whether or not you create that public property. The name may feel awkward, but it accurately describes what it does.
"The intent of exposing a promise on a stream would be to allow for more uniform consumption in the calling context, where you might be using a whole promise-base workflow."
Fair enough, but I'd probably want to wrap the stream with a promise and wait for the stream to end and then resolve or reject after you've received all of the values.
"this.once( "content", this._deferred.resolve );
this.on( "error", this._deferred.reject );"
A stream can emit more than one "content" event and more than one "error", but a promise is immutable. Once it's settled, it's settled. In other words, you can miss data and drop errors. In other words, this is a lossy mapping.
"I'm still a little stuck on where you would move certain functionality to."
Public properties go in `.state()`, private data and privileged methods go in `.enclose()`, public initialization just gets passed into the stamp as a map argument. Private initialization goes in privileged methods.
All this becomes more clear after you've seen more examples and played with Stampit for a while.
Stamps are not the only alternative to constructors. In fact, I probably use a simple factory 50x more than I use stamps.
@eric That is true but it is easy to write a script that sets all dependencies to their current installed version.
npm ls --json will give you the dependencies as a json object showing both the version and the wildcard in the package.json (the from attribute).
You can walk that object and put the installed version in the appropriate package.json.
For me that is easier than dealing with merge issues when a package changes and I am unfamiliar with the code since it can be automated.
@Eric,
Yeah, I think I just need to sit down and try out a few examples. When it comes to decoupling instantiation from initialization, I've done that in the past, but usually in the context of a dependency-injection framework, where it was a "workaround" to make everything play nice with DI.
And, actually, in ColdFusion, all instantiation is *technically* decoupled as creating an instance doesn't implicitly call the constructor unless you specifically use the "new" keyword.
In the past, you've talked about Duplex Streams in node as being a prime example of where something like Stampit would come in handy. Do you happen to have an example of how Stampit could be used to recreate a Duplex stream using Readable and Writable? Or, would Readable and Writable also need to be using Stampit for that to work?
@Ben,
It's sometimes possible to compose multiple legacy constructors together using `stampit.convertConstructor()`, but using constructors at all with Stampit is a bit slippery. Whether or not it will work depends largely on the implementation of the constructor.
In the case of the streams API, I don't think it works out of the box. However, if they had been written with stamps, creating a duplex stream would be a one liner:
var duplex = stampit.compose(readable, writable);
@Eric or anyone who may have some insight into this. Speaking of JavaScript crashes and errors, I am getting this error when running an app I'm working on:
Unhandled exception at line 129, column 222 in http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js
0x80004002 - JavaScript runtime error: No such interface supported
I am using the JQuery library. Does anyone have any insight into this? Sorry if this is OT, but I thought it related. :-/
@All,
I wanted to follow-up on something else that I read in the book about anonymous functions and stack traces:
www.bennadel.com/blog/2836-anonymous-functions-assigned-to-references-show-up-well-in-javascript-stack-traces.htm
I am happy to see that all of the modern browsers will include function expression reference names in the stack traces even if the function expression doesn't provide an explicit name (though, if it does provide an explicit name, that name takes precedence).