DEV Community

loading...
Cover image for Automate your getters and setters with Proxies

Automate your getters and setters with Proxies

aminnairi profile image Amin ・5 min read

Cover image credits go to nearsay.com.

Let's say you have a class that represents a motorcycle. It has one property. A brand. But you don't want to write by hand the getters for that class. You could use a Proxy to do the job.

"use strict";

class Motorcycle {
    constructor(constructionYear) {
        this.constructionYear = constructionYear;
    }
}

const withGetters = {
    get(object, property) {
        // "getConstructionYear"
        if (property.startsWith("get")) {
            // "ConstructionYear"
            const getter = property.slice(3);

            // "c"
            const firstLetter = getter[0].toLowerCase();

            // "onstructionYear"
            const rest = getter.slice(1);

            // "constructionYear"
            const fullProperty = firstLetter + rest;

            // motorcycle.getConstructionYear()
            return () => object[fullProperty];
        }

        // motorcycle.constructionYear
        return object[property];
    }
};

const motorcycle = new Proxy(new Motorcycle(2020), withGetters);

console.log(motorcycle.constructionYear);       // 2020
console.log(motorcycle.getConstructionYear());  // 2020
Enter fullscreen mode Exit fullscreen mode

We want to access a property

Let's explain the code step by step.

First we have our class. We defined a constructor method in which we choose to receive one property. We then attach the property. Plain and simple.

Next, we have our Proxy handler. It will receive all properties and methods accessed, just like a Web Proxy receiving the request before processing it (like Service Workers). If we try to access a method that starts with get, it means we want to access a property using its getter. But we do not have one. So we try to convert this method name into its property name. Once we know what property the user is trying to access, we can fake the method call by returning a function that will just return the property from that object.

And if the property does not start with a get, it means that our job is done and we just return the accessed property.

Now we just have to instanciate our class by wrapping it up with a proxy. Next time we try to access a property, we can use both the getter and the property syntax. This also means that it will be automated for all properties that we decide to attach to our instance.

"use strict";

class Motorcycle {
    constructor(brand, model, constructionYear) {
        this.brand = brand;
        this.model = model;
        this.constructionYear = constructionYear;
    }
}

const withGetters = {
    get(object, property) {
        // "getConstructionYear"
        if (property.startsWith("get")) {
            // "ConstructionYear"
            const getter = property.slice(3);

            // "c"
            const firstLetter = getter[0].toLowerCase();

            // "onstructionYear"
            const rest = getter.slice(1);

            // "constructionYear"
            const fullProperty = firstLetter + rest;

            // motorcycle.getConstructionYear()
            return () => object[fullProperty];
        }

        // motorcycle.constructionYear
        return object[property];
    }
};

const motorcycle = new Proxy(new Motorcycle("Triumph", "Street Triple", 2020), withGetters);

console.log(motorcycle.brand);              // "Triumph"
console.log(motorcycle.model);              // "Street Triple"
console.log(motorcycle.constructionYear);   // 2020

console.log(motorcycle.getBrand());              // "Triumph"
console.log(motorcycle.getModel());              // "Street Triple"
console.log(motorcycle.getConstructionYear());   // 2020
Enter fullscreen mode Exit fullscreen mode

Getters & setters

Of course we could do the same for setters as well.

"use strict";

class Motorcycle {
    constructor(brand, model, constructionYear) {
        this.brand = brand;
        this.model = model;
        this.constructionYear = constructionYear;
    }
}

function getPropertyFromGetterSetter(property) {
    const sliced = property.slice(3);
    const firstLetter = sliced[0].toLowerCase();
    const rest = sliced.slice(1);

    return firstLetter + rest;
}

const withGettersSetters = {
    get(object, property) {
        // "getConstructionYear"
        if (property.startsWith("get")) {
            // motorcycle.getConstructionYear()
            return () => object[getPropertyFromGetterSetter(property)];
        }

        if (property.startsWith("set")) {
            // motorcycle.setConstructionYear(2021)
            return (newValue) => {
                object[getPropertyFromGetterSetter(property)] = newValue;
            };
        }

        // motorcycle.constructionYear
        return object[property];
    }
};

const motorcycle = new Proxy(
    new Motorcycle("Triumph", "Street Triple", 2020),
    withGettersSetters
);

console.log(motorcycle.getConstructionYear()); // 2020

motorcycle.setConstructionYear(2021);

console.log(motorcycle.getConstructionYear()); // 2021
Enter fullscreen mode Exit fullscreen mode

You could even use the proxyfication inside your class in the constructor to ease the syntax.

"use strict";

function getPropertyFromGetterSetter(property) {
    const sliced = property.slice(3);
    const firstLetter = sliced[0].toLowerCase();
    const rest = sliced.slice(1);

    return firstLetter + rest;
}

const withGettersSetters = {
    get(object, property) {
        // "getConstructionYear"
        if (property.startsWith("get")) {
            // motorcycle.getConstructionYear()
            return () => object[getPropertyFromGetterSetter(property)];
        }

        if (property.startsWith("set")) {
            // motorcycle.setConstructionYear(2021)
            return (newValue) => {
                object[getPropertyFromGetterSetter(property)] = newValue;
            };
        }

        // motorcycle.constructionYear
        return object[property];
    }
};

class Motorcycle {
    constructor(brand, model, constructionYear) {
        this.brand = brand;
        this.model = model;
        this.constructionYear = constructionYear;

        return new Proxy(this, withGettersSetters);
    }
}

const motorcycle = new Motorcycle("Triumph", "Street Triple", 2020);

console.log(motorcycle.getConstructionYear()); // 2020

motorcycle.setConstructionYear(2021);

console.log(motorcycle.getConstructionYear()); // 2021
Enter fullscreen mode Exit fullscreen mode

And you could even go further (if you do not extend from any other classes) by creating a class for an easier integration with child classes.

"use strict";

function getPropertyFromGetterSetter(property) {
    const sliced = property.slice(3);
    const firstLetter = sliced[0].toLowerCase();
    const rest = sliced.slice(1);

    return firstLetter + rest;
}

const withGettersSetters = {
    get(object, property) {
        // "getConstructionYear"
        if (property.startsWith("get")) {
            // motorcycle.getConstructionYear()
            return () => object[getPropertyFromGetterSetter(property)];
        }

        if (property.startsWith("set")) {
            // motorcycle.setConstructionYear(2021)
            return (newValue) => {
                object[getPropertyFromGetterSetter(property)] = newValue;
            };
        }

        // motorcycle.constructionYear
        return object[property];
    }
};

class GettersSetters {
    constructor() {
        return new Proxy(this, withGettersSetters);
    }
}

class Motorcycle extends GettersSetters {
    constructor(brand, model, constructionYear) {
        super();

        this.brand = brand;
        this.model = model;
        this.constructionYear = constructionYear;
    }
}

const motorcycle = new Motorcycle("Triumph", "Street Triple", 2020);

console.log(motorcycle.getConstructionYear()); // 2020

motorcycle.setConstructionYear(2021);

console.log(motorcycle.getConstructionYear()); // 2021
Enter fullscreen mode Exit fullscreen mode

Advantages

A huge advantage to using Proxy to automate your getters & setters is that it is now easier to write trivial classes that do not have much logic in the setters & getters.

Also, people that have no IDEs like me (I code using VIM on my terminal) and that do not have access to getters/setters generator can now enjoy writing classes with getters and setters as quickly as you would in an IDE.

And another great advantage is that you don't have to think about removing you getters/setters for unused properties that you will want to remove since it is computed by the proxy at runtime.

There are also drawbacks from using this technique but I'll let you experiment and go as far as you can to see it in action and make yourself an idea of what Proxies can bring to your project.

To sum up

There are, in my opinion, unlimited possibilities for using Proxies in your project. This getter/setter example was just a pretext to show you how awesome Proxies are in JavaScript. But you could go so much further and do something similar to what Symfony does with their annotations by creating a Proxy that will be responsible to turn a class into an entity connected to your API. This is left as an exercise for the reader and you could begin by creating a Proxy that will communicate with an API like JSONPlaceholder, maybe a class called Fetchable to make it sounds and look cool!

So that's it for today. I hope you see now that Proxies are really powerful. Let me know if you have some other useful usage for Proxies and let's discuss about that in the comment section below. Thanks for reading me!

Discussion (5)

pic
Editor guide
Collapse
adamkaram profile image
Adam karam

thank u so much but i did change it a quiet little bit to Fits my needs like this
"use strict";

    let _object = {
        id: 'SD-10',
        location: 'Sv',
        addr: '123 str',
        // getSetGen: () => {

        // }
    };

    function log([...param]) {
        var v = param.map(x => console.log(x))


    }


    function getPropertyFromGetterSetter(property) {
        const sliced = property.slice(3);
        const firstLetter = sliced[0].toLowerCase();
        const rest = sliced.slice(1);



        return firstLetter + rest;
    }
    // علشان تحدد اذا كان ال Moethod
    //هتكون getter ولا setter  
    const withGettersSetters = {

        get(object, property) {
            // "getConstructionYear"
            if (property.startsWith("get")) {
                // getSetGen.get x , y  , z ()

                return () => object[getPropertyFromGetterSetter(property)];
            }

            if (property.startsWith("set")) {
                // getSetGen.set y , x ,z(value)
                return (newValue) => {
                    object[getPropertyFromGetterSetter(property)] = newValue;
                };
            }

            // getSetGen. x , y , z 
            return object[property];
        }
    };

    class GettersSetters {
        constructor() {
            return new Proxy(this, withGettersSetters);
        }
    }

    class getSetGen extends GettersSetters {
        constructor([obj , ...params]) {
            super();
            params = [...params]
            var that = this;
            var objectKeys = Object.keys(obj);
            var param = params.map(x => x)
            objectKeys.forEach((el, index) => {
                that.el === param[index]



            });
        }
    }
    const GetSetGen = new getSetGen([_object , 'SD-v12e', 'Street sw2', 2020]);

    console.log(GetSetGen.getId()); // undefined
Enter fullscreen mode Exit fullscreen mode

the main proplem it here objectKeys.forEach((el, index) => {
that.el =param[index]});

i think all i need ti do here's i want to loop throw object propeties and use it and as u see from my params i send the values and loop throw it ans evently i need to
to make equality between them like here
that.el = param[index]
but i all got undefined
console.log(GetSetGen.getId()); // undefined

Collapse
stephengfriend profile image
Stephen G. Friend

Hi Amin, can you expand on why you chose to use the same get trap for the setter? Is there an advantage to not putting this in the set trap?

Collapse
aminnairi profile image
Amin Author

Hi Stephen and thanks for your question which is interesting!

The set trap will be triggered when you set a property for the object that is being proxied.

motorcycle.brand = "Triumph";

But when using the setter method motorcycle.setBrand("Triumph"); you are really just triggering the get trap, whether it is a property or in this case a method. Using a set trap wouldn't have helped us much.

Hope this answers your question.

Collapse
nahidmbstu profile image
Nahid Hasan

can you give some example in react js

Collapse
aminnairi profile image
Amin Author

Hi Nahid and thanks for your comment!

Unfortunately, I haven't experienced enough with React to find something that could be useful. By creating this article, I was thinking more about use-cases related to the inner logic of your application, rather than something bound to a framework in specific.

But since React can be used with classes, you could make your own component class, for instance FetchableComponent to reuse api calls for instance (yeah I know, I'm giving the answer, it's too tempting).

import React from "react";
import ReactDOM from "react-dom";

class FetchableComponent extends React.Component {
    constructor() {
        super();

        return new Proxy(this, {
            get(target, property) {
                if (property === "api") {
                    return endpoint =>
                        fetch(
                            `https://jsonplaceholder.typicode.com/${endpoint}`
                        ).then(response => {
                            if (response.ok) {
                                return response.json();
                            }

                            throw new Error("unable to fetch the api");
                        });
                }

                return target[property];
            }
        });
    }
}

class UsersList extends FetchableComponent {
    constructor() {
        super();

        this.state = {
            users: []
        };
    }

    componentDidMount() {
        this.api("users").then(users => {
            this.setState({users});
        });
    }

    render() {
        return (
            <div>
                <h1>Users</h1>
                <ul>
                    {this.state.users.map(({id, username}) => (
                        <li key={id}>{username}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

ReactDOM.render(<UsersList />, document.getElementById("users"));

In this case, you could see FetchableComponent like some sort of Trait like in PHP. Except you cannot use multiple trait like in PHP in this case since it is an extend. But I think you get the idea.

I haven't done React for a while so pardon my mistakes if there are any. What is important is the idea behind Proxies and React. I hope that answer your question!