The API Mindset (Part Three)

February 6th, 2012

In the last part of this series, I wanted to discuss what makes your API easy to call. In earlier posts, we’ve already covered things like naming of parameters and return values, variable scope and Command Query Separation. But there’s more you can do to make things easy for code calling your functions.

Dependency injection

First, let me stress that I’m probably not using this term (from the realm of software testability, AFAIK) correctly. What I mean is this: if a function does its job on an entity defined within that function (e.g. a record variable), that strongly limits what callers can do - they simply have no influence on the function’s subject. If that is the result of a conscious, justifiable, intelligent code design decision - that’s great. If it’s not, it may force your successors to copy your code instead of reusing it.

A typical (and common) example in C/SIDE is a function that performs a certain task on each record in a dataset (a poor man’s Visitor pattern?). If the record variable in question is defined as a local variable within that function (or worse - as a global variable of the object in which the function resides), it won’t be easy to let the function do its job on another dataset (than the one stored in the database table) - the most common example of which being a temporary record variable. A bit like this:

PROCEDURE LessFlexible()
VAR
  MyRecordVariable: Record;
BEGIN
  IF MyRecordVariable.FINDSET THEN
    REPEAT
      DoSomething(MyRecordVariable);
    UNTIL MyRecordVariable.NEXT = 0;
END;

vs.

PROCEDURE MoreFlexible(VAR MyRecordVariable : Record)
BEGIN
  IF MyRecordVariable.FINDSET THEN
    REPEAT
      DoSomething(MyRecordVariable);
    UNTIL MyRecordVariable.NEXT = 0;
END;

I guess it is a trade-off between flexility and effort required to call a function: the LessFlexible variant is easier to call, because the calling code does not require a record variable to pass as a parameter.

Complex parameters vs. simple parameters

Another related dilemma is the following: if my function does its job based on a number of field values in, say, a record variable, do I design the interface so that I need to pass the entire record variable to my function, or in such a way that I pass it only the field values it needs in its current implementation. Something like this:

VAR MyRecordVariable: record;

BEGIN
  […]
  SimpleValues(
    MyRecordVariable.MyFirstField,
    MyRecordVariable.MySecondField,
    MyRecordVariable.MyThirdField);
  […]
END;

PROCEDURE SimpleValues(MyParameter1: decimal, MyParameter2: decimal, MyParameter3: decimal) : decimal
BEGIN
  EXIT(
    MyParameter1 +
    MyParameter2 +
    MyParameter3); // e.g.
END;

vs.

VAR MyRecordVariable: record;

BEGIN
  […]
  EntireRecord(MyRecordVariable);
  […]
END;

PROCEDURE EntireRecord(MyRecordParameter: record)
BEGIN
  EXIT(
    MyRecordParameter.MyFirstField +
    MyRecordParameter.MySecondField +
    MyRecordParameter.MyThirdField); // e.g.
END;

The first option feels nicely decoupled, i.e. function SimpleValues doesn’t care at all where it’s input comes from, as long as it has the right datatypes. Function EntireRecord, however, assumes that its input takes the form of a record variable. If the calling code doesn’t have a record with the desired field values available, it’s going to have to prepare one in order to call EntireRecord.

On the other hand, EntireRecord is less likely to be impacted by changed requirements. If future versions of the algorithm involve field MyFourthField, EntireRecord already has access to the field, without changing the function’s parameter signature. SimpleValues would need an extra parameter, thus breaking any code that depends on it.

How do you normally cope with this devilish ;-) dilemma?

7 Responses to “The API Mindset (Part Three)”

  1. jhoek Says:

    Thinking about it some more, I realized that my recent tendency towards record parameters (instead of simple values) may have something to do with an increased experience in C# programming. For a programmer in an object oriented language, it feels quite natural to pass an entire object (i.e. state plus behaviour) as a function parameter.

  2. kriki Says:

    I also,most of the time, pass the record variable for some reasons:
    It generally costs less typing to call the function.
    If I all the functions a lot of times, I put it as call-by-reference (toggle var=true). This way, I can speed up the calling process a little. I have to admit that saving a few microseconds won’t do a lot if I have to wait for milliseconds before the database+network gives me the records. But still….

  3. jhoek Says:

    As you said, I’m not sure if the performance gain will be noticable. The downside of passing the record parameter by reference is that it doesn’t enforce Command Query Separation: even if your function is supposed to return a value (i.e. “answer something”), it might also change some field values (i.e. “do something”). Passing simpler data types as parameters, or passing the record variable by value is safer in that sense, I believe.

  4. kriki Says:

    Generally it will not be noticeable (or you have to call the function around 1000 times for each record and it this case I would advice to reprogram it).
    It is true that it is save NOT to use by reference calls, but it is still better to throw in the whole record and let the function do what it needs to be done.
    My idea of functions is that you give something to them and then they handle it for themselves (like when you hit the send-button of your email-program=>you don’t care what it is doing. You expect that the email is send and will arrive).
    And also: if the function is changed, and needs a new value for it, there is a good change it is already available in the record.

  5. jhoek Says:

    Yes, I thinks we agree: the function should be a black box (~fire-and-forget), but it also shouldn’t have any side effects.

    Thanks again for replying!

  6. Sebastiaan Lubbers Says:

    >> Complex vs Simple

    Depends if your function is bound to a certain table or multiple…

    Because your Rec variable is bound to a table you cannot call the same function with other table…

    EntireRecord(CustomerRec) and
    EntireRecord(VendorRec)

    vs

    EntireRecord(Customer.No,Customer.Name) and
    EntireRecord(Vendor.No,Vendor.Name)

  7. jhoek Says:

    Agreed. That’s what I meant by “decoupled”.

    Maybe a hybrid solution could work: two functions with different records as parameters, that both call a (non-global) function that accepts simple values as parameters. :-)

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