Private Names and Methods

Allen Wirfs-Brock allen at wirfs-brock.com
Thu Apr 5 12:23:42 PDT 2012


On Apr 4, 2012, at 6:27 PM, Kevin Smith wrote:

> ...
> Will private methods, implemented with private name objects, provide us with the same guarantees?  Let's refactor again, this time using a private name for the new method:
> 
>     // Assume that there exists a "const M3 = Name.create();" somewhere above
> 
>     M1() {
>       <A>
>       <B>
>       this[M3]();
>     }
> 
>     M2() {
>       <D>
>       this[M3]();
>       <E>
>     }
> 
>    [M3]() {
>       <C>
>     }
> 
> Since M3 is not visible to any code that does not also have a reference to the M3 private name, we can say that this refactoring provides the same public interface.  But is it functionally identical?  
> 
> Let's assume that prior to the refactoring, M1 and M2 were generic methods.  That is, they reference only properties of |this| which correspond to some subset S of the public interface of the class.  Any other object which implements S can also be used with M1 and M2 (like the generic methods defined on Array.prototype).
> 
> After the refactoring shown above, there's no possibility that M1 and M2 can be generic, since they access a privately named property of |this| which by definition is not a part of the public interface of the class.  So in this sense, the refactored code is not functionally identical to the original code.

I don't think this is really a problem or if it is, that the problem isn' specific to private name based procedural decomposition.  Consider such a "generic method".  How can you actually use such a method with "any other object that implements S".  In ES there are basically two ways that are pretty much semantically equivalent.  You access the M1 function value and install it as a method (with a name of your choosing) in the other object and invoke it as a method.  Or, you use Function.prototype.call (or apply) to invoke the functions with the other object as the this value.  Either technique only works if M1 is completely self contained or is only dependent upon the S interface.  The manner in which it is not self contained is really important.  Any mechanism that depends upon dynamic coupling to state or behavior that  is not part of S breaks the generic-ness of M1.  A reference to a Java-like private declaration would still have the same problem (at least for state access) as the "other object" would not have the proper shape to support that reference. 

If you want to refactor the implementation of M1 you must keep it "self contained" in order to keep it generic.  A captured lexical reference to a helper function does that. 

> 
> If we wanted to preserve identical functionality, we could define M3 as a regular function and call it using the call method of Function.prototype:
> 
>     // Somewhere *before* the class definition we have:
>     function M3() {
>       <C>
>     }
> 
>     ...
> 
>     M1() {
>       <A>
>       <B>
>       M3.call(this);
>     }
> 
>     M2() {
>       <D>
>       M3.call(this);
>       <E>
>     }
> 
> The above strategy works, but (a) explicitly calling M3 via call() is awkward, and (b) it defeats the purpose of an elegant class syntax if we must refactor code into functions outside of the class definition.

You don't have to use call, you can just define an additional argument for M3 (which should perhaps be called F3 as it isn't actually a method).  A principle of refactoring is that valid refactorings are based upon well defined rules that preserve the semantics of the original code across the transformation.  The transformations/rule sets are typically given meaningful names. In this case, the refactoring you want to perform is not "Extract Method" but something else, perhaps called "Extract helper function from method".  Just like "extract method", the refactoring would turn any references to method arguments or local declarations (outside of the extracted fragment) into explicit parameters of the extracted function.  However, if the fragment contained any "this" references, it must also provide an explicit parameter for transmitting the this value.  The proper refactoring would be:

    M1() {
      <A>
      <B>
      F3(this, /*any local values referenced by <C>*/);
    }

    M2() {
      <D>
      F3(this, /*any local values referenced by <C>*/);
      <E>
    }

   // ES scoping rules permit this to appear either *before*or *after*  the class definition:
    function F3(baseThis,  /* any free variables in <C>*/   ) {
      <C with all occurrences of "this" replaced with "baseThis">
    }

Allen

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20120405/f1312435/attachment.html>


More information about the es-discuss mailing list