Removing Side Effects - some juice isn't worth the squeeze

Justin Meyer by Justin Meyer

Removing Side Effects - some juice isn't worth the squeeze

Justin Meyer CanJS 2.3 had awesome plugins that would create side effects for modules that didn't explicitly use them.  Learn why this was bad design, how you can avoid this in your own code, and a preview of what mixins in the view/template might look like.

posted in Development ,CanJS ,stable-and-innovative on September 11, 2017 by Justin Meyer

In This Series: Stable and innovative code bases

  1. Stable and Innovative Code Bases
  2. How to Manage Code Across Many Independent Repositories
  3. Removing Side Effects - some juice isn't worth the squeeze
  4. Coping with Stateful Code

In this article, we will:

  • Learn about modules with side effects
  • Understand how CanJS removed side effects in plugins
  • See a preview of how plugins in views might work

For CanJS 3.0, simply moving everything into its own repository wasn't enough to ensure stability. Stable code means that if you write a module, it should always behave the same way no matter what else is happens in the application.  This is the what you see is what you get (WYSWYG) principle.

It turns out that in CanJS 2.3, we were violating this principle with almost all of our plugins such as:

These modules created side effects for other modules, breaking WYSIWYG. The following example shows why. In CanJS 2.3, if you had one module that imported the can/map/validate/ plugin on a can.Map, that map would have an .errors() method like:

// task.js 
var Map = require("can/map/");
require("can/map/validate/");

var Task = Map.extend({ 
  define: {
    name: { 
        value: '', 
        validate: {  required: true }
    }
  }
}); 

new Task().errors()  //-> {name: ["name is required"]}

can/map/validate/ worked by changing Map.prototype directly. (Perhaps I was too heavily influenced by $.fn). This meant that every Map would suddenly have an .errors() method regardless if it required can/map/validate/ or not.

// user.js 
var DefineMap = require("can-define/map/");  

var User = DefineMap.extend({ });  

new User().errors //-> function ??

Notice how User has an .errors() method? This is not WYSIWYG. This prevents other plugins that might want to create an errors method from being used anywhere in the application.

To fix this, we're making all plugins work as mixins. In 3.0, you import the can-define-validate-validatejs module and pass your type to that as follows:
// task.js 
var DefineMap = require("can-define/map/"); 
var defineValidate = require("can-define-validate-validatejs");  

var Task = DefineMap.extend({ 
    name: { 
        value: '', 
        validate: {  required: true }
    }
}); 
defineValidate(Task);

new Task().errors()  //-> [{message: "name is required", related: ["name"]}]

defineValidate(Type) adds .errors() only to the type passed to it. This means that other DefineMaps will not have .errors():

// user.js 
var DefineMap = require("can-define/map/");  

var User = DefineMap.extend({ });  

new User().errors //-> undefined

This is WYSIWYG! Even better, when decorators land in JavaScript, you can use these mixins like:

// task.js 
import define from "can-define"; 
import defineValidate from "can-define-validate-validatejs";  

@defineValidate
@define({
    name: { 
        value: '', 
        validate: { required: true }
    }
})
class Task {}

new Task().errors()  //-> [
    {message: "name is required", related: ["name"]}
]

We’ve either completed or started making the following mixins side effect free:

Mixin Purpose
can-define-validate-validatejs Validates a DefineMap with validatejs.
can-connect/* Mixin a variety of behaviors into a connection.
can-define-stream-kefir Define properties from streams with KefirJS.
can-connect-cloneable Store a backup of a DefineMap’s data.

Mixins make a lot of sense in Models and ViewModels and we have easy ways providing them - exporting functions. To completely remove side effects from our codebase, we’ll need something similar for Views. Lets see what mixins might look like in the view.

View Mixins

Currently, you can import custom events like DOM enter events into CanJS's global event registry in can-stache like:

<can-import from="can/util/dom/events/enter"/>
<div on:enter="doSomething()"/>

This mixes in the enter event across all of CanJS, not just this template.  This isn’t WYSIWYG.  To solve this, we plan on making event bindings accept a variable which is used to setup the binding.  It might look like:

<can-import from="can-event-dom-enter" value:to="scope.var.enter"/>
<div on:[scope.var.enter]="doSomething()"/>

Note that can-event-dom-enter exports an event definition that can-stache-bindings (and eventually other utilities) can use to perform the event binding.  

Final Thoughts

As we make new releases, we’ll continue to remove side effects so that all of your code is WYSIWYG. But there are some places this is impossible.  In the next article, I'll discuss strategies that minimize the impact of places where side effects must exist.

 

Create better web applications. We’ll help. Let’s work together.