Skip to content

Instantly share code, notes, and snippets.

@brainysmurf
Last active September 24, 2023 12:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brainysmurf/818dac872a5a79f89c2bba8595f125d7 to your computer and use it in GitHub Desktop.
Save brainysmurf/818dac872a5a79f89c2bba8595f125d7 to your computer and use it in GitHub Desktop.
Decorator Pattern. A way to manipulate function behavior other than sending parameters.

Decorator Pattern in Apps Scripts

Introduction

We'll identify a problem that can be solved with the decorator pattern. This is essentially "decorating" a wrapping function where there is additional implementation "inside". It's an effective way to define a inject some code around before or after a function executes. Here is an example that times how long it took to execute a function:

var timeit, publicFunction;

timeit = function (f) { /* definition below */ };

publicFunction = timeit(
  function () {
    Utilities.sleep(1000);
  }
);

where timeit is defined as:

timeit = function (f) {
  var before = new Date().getTime();
  f();  // invoke the function
  var after - new Date().getTime();
  Logger.log("It took " + (after - before) / 1000 + " seconds.");
}

A similar sort of wrapping technique can be used where — instead of of invoking the function — we add some properties onto it instead:

var decorator, publicFunction;

decorator = function (wrapped) {
  wrapped.extra = 'extra';
  return wrapped;
}

publicFunction = decorator(function Me () {
  Logger.log(Me.extra);
});

Notice that we have added an extra property that can be retrieved from within the defined function (since we named the function Me), and outside of it. This means that we can change the implementation details of the function by accessing and modifying its properties, rather than just sending in parameters.

Problem

Let's solve a problem using the second variant of the decorator pattern, where we interact with functions and change its behaviour by setting its properties, instead of sending parameters.

When interacting with Google API endpoints, there are standard query parameters that can apply to every call. The one we care about the most for this example is the fields query parameter which determines the content of the json that is returned.

https://example.com/endpoint/1234567890?fields=path.of.information

The above tells the API to only return an object that has a property path, which has a property of and then a property of information. Controlling this aspect to the endpoint is useful in applications where large amounts of data is returned but we just want to zero-in on some specific part of the information. It's best practice to reduce overhead such as this, not only for performance reasons but also for our own sanity's sake.

Suppose we have a group of functions that interact with different sheets api endpoints (here just called example.com), but we don't want to keep defining an options or opt parameter for each of these functions. That would be one way to do it:

var getRequest = function (id, opt) {
  opt.fields = opt.fields || '*';  // default is *, meaning "everything"

  // make the get request:
  UrlFetchApp.fetch('https://example.com/endpoint/' + id + '?fields=' + opt.fields
}

In fact, fields has a default value and implementing that boilerplate would get tedious, in, say, a library. However, the value of fields potentially could be different for each api endpoint (because the names of the properties of what they return are different between each), which means that defining a constant outside of that function scope would also be equally tedius.

So let's solve it using a decorator:

var apiObject = {};
var standardQueryDecorator = function (func) { /* definition below */ };

apiObject.getValues = standardQueryDecorator(
  function Me (spreadsheetId, range) {
    return UrlFetchApp(url + '/' + spreadsheetId
                           + '?ranges=' + range 
                           + '&' + Me.options['fields']);
  }
);
apiObject.setValues = standardQueryDecorator(  /* ... and so on ... */  );

where standardQueryDecorator is defined as:

standardQueryDecorator = function (func) {
  func.options = {
    fields: '*',
    prettyPrint: false,
    // others...
  };
  func.setOption = function (name, value) {
    func.options[name] = value;
  };
  return func;
}

And if we want to change the default behaviour, we just do this, once:

apiObject.getValues.setOption('fields', 'sheets');  // only return sheet info

And thus every subsequent call to .getValues will tell the api endpoint about the only limited response information that we are interested in.

@jhavenz
Copy link

jhavenz commented Apr 8, 2020

Hello Sir,

I've recently come across your modular libraries for GAS and really see great possibilities in using them.
I'm, I'd say, an intermediate level developer whose had to dive into GAS a lot over the past few months for some work I'm doing.

After reading several pages of explanations of creating a library that I can then Import and use, I'm still confused as to, I guess you'd say, the "order of operations" in which the methods and properties of the class are fired, once a library in instantiated.

One basic way I'd like to start using these libraries is for when I'm instantiating different Spreadsheets within one script. I find myself having to create config files for each sheet (which contain their sheet ids, sheet names, columns names, which column names have which index, etc), then reference those config files throughout each script, use the returned configs to instantiate sheets/values/ranges, then write the same ol' SpreadsheetApp.openById()... yada yada...
What I'd like to be able to do with your code is just to say ...
const sht = Import.SheetOne()
or
const vals = sht.vals('Some Range or Named Range') // which would return a 2dArray of a pre-declared range or named range
and so on...

So I wanted to ask if it'd be possible to have you write a very basic implementation of something like this so I could get a better understanding of how to ..

  1. Get a better grasp on working with your Import libraries,
    and
  2. See how you'd implement something like this when in comes to creating libraries with your Modules.

As far as using the ones you've already created, I love them!
Thank you so much for all your work!!
Cheers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment