2

Context

I am writing a JavaScript library, and I am finding that I don't know what the best way to support object creation. I came up with a list things that I am considering:

var obj = Library.newObj('object-type');
var obj = Library.newObj(Library.ObjectType); // Library.ObjectType is an object containing information about how to create the new object
var obj = Library.newObj(Library.ObjectType); // Library.ObjectType is an integer specifying the type
var obj = new Library('object-type');
var obj = new Library(Library.ObjectType); // Library.ObjectType is an object containing information about how to create the new object
var obj = new Library(Library.ObjectType); // Library.ObjectType is an integer specifying the type
var obj = new Library.ObjectType();

In particular, these objects should also have specifiable options. This would then look like

var obj = Library.newObj('object-type', { /* options */ });
var obj = Library.newObj(Library.ObjectType({ /* options */ }));
var obj = Library.newObj(Library.ObjectType, { /* options */ });
var obj = new Library('object-type', { /* options */ });
var obj = new Library(Library.ObjectType({ /* options */ }));
var obj = new Library(Library.ObjectType, { /* options */ });
var obj = new Library.ObjectType({ /* options */ });

// Or, when the user wants to change options separately:
obj.setOptions({ /* options */ });
obj.setOption1(val1).setOption2(val2);

I am thinking that I might not want to specify the object type simply by a string or an integer because this might be difficult to maintain and is not very extensible.

Question

What are the pros and cons of these variants? Is there a best-practice / convention that I am not aware of? Should I support multiple ways to create objects? (Or are there any other variants that I did not consider?)

My Specific Case

(The particulars of my situation are that I want to allow developers using the library to provide their own 'object creation templates' that work with the library, but the library will also provide some pre-made 'templates' (like Library.ObjectType). Furthermore, I don't want to expose more than one thing (like Library above) to the global namespace when used in the browser. But would still like to avoid dot-horror, eg:

Library.objects.objectCreator.createObject(Library.objects.objectTypes.ObjectType1);
hugabor
  • 23
  • 3
  • Possible duplicate of [Are there any OO-principles that are practically applicable for Javascript?](https://softwareengineering.stackexchange.com/questions/180585/are-there-any-oo-principles-that-are-practically-applicable-for-javascript) – gnat Aug 09 '17 at 06:13
  • 1
    see also: [What is the problem with “Pros and Cons”?](https://softwareengineering.meta.stackexchange.com/q/6758/31260) – gnat Aug 09 '17 at 06:13

1 Answers1

6

Probably don't use new

You probably don't want to use the new operator very often in JS unless the function you're directly invoking is the constructor. Calling new X(…) does this:

  1. A new object is created.
  2. This object inherits from X.prototype.
  3. The constructor function X(…) is executed with this bound to the new object.
  4. If the constructor doesn't return anything, the new object is returned instead.

The new operator is therefore unsuitable or at the least misleading when calling a generic factory function instead of a concrete constructor.

Type tags (integers, strings, or objects)

By using some kind of type tag, you are creating a very dynamic system. This means the system is harder to reason about for humans, and also more difficult to process for tools like linters, type checkers, and type based autocomplete in IDEs. These tools have value, so rendering them less useful should not be done lightly.

Some problems do need this flexibility. But in most cases, all you're doing is adding your own (unnecessary) mechanism for object creation or for method calls on top of the actual language.

One good reason for this is if you want very clear encapsulation. JavaScript has no concept of public/private accessibility, and object properties can be changed at any time. The only way to really hide something is to use closures. This kind of paranoia is not generally needed.

Using literal strings as type tags is fairly readable, but is very susceptible to typos.

Using integer IDs as type tags should be avoided because they are not very debuggable. They are effectively untyped: if you see an integer somewhere you can't be sure that it was intended as a type tag, or is just a normal number. And you might have difficulty mapping the ID back to the type.

Using “constants” for the type tags like Library.newObj(Library.ObjectType, ...) seems reasonable at first, but is very unnecessary: If you can expose an ObjectType field, that could be a constructor or factory on its own: Library.ObjectType(…) or new Library.ObjectType(…) or Library.ObjectType.create(…).

Dot-Horror? Please design your public API

Complaining about dot-horror (or Law of Demeter violations) is a bit of a strawman here. You've shown this potential example:

Library.objects.objectCreator.createObject(Library.objects.objectTypes.ObjectType1);

This is silly because users are free to alias parts of your library to a name of their choosing:

var LibObjects = Library.objects;
var createLibObject = LibObjects.objectCreator.createObject;
var LibTypes = LibObjects.objectTypes;

...

createLibObject(LibTypes.ObjectType1);

Additionally, you can design the library to avoid such problems. The exported Library symbol does not have to be the “root” of your library from which all classes are reachable. Instead, it is a facade for the functionality of the library. Auxiliary objects have no place here. The publicly visible structure of your library doesn't have to precisely match the internal structure. It is sometimes sensible to divide your public symbols into multiple sub-objects for namespacing, but not to the point where there's more structure than substance.

In the above example, the createObject function could have been lifted to the top level of the library namespace, and the objectTypes to a sub-namespace:

var Library = {
  createObject: function(x) {  // TODO arguments forwarding
    return objects.objectCreator.createObject(x);
  },
  types: objects.objectTypes,
  ...
};

Then:

Library.createObject(Library.types.ObjectType1)

which is somewhat reasonable. While assembling your facade you can also do other transformations, like adding a Library.newObjectType() function for each registered ObjectType type tag. Even if you use these type tags internally, they do not have to be part of your public API.

amon
  • 132,749
  • 27
  • 279
  • 375