aspnetNOW
Program
Efficiently
Use
declarative programming to streamline your work.
By Jeffrey
Richter
Programming
is hard work and often can be quite tedious. I can't count the number of times
I've written a linked list (or some other algorithm) over and over again and
added just a few tweaks to the code here and there. Obviously I'm not alone
because many new technologies have appeared over the years to help developers.
I'm sure you're familiar with object-oriented programming as a technology that
allows one developer to design and implement an algorithm in a generic fashion;
then, other developers can tweak the generic algorithm for a specific situation
(by deriving from a base class and overriding virtual methods). No developer
questions the incredible productivity boost object-oriented programming
provides - it has proven to be a huge benefit, especially when you design and
implement big applications.
For
Windows programming, C# has made object-oriented programming mainstream because
it makes building Windows applications and components more efficient. C#
brings, however, another technology that, for some reason, doesn't enjoy as
much of the limelight as object-oriented programming. This other technology is
known as declarative programming, which is when you instruct your application
or component to do something using data rather than by writing source code.
(The act of writing source code is sometimes called imperative programming.)
Go
Mainstream
An
example of declarative programming is the production of an HTML Web page. In
this process, the "programmer" defines how the Internet browser application
should process the HTML tags and text to lay out the page in a window. Many
hard-core programmers don't consider HTML programming to be "real programming."
But I do, and I think this kind of declarative programming is going to become
much more mainstream - thanks to C#.
C#
offers many forms of declarative programming. First, there's metadata - data
produced by the C# compiler as it processes your source code and is embedded in
the resulting .exe or .dll file. This declarative data is used by several
facilities that are part of the .NET Framework and its Framework Class Library
(FCL). For example, the serialization formatters - BinaryFormatter and
SoapFormatter - classes can discover automatically the fields within an object
and fields that refer to other objects by examining the object type's metadata.
From this declarative information, the formatters easily can walk an entire
object graph spitting out a stream of binary bytes or a stream of SOAP.
Imagine
how you would have to implement serialization if metadata didn't exist. Without
metadata, Microsoft wouldn't have been able to create the BinaryFormatter or
SoapFormatter classes because there would be no way for these classes to obtain
enough information about an object graph to do anything useful with it.
Instead, if you needed serialization in your own application, you would have to
implement the serialization code manually and the implementation probably would
be specific to the types of objects your application needs to serialize.
Maintaining the code for this application as your application's types change
would be quite an undertaking.
In
addition, without metadata, the code to serialize the object graph would be
sprinkled through your application's types; there's no "serialization
algorithm," per se. The code to serialize one type is in one source-code file,
the code to serialize another type is in another source-code file, and so on.
This means similar code frequently is copied from one place to another and,
therefore, the "algorithm" has multiple maintenance points.
Infuse Your
Source-Code Files
Earlier
technologies introduced the notion of declarative programming. Many years ago,
an Interface Definition Language (IDL) was created to help you call a method
whose code was on another machine. This is referred to as a Remote Procedure
Call (RPC). You would create an IDL file containing the definitions of the
interfaces (functions) that could be called remotely. Then, a special compiler
such as Microsoft's MIDL.exe would parse the IDL file. This compiler would
produce the client stub code, the server stub code, and a header file the
client code could use to make the remote call.
Having
IDL files and the MIDL.exe compiler is fantastic for programmer efficiency and
productivity because IDL lets you describe how to call a function remotely, and
the IDL compiler is endowed with a generalized algorithm that knows how to spit
out the complex code for exposing and calling remotable procedures. So, IDL
files are a form of declarative programming.
Unfortunately,
this form of declarative programming has a major drawback: The declarative
information is separate and disjointed from the actual code implementing the
remotely callable function. For programmers, this causes all kinds of trouble.
For example, if you change the prototype of a function, you also must remember
to update the IDL file and rerun the IDL compiler so the stub code is in sync
with the most recent version of the function's prototype. I can't remember how
many times I was stung by this kind of "bug" and I'd like to forget how many
hours I wasted debugging these kinds of problems.
Fortunately,
the C# team came up with a way to add declarative programming directly inside
your source-code files. In other words, you can add custom declarative
information right in your source-code files. Then, when you compile your source
code, the C# compiler parses the declarative information and embeds it in the
resulting metadata (the other declarative information produced automatically by
the compiler itself). This produces several huge benefits for programmers.
First,
your intended declarative information is in the same file (and even in the same
location within the file) as the code to which the declarative information
applies. This means you are far less likely to forget to modify the declarative
information if you make a change to the source code itself. Second, there's
only one syntax to learn instead of learning C# syntax, IDL syntax, and so on.
Third, there's only one compiler to run, and it compiles both your source code
and the declarative information simultaneously. Finally, because the compiler
compiles both the source code and the declarative information at the same time,
it's not possible for the information to be out of sync and, because the
compiler emits all this information into the resulting .exe or .dll file's
metadata, the code that implements the types and methods always will be in sync
with the metadata and other declarative information. You'll no longer waste
countless hours debugging because the declarative information was out of sync
with the implementation, or because you forgot to recompile the declarative
information, or because you used an old version of the declarative information.
In C#, a
custom attribute identifies information you can apply declaratively to a piece
of source code. A custom attribute is simply a class (like any other class)
except it's derived from System.Attribute. And, generally, a custom attribute
class defines some constructors, some fields, and maybe some properties, but no
other methods or events. Figure 1 defines the DllAttributeClass.
Figure 1. This sample code listing
highlights a constructor and fields.
Basically,
think of a custom attribute class as a state holder. In this article, I don't
have the space to explain fully what a custom attribute class is or how to
define it, apply it, and look for its existence programmatically. So I'll
assume you're somewhat familiar with this process or know of some good
references. Naturally, I'd like to recommend my own book, Applied Microsoft
.NET Framework Programming (Microsoft Press); Chapter
16 focuses on custom attributes.
Define it
and Apply it
Once you
define a custom attribute class, you can apply an instance of it to a piece of
source code. In C#, you can apply custom attributes to an assembly, module,
type (class, struct, interface, or delegate), property, event, field, method,
method parameter, or a method's return value (see Figure 2).
using System;
[assembly:Xxx] // Applied to the assembly
[module:Xxx] // Applied to the module
[type:Xxx] // Applied to the type
class SomeType
{
[property:Xxx] // Applied to the property
public String SomeProp { get { return null;
} }
[event:Xxx] // Applied to the event
public event EventHandler SomeEvent;
[field:Xxx] // Applied to the field
public Int32 SomeField = 0;
[return:Xxx] // Applied to the return value
[method:Xxx] // Applied to the method
public Int32 SomeMethod(
[param:Xxx] // Applied to the parameter
Int32 SomeParam) { return SomeParam; }
}
Figure
2. This sample
shows where you can apply custom attributes.
The .NET
Framework Class Library (FCL) defines many custom attributes you can apply to
items in your own source code. Here are some examples:
- Applying
the DllImport attribute to a method informs the CLR that the implementation of
the method actually is located in unmanaged code contained in the specified
.dll
- Applying
the Serializable attribute to a type informs the serialization formatters that
instances of the type can be serialized to binary or SOAP
- Applying
the AssemblyVersion attribute to an assembly sets the version number of the
assembly
- Applying
the ParamsArray attribute to a parameter tells compilers that the method
accepts a variable number of arguments
- Applying
the WebMethod attribute to a public method tells ASP.NET that the method is an
XML Web Service method callable remotely over the Internet
The FCL
is filled with literally hundreds of custom attributes; the previous list is an
extremely small sampling that demonstrates the most popular uses.
Now that
you have a feel for declarative programming and some examples of how Microsoft
has defined some custom attributes, I'd like to share with you an idea of my
own.
In my
day-to-day work, I write many small utility programs, and almost all of them
require the end user to specify command-line arguments. This means all my
applications contain code to parse command-line attributes. One day I was
pondering how I could make it easier to parse command-line arguments, and it
dawned on me: declarative programming. I'll design my own CmdArgAttribute
custom attribute that'll make it trivially simple for me to add command-line
argument parsing to all my programs. This article's code download defines my
custom attribute, checks for its existence, and demonstrates how to use it (see
the Download box for details). You'll probably want to refer to the source code
while reading the remainder of this article.
Start it Up
Let me
explain how I (or you) would start using my custom attribute. First, define the
usage for my application. The code download is a simple utility that creates a
file or appends to an existing file, writing some text to the file several
times. The program isn't particularly interesting (or useful in real life), but
it does demonstrate how to use my custom attribute to parse command-line
arguments. If you run the program specifying the /? switch, this usage
information appears:
Usage: WriteTextToFile /P:Pathname /T:String
[/M:Create|Append] [/N:NumTimes]
/P The pathname of the file to be created or
appended to
/T The text to write to the file
/M:Create Create the file (if the file already exists,
it is erased). This is the
default.
/M:Append Append to an already existing file (if the
file doesn't exist it is created)
/N The number of times to write the string to
the file (1 if not specified)
/? Show this usage help
Normally,
writing the code to parse the command-line options would be tedious and
error-prone. But using my CmdArgAttribute makes it incredibly simple. Once the
usage for the application is defined, the second step is to define a data
structure that contains a field for each of the application's possible
command-line arguments. Then, apply my CmdArgAttribute to each of the fields
(see Figure 3).
enum WriteMode
{ Create, Append }
// The fields
of this class map to
// command-line
argument switches.
class Options :
ICmdArgs {
// Members identifying command-line
argument settings
[CmdArg(ArgName = "P", RequiredArg = true,
RequiredValue =
CmdArgRequiredValue.Yes)]
public String pathname = null;
[CmdArg(ArgName = "T",
RequiredArg = true,
RequiredValue =
CmdArgRequiredValue.Yes)]
public String text = null;
[CmdArg(ArgName = "M", RequiredArg = false,
RequiredValue =
CmdArgRequiredValue.Yes)]
// Default to creating a new file
public WriteMode mode = WriteMode.Create;
[CmdArg(ArgName = "N",
RequiredArg = false,
RequiredValue = CmdArgRequiredValue.Yes)]
// Default to writing the text to the files
just once
public Int32 numTimes = 1;
[CmdArg(ArgName="?")]
// Default to not show usage
public Boolean ShowUsage = false;
...
Figure
3. Here's the
abbreviated Options data structure, which shows how to apply the
CmdArgAttribute to various fields.
The
fields that have the CmdArgAttribute applied to them indicate fields that
correspond to command-line options. When you apply a CmdArgAttribute to a field
or property, you can specify three optional pieces of declarative information
(see Figure 4).
|
Optional Command Argument Information
|
Explanation
|
|
ArgName
(a String)
|
This
is how you indicate which command-line switch string maps to the field. For
example, the "/P" switch maps to the pathname field and the "/?" switch maps
to the ShowUsage field. The default for ArgName is the name of the field or
property itself.
|
|
RequiredArg
(a Boolean)
|
If
true, this indicates that the user must specify this switch, or else an
InvalidCmdArgException exception is thrown. For example, the user must
specify the "/P" and "/T" switches when running the program or the program
can't run at all. The default for RequiredArg is false.
|
|
RequiredValue
(an enum of Yes, No, Optional)
|
If
Yes, this indicates that the switch requires an additional value (such as the
"/P" and "/T" switches). If No (the default), it indicates that the switch
cannot have an additional value after it ("/?" is an example). Setting
RequiredValue to Optional indicates that this switch might or might not have
a value specified with it (the downloadable sample program doesn't
demonstrate this possibility).
|
Figure
4. Here are the
three optional pieces of declarative information you can specify for a
command-line argument.
Defining
this data structure is the hard part; parsing the user's command-line arguments
to set the data structure's fields is incredibly simple. Inside Main, simply do
something like this:
static void
Main(String[] args) {
Options options = new Options();
CmdArgParser.Parse(options, args);
// Now the rest of the program can execute
fine-tuning
// its behavior by querying options's
fields...
}
The
Parse method is a static method defined in another class I created named
CmdArgParser. When calling Parse, you must pass the object whose fields will be
set by the Parse method. Main also passes on the command-line arguments passed
to it (the args parameter) to the Parse method directly. Admittedly, this Main
is pretty simple. It'd be a bit more complicated to handle some exceptions that
could get thrown by the Parse method if the user enters bad command-line
arguments or if the way you've defined the Options data structure is defective.
The sample code shows a more robust Main that handles these scenarios better.
By the
way, notice that the Options class sets default values for some fields. If the
user doesn't specify a switch that corresponds to a field, the default value
will be unaltered. I've found this to be incredibly useful when building my own
utilities. Also, notice that the numTimes field is an Int32 and the mode field
is a WriteMode enumerated type. These are very cool features! If possible, my
parser converts the strings to the appropriate data type automatically when
parsing the command-line strings. It does this conversion by checking the
metadata to determine each field's type.
Hopefully,
after all this, you can see the benefits of declarative programming and start
to imagine ways you can take advantage of it yourself by defining your own
custom attributes and applying them as needed. Personally, I believe
declarative programming is a huge technology improvement for developers, and C#
makes leveraging it easy. Using declarative programming along with
object-oriented programming is certain to improve your efficiency as a
developer.
Check
out Chapter
16 of Applied Microsoft .NET Framework Programming, where you can
learn what a custom attribute class is, how to define it, apply it, and look
for its existence programmatically.
The sample code in this article is available for download.
Jeffrey
Richter is a
co-founder of Wintellect (http://Wintellect.com),
a training, debugging, and consulting firm dedicated to helping companies build
better software - faster. He's the author of Applied Microsoft .NET Framework
Programming (Microsoft Press, http://www.amazon.com/exec/obidos/ASIN/0735614229/
) and several Windows programming books, and he has been consulting with
Microsoft's .NET Framework team since October 1999. E-mail him at mailto:JeffreyR@Wintellect.com.