“Write the tools you need for code you want to type.”

This is the best way I found to formulate how I choose to write my code. Or maybe “Write the tools you need for code you want to read.”, or “write your code as a library”? I don’t know which on is better…

I will try to explain with an example.

The example

When you code you usually have a set of pre-existing tools (methods, classes, functions, …) that you can use to accomplish a task. Sometimes it comes from some kind of standard library, sometimes it comes from the code you already written and sometimes it comes from third-party library that you imported. Wherever these tools come from doesn’t really matter.

Let’s say you have a set of function at your disposal:

declare function querySelector(selector: string): HTMLElement | null;
declare function addEventListener<Evt>(eventName: string, callback: (e: Evt) => void): void;

interface UserConnectionStatusChangedEvent extends Event {
  status: "loggedin" | "loggedout";
}

So when smeone asks you hide or show/hide the #login-button and #logout-button when the event user-connection-status-changed is fired, you write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
addEventListener("user-connection-status-changed", (e: UserConnectionStatusChangedEvent) => {
  const loginButton = querySelector("#login-button");
  const logoutButton = querySelector("#logout-button");
  if(loginButton == null || logoutButton == null) {
    console.error("Could not find login-button or logout-button");
    return;
  }
  if(e.status == "loggedin") {
    // hide the login button and show the logout button
    loginButton.style.display = "none";
    logoutButton.style.display = "inline";
  }
  else if(e.status == "loggedout") {
    // show the login button and hide the logout button
    loginButton.style.display = "inline";
    logoutButton.style.display = "none";
  }
});

This code works. But is it the code you would like to write? Is it the code you would like to read? What about:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
addEventListener("user-connection-status-changed", (e: UserConnectionStatusChangedEvent) => {
  const loginForm = new LoginForm("#login-form");
  loginForm.loginButton.setVisibility(e.status == "loggedin");
  loginForm.logoutButton.setVisibility(e.status == "loggedout");
});

// For which you would need to write the following tools:
class LoginForm {
  private element: HTMLElement;
  public constructor(selector: string) {
    const element = document.querySelector<HTMLElement>(selector);
    if(element == null) {
      throw new Error("Unrecoverable error: could not find login form in page");
    }
    this.element = element;
  }

  get loginButton() {
    return this.wrapElement(".login-btn");
  }

  get logoutButton() {
    return this.wrapElement(".logout-btn");
  }

  private wrapElement(selector: string) {
    const b = this.element.querySelector<HTMLElement>(selector);
    assert(b != null, "Could not find " + selector);
    return new HtmlElementWrapper(b);
  }
}

function assert(cond: boolean, message: string): asserts cond {
  if(cond == false) {
    throw new Error("Unrecoverable error: " + message);
  }
}

class HtmlElementWrapper {
  public constructor(private element: HTMLElement) { }
  public setVisibility(visibility: boolean) {
    if(visibility === true) {
      this.element.style.display = "inline";
    }
    else {
      this.element.style.display = "none";
    }
  }
}

In my opinion the second addEventListener call has a much lighter mental load. But I have to admit that the second solution need a lot of tools to work.

The first solution is 18 lines long and the second is 49 lines long. So from a line count point of view it becomes interesting as soon as we write the same kind of code three times.

Let’s say that later you have to hide the login button when in fullscreen mode, and when you preview a file. With the first solution you need to write 27 more lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
addEventListener("fullscreen", (e: any /* any for convenience, should be something like FullScreenEvent */) => {
  const loginButton = querySelector("#login-button");
  const logoutButton = querySelector("#logout-button");
  if(loginButton == null || logoutButton == null) {
    console.error("Could not find login-button or logout-button");
    return;
  }
  if(e.fullscreen == true) {
    loginButton.style.display = "none";
    logoutButton.style.display = "none";
  }
  else {
    loginButton.style.display = "inline";
    logoutButton.style.display = "inline";
  }
});

addEventListener("enter-file-preview", () => {
  const loginButton = querySelector("#login-button");
  const logoutButton = querySelector("#logout-button");
  if(loginButton == null || logoutButton == null) {
    console.error("Could not find login-button or logout-button");
    return;
  }
  loginButton.style.display = "none";
  logoutButton.style.display = "none";
});

With the second solution you would need to write 11 more lines:

1
2
3
4
5
6
7
8
9
10
11
addEventListener("fullscreen", (e: any) => {
  const loginForm = new LoginForm("#login-form");
  loginForm.loginButton.setVisibility(!e.fullscreen);
  loginForm.logoutButton.setVisibility(!e.fullscreen);
});

addEventListener("enter-file-preview", () => {
  const loginForm = new LoginForm("#login-form");
  loginForm.loginButton.setVisibility(false);
  loginForm.logoutButton.setVisibility(false);
});

But the second solution is also much more error resistant. You probably won’t make copy/paste mistakes for example. If you decide on a fallback method for when the buttons can’t be found you’ll just have to update it in one place. You could also imagining adding methods to the LoginForm and HtmlElementWrapper classes as your project grows.

Get on with it!

This kind of thinking is, I think, related to the DRY principle in the way that as soon as you start writing the code for the “fullscreen” event and “enter-file-preview” the second solution becomes kind of obviously better but as long as you limit yourself to the “user-connection-status-changed” it’s less straightforward.

So maybe we should talk about Anticipated-DRY? Because we want to write code like if you were going to repeat it even when you’re not (yet).

(Now that I have written this it would be fun to call this CRY for “Can’t Repeat Yourself”.)

Obviously, as with many things, this is not absolute, and sometimes repeating yourself is the way to go.

I’m not sure I was able to convey my point in this article. Maybe all of that is obvious. Maybe it’s just rambling.