Reputation: 91861
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:
The user can then hit a "validate" button that will check the following:
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:
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:
I can think of several ways the function validateEventTitle
can report its status:
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.
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
}
}
}
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;
}
}
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();
Upvotes: 0
Views: 499
Reputation: 371009
For concise, clean code, I'd suggest having the functions that result in errors actually throw the errors, if they don't do so already. This allows for any errors thrown to immediately percolate up to the top to a containing try
block:
const fns = [
method1,
method2
];
// If the methods return errors but don't throw them, pipe them through isError first:
const fnsThatThrow = fns.map(fn => () => {
const result = fn();
if (isError(result)) {
throw new Error(result);
}
return result;
});
Then, all you have to do is check whether either function call results in REPEAT
(in which case, recursively call validateSpreadsheet
), which can be achieved with Array.prototype.some
:
function validateSpreadsheet() {
if (fnsThatThrow.some(fn => fn() === REPEAT)) {
return validateSpreadsheet();
}
}
try {
validateSpreadsheet();
} catch(e) {
// handle errors
}
Upvotes: 1