From 4c4c7165965c378f341896a1d1594af600ad6827 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 25 Nov 2011 17:27:08 -0500 Subject: [PATCH] [#5] Added additional visibility rationale examples --- doc/impl-details.texi | 192 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/doc/impl-details.texi b/doc/impl-details.texi index 0882807..f7c5c2c 100644 --- a/doc/impl-details.texi +++ b/doc/impl-details.texi @@ -510,7 +510,7 @@ inst.push( 'foo' ); console.log( inst._data ); // [ 'foo' ] // uh oh. -inst.pop( 'foo' ); // foo +inst.pop(); // foo console.log( inst._data ); // [] @end verbatim @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; 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 @section Internal Methods/Objects