Writing Javascript Your Team Can Talk About

Developers are really good at expressing ideas in code. For many, simply saying “show me the code” is shorthand for acknowledging that they’ll understand something far quicker by reading through the logic, rather than having it explained to them. But when a team of developers sits down to review work, they aren’t afforded the time to dig through every last piece of an application and understand it as a whole. In order to effectively communicate ideas and get feedback, teams need to be able to ‘grok’ the most important pieces at a glance.

Talking about code is hard. Ideas and concepts that might be easy to express in code may be very difficult to verbalize. Conversely, something easy to communicate in words might have a complex implementation in code. As developers, it’s our goal to both write elegant code, and be able talk about code elegantly.

How can developers author code that’s easy to talk about? How does the design of the code affect our ability to discuss it? Is code that’s easier to talk about also easier to refactor and maintain?

Let’s imagine we’re in a pairing session with another developer, working together to debug some previously written code. After taking a few moments to ingest the program, imagine getting a comment like this:

I noticed on line four-eighty-six of classroom-dot-js, that inside the callback to the XHR to fetch the list of students, you’re filtering the returned JSON object according to the string value of the selection in the teacher name control. But I noticed that when the page loads, selectedTeacher is undefined, which isn’t handled in the callback.

Versus a comment like this:

StudentList needs to handle an undefined param in its filterByTeacher method.

Which comment is easier to work with? If you think the second comment is clearer and more focused, I agree with you. The first comment is more explicit, but having to verbally trace through an execution path in order to help illustrate the problem adds to our own ‘mental tax’ in trying to communicate effectively. This is an indication that our code is hard to talk about. The second comment, however, is succinct and addresses the design of the code directly. I also think it’s more actionable – suggesting a direct change instead of a vague observation. Finally, it indicates that both you and your co-worker have a shared understanding of how the code has been written.

Good techniques behind writing clear and literate code affects developers writing in any language. But I wanted to take some time to address Javascript specifically. The dynamic nature of Javascript allows developers to approach their code design in many different ways, with the language itself imposing few opinions or restrictions. This means that how we read a piece of Javascript often depends heavily on who wrote it. This can be a serious problem for teams who spend a lot time working amongst each other’s projects.

It’s important that teams have a unified way they approach code design. This makes it easier for developers to move between code bases, and facilitates detailed discussion around implementing business logic, rather than getting hung up on semantic differences between code styles.

Many organizations publish style guides for coders to follow. Those are certainly valuable in helping document the rules that govern a codebase, but they do not often make a detailed case for why certain rules exist. In this post, I will suggest several practices around code style, and explore how they can be beneficial to teams that often need to come together to talk about their code.

Model Your Domain on the Client Side

I often find the toughest part about talking through code with another developer is not having enough nouns in the code. Without good names, it becomes hard to nail down the exact parts of the application we’re talking about — and harder still to clearly delineate between layers and responsibilities.

I’m not just talking about following good naming conventions, but actually making a conscious decision to create a named thing in your application. I often see Javascript developers forgoing the creation of formalized entities, instead relying heavily on primitives, or worse yet, anonymous JSON, to represent their domain objects.

Consider this snippet, consuming JSON from a web service:

var students = [] $.ajax({   type: ‘GET’,   url: ‘/students’, }).then(function(results) {   students = results; }); 

The Student object is a central to our application, but what do we know about the data returned from our web service, other than it is an array of objects that has something do with students? Do we know what the properties of these objects are? Is it a deeply nested structure, or flat object? Are all the required properties there? Which properties are nullable? All the information we have about students is contained in the payload we get back from the server. There is little transparency in what our data looks like, or how it should behave.

What if we create a formalized Student entity? What might that look like?

function Student(attrs = {}) {
    const defaults = {
        id: null,
        teacherName: null,
        fname: null,
        lname: null,
        gpa: null,
        currentlyEnrolled: false,
        lastAttendance: null,
        tuitionPaid: false
    }
    return Object.assign(defaults, attrs);
}

var students = [] $.ajax({
    type: 'GET',
    url: '/students'
}).then(function(results) {
    students = results.map(result => Student(result));
});

With a formal entity around what defines a student, it becomes considerably easier to talk about what our student data looks like, and the role it has in our application.

Formalizing data structures into domain entities is a good first step toward to better code design. It’s not just good practice for entities, but for any set of code that encapsulates business logic. For example, in this same app, we might have an AttendanceTaker or an GpaCalculator that consumes our Student entities. Just by looking at these names, roles and responsibilities become clearer to mentally navigate. (It also becomes easier to test!)

Extract Inline Logic

My second piece of advice in helping to keep code obvious is to reduce how often we use inlined functions or object literals as arguments to functions. This is more of an aesthetic suggestion than a functional one, but it helps with code readability.

One of the great features in Javascript is that we can treat functions as first-class values, meaning they have the ability to be used just like primitives when supplying arguments. This leads to code that looks like this:

angular.controller('teacherDashboardController', function() {
    this.students = [];
    this.teachers = [];
    this.filterByTeacher = function(teacher) {
        this.students.filter(student => student.teacherName === teacher);
    }
    this.markAttendance = function(student) {
        student.lastAttendance = Date.now();
    }
});

This should look like a pretty normal way to write Javascript, and there’s nothing obliquely wrong with it. But consider for a moment, as a developer, what is more important: the call to angular.controller() or the code that follows? It’s the code that follows, of course! That’s our code! The call to angular.controller() is just boilerplate, so why do we lead with it? What if we instead de-emphasized the importance of Angular in this example?

function TeacherDashboardController() {
    this.students = [];
    this.teachers = [];
    this.filterByTeacher = function(teacher) {
        this.students.filter(student => student.classroomId === teacher);
    }
    this.onMarkAttendance = function(student) {
        student.updateAttendance();
    }
}
angular.controller('teacherDashboardController', TeacherDashboardController);

I admit the difference is subtle, but it makes an important distinction between our code and our framework’s code. Plus, it suddenly becomes obvious that our controller is just a plain old javascript function, and now we can talk about it in that context, instead of possibly spending too much time on what the angular.controller() call is up to.

I’m not suggesting you extract all your inline code. Inline functions are great for short expressions or list comprehensions. However, if you find yourself putting meaningful, important logic inside someone else’s function call, maybe think about refactoring it to have your work front and center instead. This way, it will probably be easier to refactor when you end up switching frameworks. And since you’ve separated your work from the consuming logic, it will probably be easier to write tests for as well.

Don’t Fear The Class

Remember earlier when we created our Student entity? What if we started adding more logic, such as creating validation rules, updating enrollment or attendance, or calculating GPA? It might look something like this:

// extend a defaults object with the passed in attrs, 
// then add additional properties for methods 
function Student(attrs = {}) {
    const defaults = {
        id: null,
        teacherName: null,
        fname: null,
        lname: null,
        gpa: null,
        currentlyEnrolled: false,
        lastAttendance: null,
        tuitionPaid: false
    }
    let student = Object.assign(defaults, attrs);
    student.updateAttendance = function() {
        student.lastAttendance = Date.now();
    }
    student.enroll = function() {
        if (student.tuitionPaid) {
            student.currentlyEnrolled = true;
        }
    }
    student.unEnroll = function {
        student.currentlyEnrolled = false;
    }
    student.calculateGPA = function() {
        let calc = new GpaCalculator(student);
        student.gpa = calc.gpa();
    }
    return student;
}

We’ve expanded our implementation of our Student entity, adding several methods. We do this by creating an object at the top of the function body, then modifying it by merging in passed arguments with defaults and adding additional functions as properties before returning our newly constructed object. This is a prime example of the “factory function” pattern.

Factory functions have a lot of benefits, especially when it comes to simplicity — the only requirement of a factory function is that it returns an object. Simple needs should have simple solutions, and the factory function should be part of your development "Swiss Army knife". But is it the best solution for encapsulating all your application’s object logic? Perhaps not.

One potential issue is that there are many different ways to write a factory function. Here’s two more samples of the exact same logic.

One using object literal syntax:

function Student(attrs = {}) {
    return {
        id: attrs.id teacherName: attrs.teacherName fname: attrs.fname,
        lname: attrs.lname,
        lastAttendance: attrs.lastAttendance,
        currentlyEnrolled: attrs.currentlyEnrolled,
        gpa: attrs.gpa,
        tuitionPaid: attrs.tuitionPaid,
        updateAttendance: function() {
            this.lastAttendance = Date.now();
        },
        enroll: function() {
            if (this.tuitionPaid) {
                this.currentlyEnrolled = true;
            }
        },
        unEnroll: function(length) {
            this.currentlyEnrolled = false;
        },
        calculateGpa: function() {
            let calc = new GpaCalculator(this);
            this.gpa = calc.gpa();
        }
    }
}

And another using the revealing module pattern:

function Student(attrs = {}) {
    function updateAttendance() {
        props.lastAttendance = Date.now();
    },
    function enroll() {
        if (props.tuitionPaid) {
            props.currentlyEnrolled = true;
        }
    },
    function unEnroll() {
        props.currentlyEnrolled = false;
    },
    function calculateGpa() {
        let calc = new GpaCalculator(props);
        props.gpa = calc.gpa();
    }
    const defaults = {
        id: attrs.id teacherName: attrs.teacherName fname: attrs.fname,
        lname: attrs.lname,
        lastAttendance: attrs.lastAttendance,
        currentlyEnrolled: attrs.currentlyEnrolled,
        gpa: attrs.gpa,
        tuitionPaid: attrs.tuitionPaid
    }
    let props = Object.assign({}, defaults, attrs);
    return {
        props: props,
        updateAttendance: updateAttendance,
        enroll: enroll,
        unEnroll: unEnroll,
        calculateGpa: calculateGpa
    }
}

We now have three versions of our Student() factory that returns an object with the same logic in it. Which one is right? How does a developer decide that? Object creation is a fundamental aspect of writing Javascript. It shouldn’t be something developers have to spend a lot of time thinking about (and in fact, very few languages provide such a multi-pronged approach to building object instances).

As an individual developer, it’s easy to have your own one way of doing something consistently But remember, we’re talking about sharing code amongst a team here. What version of object construction is going to be familiar to other developers now, and in the future?

ES2015 introduced classes to help solve this issue. Javascript has always had facilities for object creation and property inheritance, but the syntax was often confusing and verbose. Classes provide major syntax improvements to the constructor functionality that has been in Javascript since the beginning. And more importantly, it’s part of the language standard, so many developers are already familiar with it. This also means you don’t need to worry about it being a “flavor of the week” technique quickly outmoded by the typical churn of the Javascript community.

Essentially, what classes provide at the human level is a common set of expectations around how code can be designed and structured. Classes represent a predictable way to do things, with very few ways to deviate from the pattern. And because it’s a pattern supported by the language itself, it has begun to see wider adoption in the community, and will be likely see improvements in ES2016, ES2017 and beyond.

By using classes, we gain the following benefits:

  1. Consistent location for initialization. In our factory examples, I handle initialization of our object attributes in three different locations. Using classes, I only have one way to handle initialization: inside my constructor() method, which should always appear at the top. And it’s always called constructor(). Not initialize() or init() or extend() or part of my return statement.
  2. Provides one way to define methods, using a simple syntax:myMethod() { }. Don’t need to debate using named functions or assignment with function expressions, or binding function references in another object.
  3. Usage of this is consistent, in the sense that it will always be set up for you, pointing to your object’s properties, and properly bound inside your methods. Combined with proper usage of lambdas (arrow functions), you should be able to avoid having to alias or re-bind this in most cases.
  4. No need to return your object. Return statements aren’t difficult, but they’re easy to miss, and it’s easy to forget to add references for new members, or incorrectly set up properties. Classes handle the object construction for you.
  5. An easier inheritance model which provides some core object-oriented design features which were sorely missed in the early days of Javascript
  6. Classes are already familiar to thousands of programmers. Anyone coming from a C#, Java or PHP background will probably be able to look at a Javascript class and get the gist of what’s going on. And while classes in Javascript do operate differently than in those other languages, the basic usage patterns are roughly the same.

Here’s what a class version of Student looks like:

class Student {
    constructor(attrs = {}) {
        this.id = attrs.id;
        this.teacherName = attrs.teacherName;
        this.fname = attrs.fname;
        this.lname = attrs.lname;
        this.lastAttendance = attrs.lastAttendance;
        this.currentEnrolled = attrs.currentEnrolled;
        this.gpa = attrs.gpa;
        this.tuitionPaid = attrs.tuitionPaid;
    }
    updateAttendance() {
        this.lastAttendance = Date.now();
    }
    enroll() {
        if (this.tuitionPaid) {
            this.currentlyEnrolled = true;
        }
    }
    unEnroll() {
        this.currentlyEnrolled = false;
    }
    calculateGpa() {
        let calc = new GpaCalculator(this);
        this.gpa = calc.gpa();
    }
}

I know that to some developers, classes feel unnecessary and there is a great debate around their technical merits. In the same manner that I caution against over-using informal factory patterns, I would also caution against a classes for everything approach. Classes are just another tool for designing your code. Understanding their merits and their flaws is essential in picking the right tool for the job.

Finally, another reason to prefer classes is because classes expose a type to my code editor, that can be used for more accurate code completion hints.

Without the sort of built-in consistency provided by classes, most editors can only rely on symbol lists generated from files. Having a type definition makes our tools much, much smarter about our own code. This brings me to my last point.

 

Use A Type System

I’m sure by now you’ve noticed a few common threads in this post: make things obvious, consistent and explicit for the sake of readability and understanding. Of all the other techniques I’ve mentioned, using type annotations in your code has the potential to provide the biggest benefit.

Unfortunately, you can’t use a type system with just plain Javascript. At the time of this writing, you have two options: adding type annotations using Facebook’s Flow or Microsoft’s Typescript. For our examples, I will use Typescript.

// This is an example type annonation telling us that
// students is an array of Student types.
let students: Student[] = results.map(result => new Student(result));

// If we try to use a type other than Student, Typescript will
// refuse to build the code, showing a helpful error message.
let students: Student[] = results.map(result => result.fname);

There was a time when strong typing felt like a tool for the compiler, something we did simply to please the machine. But in a modern language like Typescript, types are a feature intended to help humans make good design decisions. A well thought out type system eliminates ambiguity in our code, allowing developers to understand portions of our codebase without having to understand the entire system.

Typescript allows us add annotations to our code for the core Javascript, for our own types, and for types exposed by third party libraries. It also provides us with interfaces that allow us to design contracts around how our code should be written and consumed. For example, instead of creating an object of Student defaults, I could write an interface that simply describes the "shape" of a Student.

interface Student {
    id: number, 
    teacherName: string,
    fname: string,
    lname: string,
    gpa: GradePointAvg,
    lastAttendance: Date,
    currentlyEnrolled: boolean,
    tuitionPaid: boolean
}

And just as I mentioned above, having types means our tooling gets smarter as well. It enables our tools to point out mistakes before we run our code, provide highly detailed code completion hints and assist in refactoring.

interface GradePointAvg {
  academicYear: number;
  value: number;
} 

Editors that support Typescript (like Atom) can catch more than just syntax errors. Here, we’ve been informed that this method is returning just a number, instead of a GradePointAvg value object, which also includes a value for the academic years (see below for corrected version.)

Bringing It Together

I hope you’ve been able to get some insight from this post, and that next time you sit down to start a project, you can have a better discussion with your team about code design and style. I want to leave you with a more fleshed out example of the ‘teacher dashboard’ used in our examples. As you read through, does this sample meet our previous criteria? It is easy to understand at a glance? Are you able to zoom in on just a couple lines and divine their purpose? Is this a piece of code a team could talk about?

interface GradePointAvg {
    academicYear: number;
    value: number;
}

interface StudentProps {
    id: number;
    teacherName: string;
    fname: string;
    lname: string;
    gpa?: GradePointAvg;
    lastAttendance: Date;
    currentlyEnrolled: boolean;
    tuitionPaid: boolean;
}

class Student implements StudentProps {
    // Typescript allows us to declare properties above our constructor.     
    // (We can also declare properties as private, but that is not shown here.)     
    id: number;
    teacherName: string;
    fname: string;
    lname: string;
    gpa: GradePointAvg;
    lastAttendance: Date;
    // We can even intialize properties here.  
    currentlyEnrolled: boolean = false; 
    tuitionPaid: boolean = false;

    constructor(attrs: StudentProps) {
        this.id = attrs.id;
        this.fname = attrs.fname;
        this.lastAttendance = attrs.lastAttendance;
        this.currentlyEnrolled = attrs.currentlyEnrolled;
        this.gpa = attrs.gpa;
        this.tuitionPaid = attrs.tuitionPaid;
    }

    updateAttendance(): void { this.lastAttendance = new Date(); }

    enroll(): void {
        if (this.tuitionPaid) {
            this.currentlyEnrolled = true;
        }
    }

    unEnroll(): void { this.currentlyEnrolled = false; }

    calculateGpa(): void {
        let calc = new GpaCalculator(this);
        this.gpa = calc.gpaOnDate(new Date());
    }
}

class GpaCalculator {
    constructor(private _student: Student) {
        // Typescript also allows us to declare properties
        // in our constructor arguments. This will auto-assign
        // the passed in argument to this._student.     
    }

    gpaOnDate(date: Date): GradePointAvg {
        // Obviously, we"d need get test scores from somewhere,         
        // but for now, let"s just return a number           
        return { academicYear: date.getFullYear(), value: this._student.lastAttendance >= date ? 3.97 : 0.00 };
    }
}

class TeacherDashboardController {
    students: Student[] = [];
    teachers: string[];
    private _allStudents: Student[] = [];

    constructor(private _$http: ng.IHttpService) {
        this.teachers = ["Jones", "Snape", "Keating", "Aristotle"];
        this._fetchStudents();
    }

    filterByTeacher(teacher: string): void { this.students = this.students.filter(student => student.teacherName === teacher); }

    onRefreshGpa(student): void { student.calculateGpa(); }

    onMarkAttendance(student): void { student.lastAttendance = Date.now(); }

    private _fetchStudents(): void {
        this._$http.get("/students").then(result => {
            this._allStudents = result.data.map((item: StudentProps) => new Student(item));
            this.students = this._allStudents;
        });
    }
}

let teacherDashboard: ng.IComponentOptions = { controller: TeacherDashboardController, template: "/templates/teacherDashboard.html" };
angular.component("teacherDashboard", ["$http", teacherDashboard]);