Character Vault
Any Concept / Any System
Compendium
Your System Come To Life
Roll20 for Android
Streamlined for your Tablet
Roll20 for iPad
Streamlined for your Tablet

Personal tools

Difference between revisions of "Mod:Best Practices"

From Roll20 Wiki

Jump to: navigation, search
m
m
Line 458: Line 458:
 
}</pre>
 
}</pre>
  
βˆ’
[[Category:API|Best Practices]]
 
 
[[Category:API Development]]
 
[[Category:API Development]]

Revision as of 09:58, 17 February 2022

A best practice is a buzzword referring to a technique that is used as a benchmark when comparing to other techniques with the same language or system. A "best" practice can evolve over time, although changes are usually gradual.

The Roll20 API is written using JavaScript, so many of the best practices listed here may be considered best practices for JavaScript in general, or simply best practices for programming in some cases. When creating API scripts you are not required to use best practices, but it is absolutely recommended, especially if you want to add your script to the Script Index.

The suggestions here might also apply to creating sheetworkers for character sheets.

Contents

Namespaces

Any externally-visible functions or variables should be contained within a "namespace" object. This limits the potential for name collisions between multiple scripts, as all API scripts in a campaign are running within the same context. If you choose a single namespace for your script, a collision will only occur if someone chooses the same namespace for their script. On the other hand, if you don't use a namespace and two scripts define a function named handleInput, there will be problems.

Strictly speaking, JavaScript does not have namespacing like some other languages, but you can simply add your functions and variables to an object which will behave in a similar way. This is what using a namespace will look like:

var myNamespace = myNamespace || {};

myNamesapce.MY_CONSTANT = 5;
myNamespace.myFunction = function() { /* ... */ }

The myNamespace || {} construction ensures that you don't completely overwrite an existing copy of the namespace in memory, for example if your script is spanning multiple script tabs. This statement will create a new empty object if myNamespace doesn't exist yet, or do nothing if it already exists.

Function and Variable Names

Use easy, short, readable function and variable names. Ideally, you should be able to glean the purpose of the object represented by the variable just by reading its name. Names like x1, fe2, and xbqne are practically meaningless. Names like incrementorForMainLoopWhichSpansFromTenToTwenty are overly verbose, and the highly descriptive name may end up wrong as you change your code over time.

On the other hand, a function named splitArgs is easy to understand if you're familiar with programmer jargon: "args" is a common shorthand for "arguments," and multiple arguments are frequently passed into a program as a single string which needs to be split up into its constituent parts. A variable named element or item located within a loop iterating over an array naturally leads one to understand that it should be a reference to one of the objects within the array (in particular, the object corresponding to the current iteration of the loop).

Legibility

Keep your code legible, especially if you need to ask for help on the forums. Indent code blocks as appropriate, include spaces, etc. Compare the following:

do if(node.name.toLowerCase()==='foo')break;while(parent=node.parent);
do {
    if (node.name.toLowerCase() === 'foo') {
        break;
    }
} while (parent = node.parent);

The latter piece of code takes up more space, but it is at least an order of magnitude easier to read. You are not competing in Code Golf. You have no need to minify your script. Obfuscation of your source code does not grant you any sort of advantage.

Also, please write your scripts in English. English is the standard around the world for programming, JavaScript's keywords and library functions are in English, and the majority of the users on the forum who will help you if you have difficulties speak English. Naturally, if you are writing a script which is intended to output text in a non-English language, you'll have strings containing non-English text, but the rest of the code should be English.

Comments

Commenting can be something of a holy war among programmers. Some say you need more, some say your code should be enough, and so on. However, understand that for a Roll20 API script, the person reading your code may not be a programmer at all, and is completely bewildered by your black magic... but they still need to make modifications in order to suit their game. At the very least, comments describing your configuration variables are helpful to everyone who installs your script, and comments describing generally what's going on in each section of code can help the layperson trying to struggle his or her way through making the tabletop experience better.

A common mantra about commenting code is that your names should describe what the code does, while your comments describe why the code does that.

Underscore

Roll20 API scripts have access to the Underscore.js library, which contains a large number of utility functions. There is nothing you can do with Underscore that you can't do without Underscore, but the utility functions can frequently make your code more legible, often while making the code shorter as well. Here's an example of the improvement that Underscore offers:

for (var i = 0; i < jossWhedon.shows.length; i++) {
    if (jossWhedon.shows[i].title === 'Firefly') {
        var show = jossWhedon.shows[i];
    }
}
// show = {title: "Firefly", characters: Array[2]}

var characterDistribution = jossWhedon.shows.reduce(function(memo, show) {
    show.characters.forEach(function(character) {
        (!memo[character.role]) ? memo[character.role] = 1 : memo[character.role]++;
    });
    return memo;
}, {});
// characterDistribution = {doll: 1, mad scientist: 2, love interest: 2, slayer: 1, captain: 1, mechanic: 1}
var show = _.findWhere(jossWhedon.shows, {title: 'Firefly'});
// show = {title: "Firefly", characters: Array[2]}

var characterDistribution = _.countBy(_.flatten(_.pluck(jossWhedon.shows, 'characters')), 'role');
// characterDistribution = {doll: 1, mad scientist: 2, love interest: 2, slayer: 1, captain: 1, mechanic: 1}

Underscore examples courtesy of Singlebrook.com

Equals vs. Strict Equals

JavaScript has two equality operators (four, if you count their inverses): == and ===. The former operator will do what it can to coerce the values you're comparing to the same type before checking their equality, while the latter will leave the values you're comparing as their original type. This means that the Equals operator is able to return true for operands of different types, while the Strict Equals operator will only return true if the operands are of the same type.

[10] === 10 // false
[10]  == 10 // true

'10' === 10 // false
'10'  == 10 // true

[] === 0 // false
[]  == 0 // true

'' === false // false
''  == false // true

In general, it is recommended to use strict equals over equals. See below for the specifications for x == y and x === y. Due to the way computers handle fractional numbers, you should also avoid testing for equality with them. Sometimes it may work, but sometimes it might fail, depending on the exact numbers involved. The full subject is rather complicated, but if you must check for equality between two fractional numbers, instead check that the difference between them is within some margin of error:

var epsilon = 0.000001;
if (Math.abs(x - y) < epsilon) {
    // x and y are equal
}

Equals Specification

  1. If Type(x) is the same as Type(y), then
    1. If Type(x) is Undefined, return true.
    2. If Type(x) is Null, return true.
    3. If Type(x) is Number, then
      1. If x is NaN, return false.
      2. If y is NaN, return false.
      3. If x is the same Number value as y, return true.
      4. If x is +0 and y is βˆ’0, return true.
      5. If x is βˆ’0 and y is +0, return true.
      6. Return false.
    4. If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and same characters in corresponding positions). Otherwise, return false.
    5. If Type(x) is Boolean, return true if x and y are both true or both false. Otherwise, return false.
    6. Return true if x and y refer to the same object. Otherwise, return false.
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.
  6. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
  7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
  8. If Type(x) is either String or Number and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
  9. If Type(x) is Object and Type(y) is either String or Number, return the result of the comparison ToPrimitive(x) == y.
  10. Return false.


Given the above, the equality operator is not always transitive:

new String('a') ==            'a'  // true
           'a'  == new String('a') // true
new String('a') == new String('a') // false

Strict Equals Specification

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Undefined, return true.
  3. If Type(x) is Null, return true.
  4. If Type(x) is Number, then
    1. If x is NaN, return false.
    2. If y is NaN, return false.
    3. If x is the same Number value as y, return true.
    4. If x is +0 and y is βˆ’0, return true.
    5. If x is βˆ’0 and y is +0, return true.
    6. Return false.
  5. If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and same characters in corresponding positions); otherwise, return false.
  6. If Type(x) is Boolean, return true if x and y are both true or both false; otherwise, return false.
  7. Return true if x and y refer to the same object. Otherwise, return false.

Random Numbers

JavaScript provides the function Math.random() which produces a random number in the range [0, 1). Roll20 provides the function randomInteger(max) which produces a random integer in the range [1, max]. A lot of work has gone into improving the randomInteger function, and creating an even distribution of numbers from the result of random is more complicated than it seems.

It is strongly recommended that you prefer randomInteger as your method of generating random numbers.

Code Blocks

There are two primary styles for writing code blocks in languages with a C-like syntax. The first is called "Allman style" and puts curly braces on their own lines:

if (myVariable === 'foo')
{
    sendChat('', 'bar');
}
else
{
    sendChat('', 'baz');
}

The second style is called "K&R Style" and doesn't give opening braces their own line (nor closing braces, in the case of else, else if, or do..while blocks):

if (myVariable === 'foo') {
    sendChat('', 'bar');
} else {
    sendChat('', 'baz');
}

At first blush, this appears to be a stylistic choice, and in many cases that's true. However, JavaScript tries to be "helpful" and adds semicolons to your code wherever it thinks you've missed them. This can cause a problem with Allman style braces:

return
{
    foo: 'bar',
    fizz: 'buzz'
};

JavaScript will "helpfully" add a semicolon after return here, causing your function to return undefined instead of your object literal. You can fix this problem with K&R braces:

return {
    foo: 'bar',
    fizz: 'buzz'
};

Blocks With One Line

If you have a code block one line long, it is generally legal to omit the curly braces entirely:

if (foo === 'bar')
    return;

var buzz = [];
for (var i = 0; i < fizz.length; i++)
    buzz.push(fizz[foo]);

However, this can sometimes make the code harder to read, especially if you have several nested one-line blocks (if(...)for(...)if(...)expression for example). Additionally, when you later return to your code to make some changes, the lack of curly braces can lead you to make a mistake. It doesn't matter how good you are at programming, it can happen to you. It happened to Apple, just look:

if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
    goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &clientRandom)) != 0)
    goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
    goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
    goto fail;
    goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
    goto fail;

This isn't JavaScript code, but it demonstrates what can go wrong when you omit curly braces on your blocks. Because of this bug, a critical portion of Apple's SSL code was never running at all. Most likely, a programmer at Apple hit a keyboard shortcut to duplicate the line the cursor was on, and didn't notice. However, if the blocks had had curly braces instead of relying on the ability to omit them for one-liners, that keystroke mistake would have simply generated a single line of unreachable (redundant) code, rather than effectively cutting out a large portion of the function.

Hoisting

The JavaScript engine performs a process called "hoisting" which it does to all of your variables and functions. Hoisting moves the variable's declaration or function's definition to the start of the function scope. To see this process in action, the following two examples are equivalent:

function hoistingExample1() {
    i = 7;

    console.log(i);
    console.log(func1());
    console.log(func2());

    var func1 = function(){ return 'Hi from function #1'; }
    function func2(){ return 'Hi from function #2'; }
    var i = 2;
}
function hoistingExample2() {
   var i, func1;
   function func2(){ return 'Hi from function #2'; }

   i = 7;

   console.log(i);
   console.log(func1());
   console.log(func2());

   func1 = function(){ return 'Hi from function #1'; }
   i = 2;
}

Both of these examples will log the number 7, a TypeError (func1 is not a function), and then the string "Hi from function #2".

Because of JavaScript's hoisting process, it is recommended that you declare (but not necessarily define) all variables at the top of the function scope, followed by all functions within the scope. This does not change the behavior of your program, but it does help to make the behavior more clear.

Single Responsibility Principle

This is the first part of the more general SOLID principles of object-oriented design. JavaScript is not object-oriented, but some parts of SOLID can still be applied. For Roll20 API scripts, the Single Responsibility Principle is most relevant. The Dependency Inversion Principle may be useful for particularly large scripts. The remainder of SOLID can be implemented with JavaScript, but requires a little bit of jumping through hoops and so will not be covered here.

The Single Responsibility Principle states that "a class should have only one reason to change." What this means is that an object should have a set of behaviors, together comprising a single responsibility which would require modifying the object if that responsibility is changed. The canonical example of this is a module which compiles and prints a report. Such a module would need to be changed if the contents of the report change or if the format of the report changes — two responsibilities, breaking the principle.

Reserved Words

The following words are reserved. They either currently have special meaning, may have special meaning in the future, or are blocked from having meaning in JavaScript. You may not use these words as identifiers in your code.

  • abstract
  • await
  • boolean
  • break
  • byte
  • case
  • char
  • class
  • catch
  • const
  • continue
  • debugger
  • default
  • delete
  • do
  • double
  • else
  • enum
  • export
  • extends
  • false
  • final
  • finally
  • float
  • for
  • function
  • goto
  • if
  • implements
  • import
  • in
  • instanceof
  • int
  • interface
  • let
  • long
  • native
  • new
  • null
  • package
  • private
  • protected
  • public
  • return
  • short
  • static
  • super
  • switch
  • synchronized
  • this
  • throw
  • transient
  • try
  • true
  • typeof
  • var
  • void
  • volatile
  • while
  • with
  • yield

Strict Mode

Strict mode can be applied to entire scripts or to functions. In this case, "entire scripts" refers to all of the concatenated JavaScript, not a single Roll20 API script. Because of this, it is not recommended that you apply strict mode to scripts, but to functions.

Strict mode changes the semantics of JavaScript. While enforcing strict mode can help you write more robust code, it is slightly different from non-strict JavaScript and as such using it is hardly a requirement.

Invoking Strict Mode

In order to opt-in to strict mode for a function, simply include the statement "use strict"; or 'use strict'; as the first statement in the function.

function strictFunction() {
    'use strict';
    return 'foo bar';
}

Changes in Strict Mode

Strict mode changes both the syntax and runtime behavior of JavaScript. The following is an overview of those changes.

Mistakes Become Errors

Normally, JavaScript allows a certain set of mistakes and tries to compensate for them. Unfortunately, this can lead to worse problems down the line. Strict mode coughs up errors, instead.

The first kind of mistake that strict mode blocks is mistyped variable names. Normally, assignment to a nonexistent variable would create a new variable in the global scope. Strict mode turns that into a ReferenceError.

'use strict';
var myVariable;
myVaraible = 12; // ReferenceError

The second kind of mistake is assignments which would normally fail silently. Any assignment to a non-writable property, getter-only property, or creation of a new property on a non-extensible object will throw an exception.

'use strict';
var obj1 = {},
    obj2 = { get x() { return 12; } },
    obj3 = {};

Object.defineProperty(obj1, 'x', { value: 12, writable: false });
obj1.x = 9; // TypeError

obj2.x = 9; // TypeError

Object.preventExtensions(obj3);
obj3.x = 9; // TypeError

The third kind of mistake is deleting properties which cannot be deleted.

'use strict';
delete Object.prototype; // TypeError

The fourth type of mistake is duplicate property names. Outside strict mode, duplicate properties are handled by using the last duplicate as the definitive value, but this "feature" is only likely to cause bugs, not resolve them.

'use strict';
var obj = { p: 1, p: 2 }; // syntax error

The fifth type of mistake is similar to the previous one, applying to function parameter names rather than object properties.

function foo(a, a, c) { // syntax error
    'use strict';
    return a + b + c;
}

The final type of mistake is octal syntax for number literals. Octal numbers are not part of the ECMAScript standard, but it's supported by nearly all browsers for basic JavaScript anyway. Octal literals are achieved by prefixing the number with a 0, but this is an error in strict mode.

'use strict';
var sum = 015 + // syntax error
          197 +
          142;

Simplifying Variable Uses

This feature of strict mode is of primary benefit to the compiler, however it is good to be aware of how strict mode affects your code.

First, the with keyword is prohibited. This actually helps you avoid certain bugs that can crop up due to with, which most sources recommend against using, anyway.

'use strict';
var x = 12;
with (obj) { // syntax error
    x; // Outside strict mode, is this var x, or obj.x?
}

Second, eval with strict mode does not introduce new variables into the scope. (The eval function, when explicitly run within a strict mode scope, will automatically run in strict mode, as well.)

var x = 12;
var evalX = eval("'use strict'; var x = 24; x");
assert(x === 12);
assert(evalX === 24);

Finally, strict mode prohibits deletion of plain names.

'use strict';
var x;
delete x; // syntax error

eval('var y; delete y;'); // syntax error

Simplifying eval and arguments

eval and arguments do a lot of black magic. Strict mode cuts down on the magic so that they are easier to understand.

First, eval and arguments cannot be bound or assigned. to attempt to do so is a syntax error.

'use strict';
eval = 12;
arguments++;
++eval;
var obj = { set p(arguments) { } };
var eval;
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function('arguments', "'use strict'; return 12;");

Second, strict mode does not create aliases between function parameters and elements of arguments. If the first parameter to a function is arg, changing it does not also change arguments[0].

function func(arg) {
    'use strict';
    arg = 24;
    return [arg, arguments[0]];
}
var results = func(12);
assert(results[0] === 24);
assert(results[1] === 12);

Finally, arguments.callee is not supported in strict mode. The use-case for arguments.callee is very weak (it is simply the name of the enclosing function), and it causes difficulties with optimization at runtime.

'use strict'
var func = function() { return arguments.callee; };
func(); // TypeError

Securing JavaScript

JavaScript's flexibility allows for considerable security holes. Strict mode lets authors create more-secure code. This feature of strict mode JavaScript should not affect Roll20 API scripts much, but the restrictions it imposes should be remembered.

First, this within a function has a restricted value. API scripts are already prevented from accessing the Document Object Model, but the restriction on this could come up in your own script without that goal.

'use strict';
function func() { return this; }
assert(func() === undefined);
assert(func.call(12) === 12);
assert(func.apply(null) === null);
assert(func.call(undefined) === undefined);
assert(func.bind(true)() === true);

Second, it is no longer possible to walk the JavaScript stack in strict mode. Both functionName.caller and functionName.arguments are non-deletable properties which throw an error when set or retrieved, preventing this security hole.

function restricted() {
    'use strict';
    restricted.caller; // TypeError
    restricted.arguments; // TypeError
}

function privilegedInvoker() {
    return restricted();
}

privilegedInvoker();

In addition to functionName.caller throwing an error in strict mode, arguments.caller also throws an error. This enables function abstraction and additional optimizations.

'use strict';
function func(a, b) {
    'use strict';
    var v = 12;
    return arguments.caller; // TypeError
}
func(1, 2);

Preparing for Future Versions

Strict code tries to make the transition to future versions of the ECMAScript standard smoother.

The first way in which strict mode helps is to prohibit certain planned keywords. The following keywords in strict mode generate syntax errors:

  • implements
  • interface
  • let
  • package
  • private
  • protected
  • public
  • static
  • yield

Additionally, function statements which are not located at the top level of a script or function are considered syntax errors in strict mode.

'use strict';
if (true) {
    function func1() { } // syntax error
    func1();
}

for (var i = 0; i < 5; i++) {
    function func2() { } // syntax error
    func2();
}

function foo() {
    function bar() { }
}