The API Mindset (Part One)
January 17th, 2012
Even if that new feature you are working on doesn’t touch the sales order functionality, or doesn’t change any posting routines, it probably has some dependencies on application development work that was done earlier - whether by the application developers at Microsoft, your co-workers, or perhaps even yourself. Pretty much all development in C/SIDE has existing application objects as its basis - if you need something like number series, it’s quite uncommon not to use what’s already there in the base application. If the Excel Buffer meets your needs, it would be silly to write something similar yourself. In my view, that implies that all C/AL code should be written as an API, an application programming interface; i.e. with future consumers of the interface you provide in mind. A number of requirements instantly follow from this assumption - let’s see how they apply to C/SIDE.
Well-defined, well-documented
A good requirement to start with, since it appears to be so poorly executed in the base application. Take codeunit NoSeriesManagement as an example. It’s something that (for reasons of consistency) we’re all supposed to use when dreaming up new master and document entity types. Nonetheless, looking at the (non-local) functions in codeunit 396, it is not instantly clear what each one does, despite their consistently applied naming. I’m pretty sure the same is true for certain methods in the .NET framework, but in that case, there’s plenty of formal documentation and examples on the MSDN site. A tough problem to resolve, and likely to remain that way for a long time to come. Perhaps a move to C# could facilitate and standardize the way we generate interface documentation? ![]()
Carefully selecting the right names for your artefacts will provide at least some documentation for consumers of your API. The necessity of descriptive function names has been the subject of some earlier post; today, let’s focus on return values. Compare the following signatures for a function that retrieves the file name (e.g. “bar.txt”) from a file path (e.g. “c:\foo\baz\bar.txt”).
PROCEDURE GetFileName(Value: Text[260]): Text[260] PROCEDURE GetFileName(FullPath: Text[260]): FileName : Text[260]
If you had asked me a few years ago, I would have strongly advised against named return values unless strictly necessary. These days, however, I always consider naming my return values (even if they are never explicitly assigned to), just to provide users of my code with that extra bit of information about exactly what is being returned; even more so if the returned value isn’t as instantly clear from the function’s name as it is in the example above.
Minimal, hiding implementation
At the risk of being accused of riding my hobby horse (again!): I think it is essential to minimize your interface. Only functions that you explicitly expose as your API should be visible to the outside world. Anything else (and that typically consists of implementation details) should have its Local property set to Yes. Not only does this make the interface smaller, and therefore easier to understand, it also allows you to change the implementation later without affecting the callers of your code. If you “publish” a method (i.e. leaving Local set to No), don’t blame your successors for calling it, effectively blocking you from changing something without breaking backward compatibility.
And yes, I agree, the designers of the C/SIDE platform probably should have made Local=Yes the default state, much like in C#, where class members without a visibility specifier are considered private. Now that they haven’t done so, there’s no way back: e.g. the text format in which objects can be exported is structured in such a way that absent information is assumed to have it’s default value. Interpretation of the absence of the keyword LOCAL cannot be changed without breaking backward compatibility.
Generic helper functions are an interesting exception, it seems. Suppose you’re writing a codeunit to implement some high-level functionality. Some of your functions may act as helpers to other functions (functions for string manipulation, file name manipulation etc. spring to mind) within your codeunit. In the context of your codeunit, they are implementation details, but unlike many others, they could prove useful outside the scope of your codeunit. Leaving them as global (Local=No) functions in your codeunit, however, makes them part of the interface of your feature. Customers that didn’t purchase the feature’s granule will not have access. Ergo: resist the temptation to make these helper functions global. If they are as generic as you think they are, move them into a separate codeunit in the base granule of your application.
To be continued. In the next part, I hope to cover dependency injection and loose coupling (probably neither in the strictest sense of the word).
January 18th, 2012 at 10:44 am
Thank you very much for this article - this cannot be stressed enough!
Hopefully its content will be really applied. Regarding myself, I realize that often enough I forget to give a good example due to the poor standard code my eyes got used to.