lundi 31 décembre 2018

Design pattern for repeating a set of methods multiple times

Imagine we're writing a spreadsheet validation function. The user can enter multiple values in the spreadsheet, and there is a method that will verify if the values are correct. In addition to verifying if they're correct, there is also a "fix it for me" dialog that pops up and asks the user if they want to fix the problem automatically.

For example purposes, let's say we have the following fields:

  • Event url: The link to an event.
  • Event title: the name of the calendar event.
  • Invitees: a list of comma separated email addresses of users that should be invited to the event.

The user can then hit a "validate" button that will check the following:

  • That the Event title really matches the one in the URL. If it doesn't they are presented with an option to update the title.
  • That the invitees are all on the event. If they aren't, an option to invite the next one is presented to the user (this is only done once at a time).

What's a good programming design pattern to execute a set of functions over and over again?

function validateSpreadsheet() {
  validateEventTitle();
  validateInvitees();
}

Both validateEventTitle and validateInvitees should return one of 3 possible values:

  • Success
  • Retry (the user chose to use the "fix it for me" button.)
  • Error (the user didn't choose the "fix it" feature.)

If one of them returns Retry, the entire method validateSpreadsheet should be run (e.g. in case we decide to have the event title depend on the number of invitees).

I can think of several ways the function validateSpreadsheet could repeat its logic:

  • (A) While loop
  • (B) Recursion
  • (C) Array of functions

I can think of several ways the function validateEventTitle can report its status:

  • (1) it could return an enum with the 3 values (success, retry, error)
  • (2) it could raise an exception in the case of retry and/or error

I implemented pseudocode for solution C1 (see the end of the post), but C1 makes it hard to share code between the different methods. For example, if the meat of the code looked something like this:

function validateSpreadsheet() {
  var row = getRow();
  var title = getEventTitle(row);
  validateEventTitle(title, row);
  validateInvitees(row);
}

... that would be more difficult to get working with C1 since the methods are wrapped in functions. I realize there are ways to workaround this limitation.

I don't like solution B1, but for completeness sake, I included a version of it below too. I don't like that it uses the call stack for repetition. I also think the code is pretty messy with the double if checks. I realize I could create helper methods to make it a single if check for each method, but that's still pretty messy.

I implemented a working example of solution A2. This one seems to work well, but it heavily exploits exceptions in a way that would probably confuse a new programmer. The control flow is not easy to follow.

Is there already a design pattern to achieve something like this? I'd like to use that rather than reinventing the wheel.


Solution C1 (Pseudocode)

function solutionC1() {
  var functions = [ 
    method1, 
    method2 
  ];

  while (true) {
    var result = SUCCESS;

    for (var f in functions) {
      result = f();
      if (result === SUCCESS) {
        continue;  
      } else if (result === REPEAT) {
        break;
      } else {
        return result; // ERROR
      }
    }

    if (result === REPEAT) {
      continue;
    } else {
      return; // SUCCESS
    }
  }
}

Solution B1 (Pseudocode)

function solutionB1() {
  var result;

  result = method1();
  if (result === RETRY) {
    return solutionB1();
  } else if (isError(result)) {
    return result;
  }

  result = method2();
  if (result === RETRY) {
    return solutionB1();
  } else if (isError(result)) {
    return result;
  }
}

Solution A2 (Working with unit tests)

function solutionA2() {
  while (true) {
    try {
      // these two lines could be extracted into their own method to hide the looping mechanism
      method1();
      method2();
    } catch(error) {
      if (error == REPEAT) {
        continue;
      } else {
        return error;
      }
    }
    break;
  }
}

var REPEAT = "REPEAT";
var method1Exceptions = [];
var method2Exceptions = [];
var results = [];

function unitTests() {
  // no errors
  method1Exceptions = [];
  method2Exceptions = [];
  results = [];
  solutionA2();
  if (results.join(" ") !== "m1 m2") { throw "assertionFailure"; }
  
  // method1 error
  method1Exceptions = ["a"];
  method2Exceptions = ["b"];
  results = [];
  solutionA2();
  if (results.join(" ") !== "m1:a") { throw "assertionFailure"; }
  
  // method1 repeat with error
  method1Exceptions = [REPEAT, "a"];
  method2Exceptions = ["b"];
  results = [];
  solutionA2();
  if (results.join(" ") !== "m1:REPEAT m1:a") { throw "assertionFailure"; }

  // method1 multiple repeat
  method1Exceptions = [REPEAT, REPEAT, REPEAT, "a"];
  method2Exceptions = ["b"];
  results = [];
  solutionA2();
  if (results.join(" ") !== "m1:REPEAT m1:REPEAT m1:REPEAT m1:a") { throw "assertionFailure"; }
  
  // method1 multiple repeat, method2 repeat with errors
  method1Exceptions = [REPEAT, REPEAT, REPEAT];
  method2Exceptions = [REPEAT, REPEAT, "b"];
  results = [];
  solutionA2();
  if (results.join(" ") !== "m1:REPEAT m1:REPEAT m1:REPEAT m1 m2:REPEAT m1 m2:REPEAT m1 m2:b") { throw "assertionFailure"; }

  // method1 multiple repeat, method2 repeat with no errors
  method1Exceptions = [REPEAT, REPEAT, REPEAT];
  method2Exceptions = [REPEAT, REPEAT];
  results = [];
  solutionA2();
  if (results.join(" ") !== "m1:REPEAT m1:REPEAT m1:REPEAT m1 m2:REPEAT m1 m2:REPEAT m1 m2") { throw "assertionFailure"; }
  
   // [REPEAT, "Test"];
}

function method1() {
  // in reality, this method would do something useful, and return either success, retry, or an exception. To simulate that for unit testing, we use an array.
  var exception = method1Exceptions.shift();
  if (typeof exception !== "undefined") {
    results.push("m1:" + exception);
    throw exception;
  } else {
    results.push("m1");
  }
}

function method2() {
  // in reality, this method would do something useful, and return either success, retry, or an exception. To simulate that for unit testing, we use an array.
  var exception = method2Exceptions.shift();
  if (typeof exception !== "undefined") {
    results.push("m2:" + exception);
    throw exception;
  } else {
    results.push("m2");
  }
}

unitTests();

Aucun commentaire:

Enregistrer un commentaire