I'm frequently told by fellow Notes/Domino developers, "I don't write classes". When I ask why, on rare occasion the response is that OOP == over-engineering; procedural code is always sufficient to "get the job done". Far more frequently, however, I'm told, "I don't know how", "I don't 'get' classes", et cetera. The reason I mention this is to reassure you that, if you have also avoided classes because you don't understand what they are, how to write them, or even why they're useful... you're not alone.
One of the best explanations I've seen of OOP is the presentation Thomas Bahn gave at ILUG this year (I wasn't there, but he posted the slides and example database). It shows how real-world objects can be described in code via classes, but also makes the point that the real power of OOP is representing business processes and relationships using the same structure. The rest of this blog entry will be a paraphrase of some of that presentation, along with some of my own interpretation of what OOP is and its value to us as programmers.
As you might expect, OOP is programming that's centered around objects; classes are just templates for creating objects. So what's an object? In the "real world", we're surrounded by objects: a car, a house, a cat, a baseball. Common to all of these is that our interaction with them is principally defined by what we can know about them and what they are capable of doing ( or what we can do with or to them ). A baseball, for example, has a size, shape, weight, and current location in addition to everything else we can know about it; these are some of its "properties". We can throw, catch, or hit it; these are some of its "methods". An object in programming, then, is simply a container for properties and methods ( often described collectively as the object's "members" ): a single chunk of memory stores a reference to something that we can know certain things about and that can be instructed to do certain things.
As I mentioned before, a class is simply a template for creating an object. Just as one Domino database can serve as a template for other databases, defining what design elements each will contain, a class defines what properties and methods an object based on that class will contain. In LotusScript, the class definition for a baseball might look something like this:
Class Baseball
shape As String
size As Single
weight As Single
location As GPSLocation
Sub throw (direction As Integer, velocity As Double)
End Sub
Sub hit (direction As Integer, velocity As Double)
End Sub
End Class It's highly unlikely that you'll ever need to describe sporting equipment in your applications. As I alluded to earlier, however, this same structure can be used to define business processes and relationships. While it's never advisable to treat women as objects, in programming it's often useful to create objects that correspond to a specific individual. So let's define a generic class for creating objects that correspond to a person:
Public Class Person
Private userName As NotesName
Private birthDate As NotesDateTime
Private gender As String
Public Sub New (Byval p_name As String, Byval p_birthDate As String, Byval p_gender As String)
Set Me.userName = New NotesName(p_name)
Set Me.birthDate = New NotesDateTime(p_birthDate)
Let Me.gender = p_gender
End Sub
Public Function getAge () As Byte
Dim datNow As New NotesDateTime(Cstr(Now))
Let Me.getAge = Cbyte(Fix(datNow.TimeDifference(Me.birthDate) / 31557600))
End Function
Public Function getGender () As String
Let Me.getGender = Me.gender
End Function
End Class
In this example we've introduced a couple new concepts, one of which is the "constructor". In its capacity as a template for object creation, a class can also be thought of as a blueprint for objects. For an object to be of use, it must be constructed: we do this all the time with product object classes (Dim session As New NotesSession(), for example). This is an instruction to run the "New" Sub of the class definition - its constructor - and assign to the specified variable the constructed object. Every class, therefore, has a New Sub, even if none is defined. Defining one, however, allows us to specify code that should run when an object is constructed. This is often used to set initial properties for the object, typically by passing arguments (also referred to as parameters) to the constructor. In the Person example, it allows us to ask for the person's name, birthday and gender when the object corresponding to them is constructed and store all of that information for later inside a single object.
You may have also noticed something new in the constructor compared to the example methods in the Baseball class: namely, the keyword Byval. I've mentioned this keyword before, but its nature bears repeating. This forces an argument to be passed by value, not by reference (which is the default behavior). Put another way, it creates a temporary copy of the passed value that only exists for the life of the procedure. While this causes a minute increase in memory consumption, it adds a crucial layer of safety to your code. If you pass an argument by reference to any procedure, and that procedure makes any changes to its value, those changes remain after the procedure is complete. If your code is dependent upon an assumption that the value would remain the same, your code is now broken. Prepending each scalar argument (Strings, numbers, etc. - this is not an option with objects) with the Byval keyword is a promise to any calling procedure that the value passed to it will not be modified by it. This gives you the flexibility of modifying, if desired, the passed value inside the procedure with impunity: you're not breaking any other code that's expecting the value will be the same as it was before the procedure was called.
Another concept this example illustrates is visibility: each object member can be defined as "Public" or "Private". You've no doubt noticed the phrase "Option Public" appear automatically in certain places in your code, which instructs the compiler to treat every member as public unless otherwise specified; changing this to "Option Private" would produce the opposite result. For clarity's sake, it's best to always specify the visibility of each - though the compiler doesn't need it, your fellow humans may appreciate seeing it spelled out. But why bother making anything private to begin with? The primary reason is that it allows for greater control over how properties are read and written. It also makes it easier to break each method into smaller chunks: you can define one public method that, in turn, calls several private methods to delegate portions of its task to others to ensure that each procedure does only one thing, and does it well (making your program as a whole easier to understand and maintain). You could make all those other methods public as well, of course, but there's no need to, because they're only useful in the context of the larger task; indeed, calling them outside of that context might cause problems... keeping them private ensures the compiler will tell you something's wrong, instead of waiting for the users to inform you later. More on this concept in a moment.
Finally, the Person example also illustrates use of the "Me" keyword. Most (if not all) languages that have a capacity to create objects include a keyword for identifying the object within which the code is executing. In Java and JavaScript, for example, that keyword is "this". In LotusScript, it's "Me". So within any procedure in the example above, Me.userName would refer to the current person's name; Me.getAge would refer to the current person's getAge() method. In most cases, this is again useful for clarity: it clearly indicates that you're calling an internal method, for example, as opposed to a top-level procedure (a Sub or Function that exists in the same scope but outside of any class definition). But in cases where a naming conflict exists, it also protects your code from getting confused between the two. If, for example, your class contains a property with the same name as a global variable in a script library that you add later via a "Use" statement, "Me" ensures that your code knows it's referring to the object's property and not that new global.
Moving on: although calculating a person's age based on their birthday can be handy, so far this class wouldn't be terribly useful in most business applications. So let's create an Employee class. Every employee is a person (except, perhaps, for guide dogs), so we can spare ourselves duplicate effort by creating an inherited ( or "derived" ) class: Public Class Employee As Person
Private accountRecord As NotesDocument
Private manager As Employee
Public Sub New (Byval p_name As String, Byval p_birthDate As String, Byval p_gender As String, _
p_manager As Employee), Person(p_name, p_birthDate, p_gender)
Set Me.manager = p_manager
End Sub
Public Function getAccountRecord () As NotesDocument
If (Me.accountRecord Is Nothing) Then
Dim session As New NotesSession()
Dim dbNAB As NotesDatabase
Dim vwUsers As NotesView
Set dbNAB = session.GetDatabase( session.CurrentDatabase.Server, "names.nsf" )
Set vwUsers = dbNAB.GetView( "($Users)" )
Set Me.accountRecord = vwUsers.GetDocumentByKey(Me.userName.canonical)
End If
Set Me.getAccountRecord = Me.accountRecord
End Function
End Class
While this is still a very basic class, there are a couple things going on here that I want to highlight. First, note that we don't have to define the Person properties and methods again in this class. An Employee is a Person, so the compiler knows that every Employee will have a userName, a birthDate, and a gender; every Employee supports the getAge() and getGender() methods. But by adding in new properties and methods, we've extended the Person class to make it more useful, while still allowing us the flexibility of just constructing a Person object when that's all we need.
Next, the getAccountRecord() method demonstrates how it can be useful to store several things about an object as members of that object instead of as loose variables: instead of just automatically grabbing the employee's account record from the Domino Directory during the constructor, we wait to see if getAccountRecord() is ever called. When it is, we check to see if we've already got it stored. If not, then we go and get it, store it, and return a handle on it. If that method is ever called again, we can skip retrieval and simply return the stored document handle. This is sometimes called "lazy loading": we don't spend CPU cycles or network bandwidth until they'd buy us something we know we need.
Finally, take a close look at the constructor. My favorite interview question (on the rare occasions where I've been the interviewer and not the interviewee) has long been, "what is the one procedure in a LotusScript class that can be overloaded, how, and why?" OOP junkies often legitimately rag on LotusScript's inability to "overload" methods - in Java, you can define the same method multiple times as long as each definition has a different method "signature" (the number, type(s) and sequence of arguments the method accepts). This allows the method to run entirely differently based on what arguments are passed to it. In the case of an employee, for example, we could define several constructors: one that accepts the account record as the only argument, which allows us to determine their name, manager, and various other information; one that accepts just the name, which allows us to find the account record, et cetera. LotusScript doesn't let use do that - we can "override" methods (telling the derived class to run a method differently than it would in its parent class), but the method signature must match precisely. The only exception is the constructor, and there's still a catch: we can't overload it within the same class definition, but we can define a method signature in a derived class that differs from that of its parent constructor... but only if its signature is a superset of the arguments the parent constructor accepts. That was quite a mouthful, so I'll use the above example to try to explain what that means.
The Person class requires that we pass three Strings in a row: the person's name, age and gender. For an employee, however, we also want to know who their manager is, and we've decided we might as well collect that information up front. Unlike class methods, which run the "youngest" definition available (except in the case of event binding), when an object is constructed, every constructor in the object's genealogy is executed in chronological order. So if class A inherits from B, which inherits from C, constructing a new A causes the C constructor to run, then B, then A. In the case of our Employee class, the Person constructor is run before the Employee constructor. So the syntax of the Employee constructor is identifying which of the arguments passed to it map to which arguments for constructing a Person. In fact, we could have used completely different names for the argument variables or even accept them in a different order... as long as we then map each to the Person constructor. What's essentially occurring here is that a portion of the arguments passed to Employee are temporarily passed to the Person constructor, which constructs a Person object that is then passed to the Employee constructor. Don't worry if this particular concept feels like a bit of a mindjob... in my opinion, this is easily the most confusing aspect of OOP in Domino, so if you can eventually wrap your head around this concept, you're unlikely to encounter any syntax more bizarre than this.
Before I wrap this up, there's one more point I want to make about what makes this structure so useful. I mentioned earlier that I'd offer more detail on the advantage of distinguishing between public and private members. While many proponents of OOP prize the ability to protect object members by making them private (for example, nothing can change a certain property without going through a public method, which allows you control over what the property is changed to - and how), my favorite reason for making any member private is the ability to change the "internals" of an application later without having to rewrite the whole thing. Thomas refers to the public member set of any class as a "contract": this becomes the API via which all other objects interact with the object; any future changes to a class, therefore, may force all other code interacting with objects based upon that class to be updated in order to account for those changes. During a significant rewrite, there can be advantages to adding new public members that provide additional functionality, but the more members you leave private, the more you can improve how those members behave over time without impacting any other code. This makes your job easier, causes less confusion for your fellow programmers, and keeps users happier. This is a further extension of the Byval premise earlier: you're promising that what's going on behind the scenes at any layer of your application won't break whatever is interacting with it.
As an example of this premise, let's briefly revist the Person class described earlier. I believe it was Douglas Crockford (though he may have been quoting someone else) who said that "premature optimization is the root of all evil". In other words, a common trap we can fall into is spending too much time trying to make our code "perfect", when in reality there's no such thing. In this case, we're passing the person's gender as a String and storing whatever is passed as a private member. A good rule of thumb for any program is "flexible input, strict output". Suppose we only need to know the gender initial ( "M/F" ) in order for the object to behave correctly, but there's a portion of your application that's passing "male" and "female" to the constructor. On average, the private gender property now requires 10 bytes of memory ( 2 bytes per character - 8 for "male", 12 for "female" ). So let's modify that class slightly to make it more efficient: Public Class Person
Private userName As NotesName
Private birthDate As NotesDateTime
Private gender As Byte
Public Sub New (Byval p_name As String, Byval p_birthDate As String, Byval p_gender As String)
Set Me.userName = New NotesName(p_name)
Set Me.birthDate = New NotesDateTime(p_birthDate)
Let Me.gender = Cbyte(Asc(Ucase(Left(p_gender,1))))
End Sub
Public Function getAge () As Byte
Dim datNow As New NotesDateTime(Cstr(Now))
Let Me.getAge = Cbyte(Fix(datNow.TimeDifference(Me.birthDate) / 31557600))
End Function
Public Function getGender () As String
Let Me.getGender = Chr(Me.gender)
End Function
End Class The reason we're using the Byte datatype is because it not only requires (as its name implies) only one byte of memory, but its storage range (0 - 255) is perfect for storing a numeric representation of a single Ascii character. Conversion between Ascii and Byte is extremely rapid, so we're not slowing down our application by changing the internal storage. But we are using less memory: on average, 9 bytes less per object instance. Which brings us back to the Crockford quote: it would ridiculous to be this attentive to memory consumption in most of your applications, spending extra time coding just to save 9 bytes of RAM. But if you find out after your application has been released (or even after initial development has begun) that it needs to be massively scalable, tweaks like this can go a long way. If, for example, this class is used in a context where information about all 70,000 employees (remember, the Employee class extends Person, so by making Person more efficient, we've instantly improved Employee as well... without having to touch that code) needs to be loaded into memory, that single 9 byte improvement saves a total of 615 KB. If this is a web application that even 1% of those employees could be accessing simultaneously, a 9 byte savings per object instance adds up to over 420 MB. Meanwhile, any code constructing Employee objects doesn't have to change because it can still pass what it's always passed... the internal result is just stored more efficiently. It would only need to change if it's still expecting getGender() to return "male" but is now only receiving "M" instead.
Hopefully this novella has provided some useful insight. Let me know if you have any questions about these concepts or have suggestions regarding what concepts could be further explained or were overlooked entirely.
|
classy
|