1
0
Fork 0

[#5] Added additional visibility rationale examples

closure/master
Mike Gerwitz 2011-11-25 17:27:08 -05:00
parent 9aaef9736e
commit 4c4c716596
1 changed files with 191 additions and 1 deletions

View File

@ -510,7 +510,7 @@ inst.push( 'foo' );
console.log( inst._data ); // [ 'foo' ] console.log( inst._data ); // [ 'foo' ]
// uh oh. // uh oh.
inst.pop( 'foo' ); // foo inst.pop(); // foo
console.log( inst._data ); // [] console.log( inst._data ); // []
@end verbatim @end verbatim
@caption{Working easily with instance members in JavaScript breaks @caption{Working easily with instance members in JavaScript breaks
@ -704,6 +704,196 @@ This method of storing instance data was ease.js's initial ``proof-of-concept''
implementation (@pxref{Class Storage}). Clearly, this was not going to work; implementation (@pxref{Class Storage}). Clearly, this was not going to work;
some changes to this implementation were needed. some changes to this implementation were needed.
@subsubsection Instance Memory Considerations
JavaScript does not provide destructors to let us know when an instance is about
to be GC'd, so we unfortunately cannot know when to free instance data from
memory in @ref{f:js-encapsulate-instance}. We are also not provided with an API
that can return the reference count for a given object. We could provide a
method that the user could call when they were done with the object, but that is
not natural to a JavaScript developer and they could easily forget to call the
method.
As such, it seems that the only solution for this rather large issue is to store
instance data on the instance itself so that it will be freed with the instance
when it is garbage collected (remember, we decided that privileged members were
not an option in the discussion of @ref{f:js-privileged-members}). Hold on - we
already did that in @ref{f:js-proto-inst-noencapsulate}; that caused our data to
be available publicly. How do we approach this situation?
If we are adding data to an instance itself, there is no way to prevent it from
being accessed in some manner, making true encapsulation impossible. The only
options are to obscure it as best as possible, to make it too difficult to
access in any sane implementation. For example:
@itemize
@item
The property storing the private data could be made non-enumerable, requiring
the use of a debugger or looking at the source code to determine the object
name.
@itemize
@item This would work only with ECMAScript 5 and later environments.
@end itemize
@item
We could store all private data in an obscure property name, such as
@var{___$$priv$$___}, which would make it clear that it should not be accessed.
@itemize
@item
We could take that a step further and randomize the name, making it very
difficult to discover at runtime, especially if it were
non-enumerable@footnote{Note that ease.js does not currently randomize its
visibility object name.}.
@end itemize
@end itemize
Regardless, it is clear that our data will only be ``encapsulated'' in the sense
that it will not be available conveniently via a public API. Let's take a look
at how something like that may work:
@float Figure, f:js-obscure-private
@verbatim
var Stack = ( function()
{
// implementation of getSomeRandomName() is left up to the reader
var _privname = getSomeRandomName();
var S = function()
{
// define a non-enumerable property to store our private data (will only
// work in ES5+ environments)
Object.defineProperty( this, _privname, {
enumerable: false,
writable: false,
configurable: false,
value: {
stack: []
}
} );
};
S.prototype = {
push: function( val )
{
this[ _privname ].stack.push( val );
},
pop: function()
{
return this[ _privname ].stack.pop();
},
};
return S;
} );
var inst = new Stack();
inst.push( 'foo' );
inst.pop(); // foo
@end verbatim
@caption{Using a random, non-enumerable property name to store private members}
@end float
Now we are really starting to hack around what JavaScript provides for us. We
seem to be combining the encapsulation issues presented in
@ref{f:js-proto-inst-noencapsulate} and the obscurity demonstrated in
@ref{f:js-encapsulate-instance}. In addition, we our implementation depends on
ECMAScript 5 (ideally, we would detect that and fall back to normal, enumerable
properties in pre-ES5 environments, which ease.js does indeed do). This seems to
be a case of encapsulation through obscurity@footnote{A play on ``security
through obscurity''.}. While our implementation certainly makes it difficult to
get at the private member data, it is also very obscure and inconvenient to work
with. Who wants to write Object-Oriented code like that?
@subsubsection Other Considerations
We have conveniently omitted a number of other important factors in our
discussion thus far. Before continuing, they deserve some mention and careful
consideration.
How would we implement private methods? We could add them to our private member
object, just as we defined @var{stack} in @ref{f:js-obscure-private}, but that
would cause it to be redefined with each instance, raising the same issues that
were discussed with @ref{f:js-privileged-members}. Therefore, we would have to
define them in a separate ``prototype'', if you will, that only we have access
to:
@float Figure, f:js-obscure-private-methods
@verbatim
var Stack = ( function()
{
// implementation of getSomeRandomName() is left up to the reader
var _privname = getSomeRandomName();
var S = function()
{
// define a non-enumerable property to store our private data (will only
// work in ES5+ environments)
Object.defineProperty( this, _privname, {
// ... (see previous example)
} );
};
// private methods that only we will have access to
var priv_methods = {
getStack: function()
{
// note that, in order for 'this' to be bound to our instance, it
// must be passed as first argument to call() or apply()
return this[ _privname ].stack;
},
};
// public methods
S.prototype = {
push: function( val )
{
var stack = priv_methods.getStack.call( this );
stack.push( val );
},
pop: function()
{
var stack = priv_methods.getStack.call( this );
return stack.pop();
},
};
return S;
} );
var inst = new Stack();
inst.push( 'foo' );
inst.pop(); // foo
@end verbatim
@caption{A possible private method implementation}
@end float
While this does solve our problem, it further reduces code clarity. The
implementation in @ref{f:js-obscure-private-methods} is certainly a far cry from
something like @samp{this._getStack()}, which is all you would need to do in
ease.js.
Another consideration is a protected (@pxref{Access Modifiers}) member
implementation, the idea being that subtypes should inherit both public and
protected members. Inheritance is not something that we had to worry about with
private members, so this adds an entirely new layer of complexity to the
implementation. This would mean somehow making a protected prototype available
to subtypes through the public prototype. Given our implementation in the
previous figures, this would likely mean an awkward call that somewhat
resembles: @samp{this[ _protname ].name}.
Although the implementations show in @ref{f:js-obscure-private} and
@ref{f:js-obscure-private-methods} represent a creative hack, this is precisely
one of the reasons ease.js was created - to encapsulate such atrocities that
would make code that is difficult to use, hard to maintain and easy to introduce
bugs. One shouldn't have to have a deep understanding of JavaScript's prototype
model in order to write the most elementary of Classical Object-Oriented code.
For example, the constructors in the aforementioned figures directly set up an
object in which to store private members. ease.js will do this for you before
calling the @code{__construct()} method. Furthermore, ease.js does not require
referencing that object directly, like we must do in our methods in
@ref{f:js-obscure-private}. Nor does ease.js have an awkward syntax for invoking
private methods. We will explore how this is handled in the following section.
@node Internal Methods/Objects @node Internal Methods/Objects
@section Internal Methods/Objects @section Internal Methods/Objects