Welcome to the third part of this series of articles about how to write better code for the Notes/Domino platform. In part 1 I wrote about creating your forms in a way so you get an instant idea of how they work (what is hidden, computed-for-display fields, etc) and in part 2 the subject had moved to Lotusscript, more specifically variable names and comments.
As already mentioned in that last article (as well as in some of the comments), breaking out code into functions is a great way to make code easier to read as well as dramatically easier to maintain. That is what I will focus on in this article.
Functions
In Lotusscript, the functions are declared the same way as in Visual Basic:
Function YourFunctionName(argument As DataType) As ReturnDataType YourFunctionName = ReturnValue End Function
You can have any number of arguments, from none to many. However, it is often suggested to keep the number of arguments to a minimum. I try to not use more than three arguments, unless there is no way around it. If you need to send a lot of information into a function, use an array or a custom data type instead. Then it is easier to change the arguments later, without changing the signature of the function. I have occasionally seen that cause issues in nested script libraries. Also, with many arguments the function declaration will probably not fit on your screen, unless you wrap the line. I try to always keep code visible in the editor, as it is easy to miss things when you have to scroll sideways to view code.
So let’s take a look at one of the code samples in my last article, the one where we call names.nsf to get the email address for the current user. Let’s make this a function (like Kenneth Axi suggests in his comment), but we will make it a more generic function, returning the email address for any user present in address book, not just current user.
Here is the original code:
key = session.CommonUsername Set view = db.GetView("People") Set doc = view.GetDocumentByKey(key) emailaddress = doc.GetItemValue("InternetAddress")
Below is how I would write the function. We will talk more about error handling later, but you want to make sure to always check return values before you use them. The function takes one argument, the name of the user we want the email address for, and returns the email address, or blank if no user/address was found. The username must be in the same format as the names are being displayed in the view we doing the lookup in.
Function getEmailAddress(username As String) As String Dim personname As NotesName Dim nabdb As NotesDatabase Dim nabview As NotesView Dim persondoc As NotesDocument If username = "" Then MsgBox "Username is empty. Exiting.",,"No Value" Exit Function End If '*** Create NotesName object and get CommonName part. '*** We do this so username argument can be in any format. Set personname = New NotesName(username) '*** Use Domino Directory on main server Set nabdb = New NotesDatabase("Domino1/MyDomain","names.nsf") If nabdb Is Nothing Then MsgBox "Could not open names.nsf. Exiting.",,"Error" Exit Function End If Set nabview = nabdb.GetView("PeopleByFirstName") If nabview Is Nothing Then MsgBox "Could not find view 'PeopleByFirstName'. Exiting.",,"Error" Exit Function End If Set persondoc = nabview.GetDocumentByKey(personname.Common) If persondoc Is Nothing Then getEmailAddress = "" ' Did not find person, return blank value End If getEmailAddress = persondoc.GetItemValue("InternetAddress")(0) End Function
You may wonder why I am creating a NotesName object and not just using the name passed as argument directly? That will allow me to get the Common Name of the name, no matter in what format it is passed. You should always check the values of any arguments passed to your functions to make sure you can handle them and that they contain valid data. In this case I make sure the username is not empty, but for a string containing a date I would use the IsDate() function to test that the value is a valid date-time.
Now we can call the function like this:
MsgBox getEmailAddress("Karl-Henry Martinsson") MsgBox getEmailAddress("Karl-Henry Martinsson/MyDomain") MsgBox getEmailAddress("CN=Karl-Henry Martinsson/O=MyDomain")
Beautiful, isn’t it?
How to name functions
This brings us to the naming convention for functions. For functions, I use CamelCase, as that makes it much easier to see what the function does. I have to admit, I am not always consistent with get and set, I sometimes capitalize the first letter, sometimes not. Using CamelCase for functions and lowercase for variables makes it easier for me to read my code later, but you should do what you are comfortable with, and what works for you.
The actual name of the function should describe what it does, without being too long or too short. A well named function will by itself document what it does when you see it in your code later. The function we just created above could have been called getEmail() or getEmailAddressForUserFromCentralServer(). The first is a little too short — you don’t know if it is getting a mail address or an actual email document– while the second is too long.
Script Libraries
Consider creating script libraries where you store commonly used functions. I have a number of script libraries, containing different sets of functions, and when I need certain functionality in a new application, I just copy one or more of them into that application. I name the script library in a way so I know what functions are in each one of them:
There are many ways to name your script libraries, and I know some developers who use very complicated elaborate naming systems, where they can see at a glance if the script library uses back-end or front-end functions/classes, etc. I prefer a slightly simpler approach.
So let’s look at a real life code example, from one of my production applications. This is an agent, called from a browser, that generates some charts and other information, based on the URL parameters passed to the agent.
Thanks to the descriptive function names, hardly any comments are needed. The only comment in this code segment clarifies why we are doing what we do. Obviously the code is much more complex than what you see above, but it will give you an idea of how you can make your code easy to review and maintain.
In the next article I will talk about classes and object oriented Lotusscript.
So you’re suggesting that if you had a function whose signature was:
Function Snoot(xpos As Long, ypos As Long, etc, etc, etc) As Double
that it might be better to write it as:
Type Position
xpos As Long
ypos As Long
End Type
Function Snoot(pos As Position, etc, etc, etc) As Double
so that (let’s say) if you add a zpos later, you have just one place to do it (the type definition). And you say this is to avoid problems in nested script libraries.
I assume the problems you’re talking about are the type that can be solved by recompiling all LotusScript. But I don’t see how the above change would help with this. If type Position is defined in a different library than Snoot, changing the type definition requires a recompile just as much as changing the argument list. The difference is that with individual arguments, you’re also forced to manually update the calling code, every place the function is called. But here’s the thing. You _need_ to update the calling code anyway. With the individual arguments, you know it, because you get compile errors.
So if you add a “zpos” to Position, then everyplace we’re calling Snoot, there’s an extra question to be answered – what’s the value we want for zpos? If zpos is a separate argument, the compiler forces you to answer this question — so you have to think about how to update each call. But if you use a Position type as your argument, then some code like this:
Dim pos As position
pos.xpos = 54
pos.ypos = 23
tmp = snoot(pos, …)
still compiles, but the zpos value is zero. Was this the value you would’ve wanted to pass in this case? Maybe not; maybe you’ve just introduced a bug.
I think the problem here is that using a type or class to group arguments is a half-measure that just makes the problem worse. To me, long parameter lists are a sign that the code is not sufficiently object-oriented. The function, in other words, should be a method of a class which will have several of those parameters as members. I understand if this is more than you want to cover in one blog post, but I don’t think the recommendation you made is helpful by itself.
These days, when I write LotusScript code, my default assumption is that I’m writing classes. I only use individual functions when I have a library of functions that I would have liked to define as static class members of a utility class — which LotusScript syntax doesn’t support.
Then I learned something new. :-)
I was told in the past that the reason I had to recompile all my Lotusscript code (sometimes even multiple times) was because the signature changed. I would normally not declare my type definition in another script library, but in the same as I have my function that uses it.
In your example above, obviously I would have to modify the function Snoot() to handle the (possibly) missing/empty pos.zpos. I also need to check the values, but I don’t have to update every place I call the function, I just have to assign a value to the optional argument when I need it. This method gives me a way to mimic overloading, by having optional arguments in the type definition. A typical example would be a custom data type for person data:
Type PersonData
firstname As String
lastname As String
address As String
city As String
state As String
zip As String
End Type
Function CreatePersonDocument( pdata As PersonData)
I then later add a string containing email address to this type definition. In the function CreatePersonDocument() I have to check the value of pdata.emailaddress to see if it contains a value, If it is blank/empty I will not write the contents of that variable into the document.
OK, I know that I could still write the empty value into the document, and the end result would be the same. It’s just an example. Could been age As Integer instead, and if the value is 0 not writing it into the document.
Yes, you are correct, if one does not check the values passed to the function, you may introduce bugs.
I will cover classes very soon in this series of articles, I am writing pretty much only classes these days, except for quick one-time export functions and similar.