Search code examples
javascriptclosuresrefactoringprivate-methods

Change the behavior of a function in closure


I have to upgrade a small private function in a 1500 LoC JS module, written in The Revealing Module Pattern.

Simplified task looks like this:

var module = module || function (){

    function init(){
        // you can upgrade init anyhow to be able to replace private_method later in runtime

        (function not_important(){console.log("I do some other stuff")})()
    }

    function public_method(){
        private_method()
    }

    function private_method(){
        console.log("original private method")
    }

    // you can add any methods here, too

    return {
        init: init,
        public_method: public_method,
        // and expose them
    }
}()


/** this is another script on the page, that uses original module */
module.public_method()  // returns "original private method"

/** the third script, that runs only in some environment, and requires the module to work a bit different */
// do any JS magic here to replace private_method
// or call module.init(method_to_replace_private_method)
module.public_method()  // I want to see some other behavior from private_method here

I have studied
Accessing variables trapped by closure
how to get an object from a closure?
questions already, which looks like most relevant to my task, but without success.

The only working solution I found is to rewrite function private_method(){} to this.private_method = function(){...} which binds it to the window, so I can change it in runtime.

But I don't go this way, because the method becomes not private anymore, and I can't predict what may become broken in the old 100000 LoC spaghetti monster app, written in old-style ECMAScript 5.

UPDATE
This is the answer on https://stackoverflow.com/a/65156282/7709003 answer.

First of all, thank you for the detailed answer, T.J. Crowder.

this is usually called monkeypatching

Holy true.

You can't, unless you expose that private in some way.

Right. I just don't want to expose the method right into the window scope. I thinks the solution could be more elegant.

it appears you can change the source code

Well, as you already have noticed, this is confusing, but point is, I'm not yet sure myself. I have to keep the default module behavior, but I hope I can extend the module a bit, to help to apply this monkey patch.

If you want the full story, there is an IoT device, running the WebApp, which uses the module we discuss to translate the microcontroller registers state into a DOM view. Let's call it registers_into_view module.

By copying the same module, a web-server was created, which:

  • receives a microcontroller memory dump
  • the copy of registers_into_view module creates a model View (which normally should happen on the Frontend)
  • sends the View a form of JSON

By extending the same WebApp, the "Cloud" web-app was created, which:

  • neglates existing registers_into_view module, but instead
  • receives the View data from the backend
  • applies it directly into DOM

Normally, I would refactor the whole architecture:

  • delete the copy of registers_into_view module on the backend
  • reuse existing registers_into_view module on the frontend

But the company A, that uses the stack, refuse to do so. The existing schema works for 6 years for them. In fact, their managers avoid big changes, coz the company B, that had created this software, charge a lot of money for the work. So they have no motivation to refactor.

Well, I have. I work for company C and we're going to sell same IoT devices.
We also want to use the existing Frontend WebApp.
For this needs, I extend our existing IoT server to support those devices and their frontend.
So I have to copy another server API contracts, using another language and framework.

Now, instead of recreating 1500 LoC registers_into_view module, that runs on the backend server, I think to import already existing registers_into_view module into a "Cloud" WebApp and monkey-patch it to retreive registers data from JSON instead of a memory dump (our private_method).

The intrusion should be as little as possible, to higher chances to my patch to be merged.

Now I hope my motivation is clear enough. I find it not so interesting to everybody and so tried to clean the programming task from the context.

Let's get to solutions.

Solution 2 of yours

I cannot change the public_method of the module, because in reality it runs ~50 other private methods, which all call retreive_data_method just with different requests and put results in a different places in DOM.

Solution 1

Works like a charm. So simple and elegant. I will just simplify it a bit to my needs:

var module = module || function (){
    function public_method(){private_method()}
    function private_method(){console.log("original private method")}
    return {
        public_method: public_method,
        
        // this function allows to update the private_method in runtime
        replace_private_method: function(fn) {
                // Function declarations effectively create variables;
                // you can simply write to them:
                private_method = fn;
        }
    }
}()

// original behavior
module.public_method()

// patching
module.replace_private_method(function() {console.log("I've been monkey patched")});
// new behavior
module.public_method()

Instead of direct replace, as in your solution, I've tried to save the module context in some exposed variable and find the private method via it. Which didn't work.

Thanks.


Solution

  • I have to upgrade a small private function in a 1500 LoC JS module, written in The Revealing Module Pattern.

    I take it you mean you have to do this at runtime, from outside the "module" function. This is usually called "monkeypatching."

    You can't, unless you expose that private in some way.

    The only working solution I found is to rewrite function private_method(){} to this.private_method = function(){...} which binds it to the window, so I can change it in runtime.

    If you can do that, then it appears you can change the source code (leading me to this question on the question).

    But if you can change the source code, then you can do this (see *** comments):

    var module = module || function (){
        function init(){
            (function not_important(){console.log("I do some other stuff")})()
        }
    
        function public_method(){
            private_method()
        }
    
        function private_method(){
            console.log("original private method")
        }
    
        return {
            init: init,
            public_method: public_method,
            // *** Provide yourself functions to get `private_method` (and any
            // others you may want) and update it
            __privates__: {
                private_method: {
                    get: function() {
                        return private_method;
                    },
                    set: function(fn) {
                        // *** Function declarations effectively create variables;
                        // you can write to them:
                        private_method = fn;
                    }
                }
            }
        }
    }()
    
    // *** Where you want to make your change
    module.__privates__.private_method.set(function() { /* ... */ });
    

    You can generalize (and arguably simplify) that by putting all private methods on an object that you call them through, but it means either calling them with a different this than they may be expecting or making those calls a bit more awkward:

    var module = module || function (){
        /*** An object with the private functions you need to do this for
        var privates = {};
    
        function init(){
            (function not_important(){console.log("I do some other stuff")})()
        }
    
        function public_method(){
            // *** Calling it via that object, which has an effect on `this`
            privates.private_method()
            // *** If you want `this` to be the same as it would have been
            // with the raw call above (the global object or `undefined` if
            // you're in strict mode), you can use the comma trick:
            // (0,privates.private_method)()
        }
    
        privates.private_method = function private_method(){
            console.log("original private method")
        };
    
        return {
            init: init,
            public_method: public_method,
            // *** Expose that object with the private functions
            __privates__: privates
        }
    }()
    
    // *** Where you want to make your change
    module.__privates__.private_method = function() { /* ... */ };