Stefan said:
Now checkSlices() is accessible to any user of my Sandwich. I'd rather
hide it, because I may want to change it later
Yes, sure, but saying that, you again think about encapsulation in
"security" reason, afraid that some can change something that you
don't wanna be changed. If someone wants to change encapsulating data
- he will do it. Just opens your source code and make this method
public. The reason is - user needs it. More than, as you know, such
"encapsulated vars" is not 100% hidden [...]
Being able to clearly define an object's public interface is the heart
of the matter. Using underscore doesn't do that.
That's not why I'm hiding the details. I'm not concerned about people
changing or accessing parts of my code against my will. The reason why I
want to keep the hypothetical checkSlices() function from being exposed
as a "public" method is that I need to be able to change its
implementation, or remove it entirely, without worrying about any other
code which may use my object. If they don't see the method, they won't
be tempted to use it, and won't be affected by its change or removal.
That simplifies the object's public interface. Helps ease refactoring.
Also results in the scoped identifiers getting "munged" so the file
size gets squished. Also can help speed up identifier resolution, and
especially in Chrome.
Given the choice, I'd still hide them, because you can present a cleaner
interface to your object.
I agree.
A typical example for a private field would be
a data source (like a database connection object, or an XHR wrapper
object). If the purpose of your object is to present a facade for data
access, the data source will likely be stored as an object property, so
that the facade's methods can access it. From the outside this could
look like:
myObject:
Function loadRecord (loads a record)
Function createRecord (creates a record)
...
Object dataSource (please don't use me)
Responsible users won't call myObject.dataSource.send("raw query")
directly, but IMO the dataSource has no place in myObject's API.
Defining the object's interface is not just about "other" users, but
about the API. An API with dependencies where the dependent parts may be
accessing parts of that API that were "pretend private". When the API is
refactored, the "pretend private" parts can't really be treated as real
private. However, with internal identifiers, the scope of what is
using those identifiers is limited to, well, the "scope".
The other benefit of the makeMeASandwich() type of object generation is
that all methods can access the generator function's scope, and can
easily share data without resorting to this._mySharedObject.
I see three benefits to using generator function's scope:
1) Simple public interface
2) improved performance (fast scope resolution)
3) munge-able local identifiers.
#2 may be doubted by some, and in that case, this was discussed recently
in "where do I define my variables", "Primitive Re-definition"
"[[Delete]]'ing window properties".
I agree, it's good enough. And I'm aware of the memory issue.
What is the memory issue?
This can be deferred and the "private static prototype" methods can
be hidden away, only added when the first call to the "make sandwich"
method.
That's fine by me. If access to a property is required, it was probably
my mistake not to make it public in the first place.
The sandwich generator isn't the perfect match in every situation, and
it isn't required in any, but in some cases it's a useful pattern.
I use this pattern for widgets that decorate HTMLElement, and use
it with a "get or create" or "lazy initialization" pattern, as
"getById". I call it "createFactory".
But getting back to the Sandwich example, the pattern would be used to
create a "SandwichBuilderFactory", where the factory would return a
SandwichBuilder, which would be used to build Sandwiches, which keep
track of calories and contents (cheese, peanut butter, etc).
The SandwichBuilderFactory would be use as such:-
var fatty = SandwichBuilderFactory.create("fatty");
fatty.add("provolone", 100)
.add("bread", 200)
.add("mustard", 5);
Now we have SandwichBuilder "fatty" which should have 305 calories and
"provolone", "bread", and "mustard".
It is worth considering having "bread" be in the constructor if all
sandwiches are going to have bread (a likely design requirement). That
could be possible by using a "newApply" function, but requires more
explanation of both the context, the consequences of that decision, and
and the technical implementation of newApply, which is beside the point
here.
Here is how the SandwichBuilderFactory is implemented:
var SandwichBuilderFactory;
(function(){
SandwichBuilderFactory = createFactory(SandwichBuilder,
createSandwichPrototype);
/* constructor */
function SandwichBuilder(id, baseCalories){
this.id = id;
this.calories = baseCalories||0;
this.contents = [];
}
function createSandwichPrototype() {
/* Private static prototype members area. */
function checkSlices(upperSlice, lowerSlice) {
if (upperSlice.size != lowerSlice.size) {
throw new BreadException();
}
}
/* prototype */
return {
add : function(name, calories) {
this.calories += calories;
this.contents.push(name);
return this;
}
};
}
})();
I have re-written my createFactory in a way that is a little simpler
not using newApply, but being limited to working with 1-arg constructor.
/* get an object with a "create" method that new's constr */
function createFactory(constr, createPrototype){
var instances;
// constructor function
function F(){}
return {create:create};
function create(id) {
var instance;
if(!instances) {
instances = {};
if(typeof createPrototype == "function") {
constr.prototype = createPrototype();
}
}
if(!(id in instances)){
instance = instances[id] = new constr(id);
}
return instance;
}
}
The create method above passes 1 argument to the constructor: id.
AISB, a constructor with multiple parameters can be made to work if the
createFactory is changed to use a newApply. The implementation of that
would look like:-
// Bread is 200 calories.
var pbj = SandwichBuilderFactory.create("pbj", 200);
If factories for constructors of variable argument length
are wanted, then create needs to pass on those arguments. A newApply
method makes that possible.
The context of this pattern is a very important consideration in
its usage, and particularly with multiple arguments.
The |create| method gets an object from any one place in the code. The
first time the object is gotten, the constructor is called. The second
time it is gotten, the constructor is not called. Since the object is
identified by the |id|, that means the following:-
var pbj = SandwichBuilderFactory.create("pbj", 0);
pbj = SandwichBuilderFactory.create("pbj", 200);
would result in pbj.calories === 0.
That does not make the pattern useless but it is an important
consideration. In some contexts, multiple parameter variables with a get
or create method is useful.
This pattern is useful when a callback needs to get an Adapter for an
element.
The benefits are that it allows for lazy instantiation of the object and
prototype.