The API Mindset (Part Two)

January 31st, 2012

In the first part of this post, I explained why I believe the features of a C/SIDE application should be written with (what I call) the API mindset. Good APIs are designed to be consistent, intuitive and easy to call - making your successors more productive by allowing them to build on the solid foundation that you created. The guidelines below come from my own mental toolbox; some of them are inspired by somebody else’s wisdom (e.g. Robert C. Martin’s book Clean Code - warmly recommended!); others stem from my own hard-earned experience.

Consistent

In the NAV world, consumers of your code will usually start with virtually no knowledge about its inner workings. What they need to know (e.g. which function to call when), they will learn by looking at your function names, the way your functions are grouped, the types of values they accept as parameters, etc. That’s why consistency is key. If your function names follow a certain pattern, any interruption will stand out like a sore thumb. That’s perfectly OK if a function does not belong the “family” of functions preceding it. In all other cases, I would probably rename the function to make its name match its siblings’ names.

If several functions operate on the same set of parameters, be sure to keep the names, types and order of your parameters the same for each function. It will help flatten the learning curve for consumers of your code. Well-chosen parameter names (remember the GetFileName example from Part One?) are also useful in this area. The Record.COPY C/AL function springs to mind: I can never remember whether to pass the source or target record variable as a parameter. Parameter names to the rescue:

Record.COPY(FromRecord [, ShareTable])

Here’s another convention that I tend to follow. Sometimes you want your function library to “silently” report error conditions to you (using the return value), other times it makes more sense to let the library code raise a run-time error. Compare the following example from the .NET world: the Int32 structure in .NET defines two (overloaded) static methods for parsing a string to an Int32:

int Parse(string s) parses string s to an integer. In case of failure, it throws an exception - the equivalent of a C/SIDE run-time error.

bool TryParse(string s, out int result) parses string s to an integer. In case of success, the out parameter result contains the resulting integer value, and the function returns true. In case of failure, the function returns false, but doesn’t throw an exception.

The equivalent C/AL functions may look like this pseudo code, using the same Try prefix:

PROCEDURE Parse(s: string) result : integer
BEGIN
  IF NOT TryParse(s, result) THEN
    ERROR({ErrorMessageGoesHere});
END;

PROCEDURE TryParse(s: string; VAR result: integer) : boolean
BEGIN
  IF {CouldNotParseTheString} THEN
    EXIT(FALSE);

  result := {ParsedString};
  EXIT(TRUE);
END;

Now your callers can decide whether they want to handle the error condition silently (TryParse) or let you raise the error (Parse). And no, VAR parameters normally do not contribute much to an intuitive piece of code. ;)

Intuitive

As you probably know, I’m a big fan of limiting the scope of variables - in other words, to use locals whenever possible. That way, it is instantly clear what values a function may operate on: the ones passed in as parameters. There can be no side-effects on other variables, since the called function’s code cannot even “see” them.

To further secure and simplify the matter, Robert C. Martin suggests the principle of Command Query Separation: a function should either do something, or answer something, but not both. If a function’s purpose is to retrieve information, it shouldn’t change a few variables in the process. Although I agree with the general principle here, I can think of a few functions in the NoSeriesMgt codeunit that violate it, and still make perfect sense. Retrieving the next available number in a number series will, by its very definition, both answer something (”what is the next available number in this series”) and change something (i.c. the No. Series Line record). This kind of “retrieve and increment” style functions may be the exception to the highly recommended rule.

Apologies for the (unintended) cliff-hanger, but I’m just going to hit Publish and hope for your feedback on the above. A third part of this post will (finally) be dedicated to the “dependency injection” dilemma that I mentioned earlier. Pinky promise!

3 Responses to “The API Mindset (Part Two)”

  1. kriki Says:

    Basically: a function should ONLY communicate with the rest of the program by using parameters.

    A function should also be a ‘black box’. The first part of the function should be comment describing what the function does and how to use the parameters. Standard NAV does not do that and partly I agree because it is (or should be) the same for all versions. But add-on or customizations should contain comment because it is not standard anymore and someone else might need to understand/use it.

  2. jhoek Says:

    Thanks for your reply! Wouldn’t documentation for the base application (w1) make sense, as it forms the largest part of the library that we base our work on?

  3. kriki Says:

    True. I also would prefer more documentation somewhere. But I can see why they don’t do it.

Leave a Reply

*
To prove you're a person (not a spam script), type the security text shown in the picture. Click here to regenerate some new text.
Click to hear an audio file of the anti-spam word