Self-improving Software

Dr. Gleb Bahmutov PhD

Kensho Boston / NYC

Does your code look like this?

There is a better way

Code out of control: red flags

Divide and conquer

Tools and semantic versioning rants

Presentation outline

Source out of control - the red flags

Build time > 15 seconds

Multiple tools, environments

Impossible to unit test a feature

You have to use AND to describe the project and its goals

Source control to built and tested > 10 minutes

No code reuse

Giving up

Large projects are bad: Extra reading

 Q: Why are the giant code bases so difficult to deal with?

Software complexity

= number of interactions

var sum = add(a, b);

4 variables (a, b, add and sum)

4 * (4 - 1) / 2 = 6 interactions

Cognitive research

3 - 7 things at once

"Thinking, Fast and Slow" by Daniel Kahneman

Solution to interaction complexity

Physical separation (scope)

function add(a, b) { ... }
var sum = add(2, 3);
console.log('2 + 3 =', sum);
  • functions separate variables via scopes

  • source files separate code

  • teams separate people

  • dependencies separate files

  • slides separate bullet points

Splitting code via dependencies

Things that are simpler in small projects

  • API design
  • Testing
  • Installation
  • Documentation
  • Code reviews and standards
  • Code and module reuse
  • Onboarding

Set clear boundary

// function signature
function add(a, b) { ... }

// package.json
{
    "name": "my-utils",
    "version": "2.0.1",
    "main": "index.js"
    "dependencies": {
        "module-a": "1.0.0",
        "module-b": "0.1.0"
    }
}

Respect the boundary

// good
var a = require('another-module');
// avoid
var a = require(
    './node_modules/another-module/src/something.js');

App Assembly

Each auto part is

  1. built separately

  2. tested separately

  3. has SKU number

  4. shipped to the auto plant

  5. placed into the car

Assembling apps

  • Single repo per app
  • Shared code via separate repos (dependencies)
  • Each repo has its own version

12 Factor App (from Heroku)

Packaging

Java - Maven

Perl - CPAN

JavaScript - Node Package Manager (NPM)

NPM in action

npm search colors
npm info chalk
npm home chalk
npm install chalk --save

// index.js
require('chalk');
console.log( chalk.blue('Hello world!') );

Node: NPM

{
  "name": "my-module",
  "version": "0.1.0",
  "git": ...,
  "dependencies": {
    "foo": "0.1.*",
    "bar": "~1.2.0",
    "baz": "^2.0.1"
  },
  "devDependencies": {
    "grunt-concat": "0.1.1"
  }
}

Eliminate surprises

Use exact versions

$ npm config set save-exact true
$ npm install --save-exact grunt-nice-package
// cleans up fuzzy symbols

Dependency resolution: Nodejs

// dependencies-resolution uses module-a@2.0.0, module-b@0.6.0
// module-b@0.6.0 uses module-a@1.0.0

$ npm list
dependencies-resolution@0.0.0
├── module-a@2.0.0
└─┬ module-b@0.6.0
  └── module-a@1.0.0

Worry about the top level.

1.5.0

4.0.1

Semantic versioning

major . minor . patch

unicorns . stars . angels

really

major . minor . patch

major' . minor' . patch'

From:

To:

Semantic versioning

  • major: I broke it

  • minor: I added new feature

  • patch: I fixed something

really

Semantic versioning

Dependency tree in practice

Dependency tree in practice

0.1.0

2.2.0

2.2.0

0.1.0

Dependency tree in practice

0.1.0

2.2.0

2.2.0

0.1.0

0.1.1

0.1.2

2.2.1

2.2.2

2.5.0

3.0.0

3.0.1

Real issue: out of date

// package.json
{
    "dependencies": {
        "module-a": "1.0.0"
    }
}
// npm registry:
module-a: 0.8.0, 0.9.0, 1.0.0, 1.0.1, 2.0.0

Q: Can I upgrade?

// package.json
{
    "dependencies": {
        "module-a": "1.0.0"
    }
}
// npm registry:
module-a: 0.8.0, 0.9.0, 1.0.0, 1.0.1, 2.0.0

A: No one knows

// package.json
{
    "dependencies": {
        "module-a": "1.0.0"
    }
}
// npm registry:
module-a: 0.8.0, 0.9.0, 1.0.0, 1.0.1, 2.0.0

Relying on human-supplied semver is like relying on human-typed code comments to be 100% accurate

Q: Is this a mess?

A: Yes.

  • the mess is manageable.
  • the mess maps nicely to the software development:
    • different parts are developed at different speeds.
  • using versioned dependencies isolates the true mess: constant merging of commits

Develop + integrate

function add(a, b) { ... }
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
...

Develop + integrate

function add(a, b) { XXX }
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
...

Develop + integrate

function add(a, b) { XXX }
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
...

Develop + integrate

function add(a, b) { XXX }
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
...

Hard to improve "add" AND keep everything working

Develop THEN integrate

// package.json "add": "1.0.0"
var add = require('add');
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
// module ADD@1.0.0
function add(a, b) { ... }

Develop THEN integrate

// package.json "add": "1.0.0"
var add = require('add');
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
// module ADD@1.1.0
function add(a, b) { XXX }

Develop THEN integrate

// package.json "add": "1.1.0"
var add = require('add');
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
// module ADD@1.1.0
function add(a, b) { XXX }

Revert, no harm done

// package.json "add": "1.0.0"
var add = require('add');
...
... add(2, 3); 
    ....
add(-1, 100); 
...
...
console.log(add(0, 0));
// module ADD@1.1.0
function add(a, b) { XXX }

Q: Can we automate?

  1. Run unit tests

  2. Install each new dependency

  3. Run unit tests again

    1. Keep or

    2. Revert

A: next-update

next-update

next-update

If you test your software - you will get 3rd party upgrades with zero effort

Q: Is the update from A@x.y.z to A@x.y+1.z

likely to succeed?

next-update-stats

next-update-stats

Semver adherence

Semver adherence

Semver adherence

type      from     to      successful %
patch     0.2.0   0.2.1        100
patch     0.4.0   0.4.1        100
patch     0.6.0   0.6.1        100 
patch     0.6.4   0.6.5        100
minor     0.6.5   0.7.0        100
major     0.8.1   1.0.0          0
minor     1.0.0   1.1.0        100
patch     1.1.0   1.1.1         94
minor     1.1.1   1.2.0        100
patch     1.2.0   1.2.1        100
minor     1.2.1   1.3.0        100
patch     1.3.0   1.3.1        100
patch     1.3.1   1.3.2        100
minor     1.3.2   1.4.0        100

Semver adherence

lodash respects semver

underscore and Ramda do not

* Ramda is still 0.15.0 (< 1.0.0)

# of projects is growing

Q: Can we keep upgrading ALL projects?

A: flip the switch with next-updater

Next-updater

Next-updater: scared?

iOS6 vs iOS7 updates

Automatic updates with next-updater are not scary

  • Minor / patch updates

  • If your project has high code coverage

  • If a particular dependency update has a high probability of being successful

NPM     zero.x.y

We fail to follow semver

We fail to follow semver

- and it does matter

Project activity - # of commits

Is your new release likely to NOT work for MY project? - SEMVER

rant

We fail to follow semver

- and it does matter

Backbone@43.0.0 is OK

Backbone 43.0.0 -> 43.0.1 NOT working right away for my web app is NOT OK

rant

Automate SemVer

Test each code against its own

previous tests

Q: Are you going to break everyone?

dont-break from module X

  1. Install each dependent project

  2. Replace X@x.y.z with X@current

  3. Run unit tests 

If we are breaking dependent projects - maybe we should increment MAJOR in semver major.minor.patch

dont-break from module X

Conclusions

  1. Single huge project - lots of small projects

  2. Test your software against the world: dont-breaksemantic-release
  3. Upgrade versions without breaking stuff: next-update / next-updater

Self-improving Software

Dr. Gleb Bahmutov PhD

Kensho Boston / NYC