|
Tuesday, April 22, 2003
The .NET Abstraction Pile
An abstraction is a boundary with two sides. On the top side, the
abstraction presents a simplified view. Below, there is
something more complex and more real. The purpose of the abstraction is to
obscure what is really going on.
The world hidden underneath an abstraction is quite likely to be yet another
abstraction. In fact, it is typical to have many abstractions stacked
together, each one attempting to present an illusion which is even further from
the truth. If you stack them up vertically, the ones at the bottom are
more real than the ones at the top.
This is what programmers do. We build piles of abstractions. We
design our own abstractions and then pile them up on top of layers we got from
somebody else. Abstractions can be great. We use them because they
save us a lot of time. But abstractions can also cause lots of
problems. They're never perfect, as Joel Spolsky explains in his excellent
article on "The Law of
Leaky Abstractions".
But you can't have the benefits of an abstraction without its
risks. We need to make wise decisions about our piles of
abstractions. I'll start by offering three rules to keep in
mind:
Abstractions contain
bugs.
Using somebody else's code can save a lot of time. For example,
a special GUI component allows you to program at a higher level of
abstraction. Why write your own?
You have to remember that you are accepting a tradeoff. By using
somebody else's code you are inheriting somebody else's bugs. Often you
are accepting risks that are not under your control.
Abstractions reduce
performance.
Writing in Java is faster than
writing in C. But C code runs much faster than Java code. It's a
tradeoff that you cannot avoid. All you can do is make the right
choice.
Abstractions increase overall
complexity.
The goal of each abstraction is to
decrease complexity by presenting a simplified view of something else.
However, by the time you pile them all up, you've got a lot of complexity
which you may have to deal with. In fact, the more layers of abstraction
you have, the more complexity you've got involved.
A Really Tall
Pile
Let's work through an example. Suppose that I am working from my home
and I am ready to checkin some really important code changes to a source code
file. I'm using the SourceGear Vault client to connect to our server back
at SourceGear's main office. Below is a list of [almost] all the
abstractions which are in play.
- Vault.
The version control system itself is an abstraction. It presents our
users with concepts like Check Out, Check In, Label, Branch, Pin and
Share. This layer is obviously very important, since it's the only one
we can charge money for. :-)
Control
Flow:
- C#.
Vault is written entirely in C#, which is a very nice abstraction
indeed. From C# we get classes, objects, methods, strings,
looping constructs, logical operators, and the ability to attach names to
things. Cool.
- CLR.
C# runs on the Common Language Runtime, which is a huge
abstraction. In fact, if we all weren't so worried about comparing .NET
to Java we would be calling the CLR a "virtual machine", which it
is.
- C++.
The CLR is written in lower level languages like C++ and C.
- Assembly.
C++ is implemented by compiling it to x86 assembler code. We've taken a
big, big jump here. Compared to C++, assembly language doesn't feel very
abstract at all.
- Microcode.
Did you think Assembly was the lowest level of programming? Certainly
not. Each x86 assembler instruction is a little program written in an
even lower level language called microcode.
- Logical
gates. Microcode is implemented by circuits which provide
logical gates, including NOT, AND, OR, and NAND.
- Transistors. Logical
gates are implemented by transistors, an electronic component with three wires
sticking out of it.
Memory:
- ArrayList.
The .NET framework gives us "collections" we can use to manage memory in
aggregated ways.
- Objects.
From the realm of OOP we get "objects", self-contained pieces of data which
are bound to the operations which can be performed on that data. Very
handy.
- GC.
This is a big one. Because the .NET Common Language Runtime has a
garbage collector, we can create objects and know that they will automatically
be destroyed later when we are done with them.
- Handles.
In reality, memory has to be explicitly requested and released from
the operating system. Each chunk of memory is identified by a
handle.
- Virtual
Memory. This layer gives us another important illusion:
There is more memory available than we actually have.
- RAM.
Random Access Memory is itself an abstraction. Transistors don't
really remember anything. Furthermore, the notion of a bit doesn't
really exist. We simply assign conventions. When a
wire is at 5V, we call it a one. When it has no voltage on it, we call
it a zero. Collect a few hundred million of these in one place and
you've got a DIMM. (Actually, it's 3.3V nowadays,
right?)
The Check In
Button:
- Button.
The Check In dialog has a button on it. When the user presses this
button, the Check In operation will commence. But the button itself is
an abstraction. It is designed to simulate the concept of a physical
button like you might find on your microwave or TV. No such button
really exists. Windows Forms provides this abstraction.
- HWND.
Windows Forms is a layer of abstraction which is built on the Win32 API
underneath. The button is actually a window with its own WndProc.
.NET tries to hide this world, but it's definitely still there. One of
the glaring "leaks" in the Windows Forms abstraction is the absence of the
Win32 ScrollWindow() call.
- GDI.
The button is actually drawn using graphics primitives from GDI. It
doesn't just magically appear. It needs to be drawn using things like
DrawRect, fonts and colors.
- Pixels.
GDI contains primitives like DrawLine, but these are implemented in terms of
pixels. Graphics primitives are actually not quite so primitive.
If you think line drawing is easy, look up Bresenham.
- Video
Card. The pixels are actually an abstraction presented by
a video card.
- Monitor.
The monitor presents the illusion that all those pixels are organized into
pictures and images.
- Light.
I stop whenever I get to Physics or Chemistry. For my purposes, light is
real, not an abstraction.
Architecture of
the Vault Client:
- VaultClientPresentationLib.
We wrote this layer as part of Vault. It contains all the windows and
dialog boxes necessary to create the Vault GUI client.
- VaultClientOperationsLib.
This layer is a big part of Vault. It contains basic non-GUI primitives
which are necessary to write a Vault client. Create an instance of the
ClientInstance class. The methods on this class will communicate with
the Vault server and simultaneously keep your local working folder updated.
- VaultClientNetLib.
The previous layer actually calls VaultClientNetLib to communicate with the
Vault server. This layer is fairly thin. It is mostly a wrapper
around the Proxy Class.
- Proxy
Class. This important layer is generated by Visual Studio
.NET. It presents the illusion that the XML Web Service on the
Vault server is actually a C# class.
- SOAP.
When a call is made through the proxy class, the parameters for that call are
bundled up in SOAP
format. This format presents the concept of a method invocation message.
- XML.
SOAP is built on XML, a syntax framework for representing data.
- HTTP.
The SOAP message is transported to the server over HTTP, the
networking protocol on which the Web is built.
- DNS.
The Vault user types the name of the server, but that name isn't
really useful. It has to be converted to an IP address before real
network communication can take place. The Domain Name System is used to
look behind the abstract name and get the actual machine address.
- SSL.
The Secure Sockets Layer offers the illusion that communication over the
Internet can be private. This layer tries to look just like a regular
socket, except all of the data is encrypted as it passes through to/from the
socket itself.
- Sockets.
This layer is a great abstraction. Sockets present us with the illusion
of connections and the ability to send and receive data between endpoints.
- TCP.
The basic illusion of TCP is the idea that packets of data will arrive and in
fact, will arrive in the order they were sent.
- IP.
TCP is built on IP, which is even lower level network protocol. At this
layer, packets may or may not actually arrive, and they may arrive in a
different order than how they were sent.
- Ethernet.
The IP packets are carried on a cat5 wire sticking out the back of my
computer.
- Radio.
The Internet connection at my home is a wireless antenna pointed at the top of
a grain elevator eight miles away. So right now, the important code
change I am trying to checkin is a bunch of radio signals which represent
packets that may or may not arrive, but they are flying through the air, 25
feet above a corn field.
Architecture of
the Vault Server:
- VaultService.asmx.
The Vault server is an XML Web Service. This allows us to think of our
server as a collection of methods which will invoked in an "RPC-like"
fashion.
- ASP.NET. The
illusion of XML Web Services is actually provided by ASP.NET.
- VaultServiceSQL.
This library provides a wrapper which insulates the rest of the server from
having to know anything about SQL.
- Stored
Procs. This layer is a collection of stored procedures running
inside SQL Server.
- SQL.
The SQL language is an enormous abstraction. It presents concepts like
tables, rows and indices, as well as atomic transactions.
- IO
calls. Somewhere deep inside SQL Server 2000 is the place where
data is actually written to the disk file. They probably call the native
Win32 IO calls.
- NTFS.
The filesystem is a very important abstraction. It presents the concept
of files and folders, as well as permissions and attributes.
- Partitions.
The filesystem exists on a "partition", which is a portion of the space on a
hard disk.
- RAID
array. The RAID controller presents the illusion of one hard
disk when it is actually several.
- Hard
disk. In practical terms, this was the goal of the checkin all
along. My bits are finally stored in my hard disk. But the disk
itself is actually an abstraction...
- Platters.
The term "hard disk" sounds singular, but hard disks today usually have
several platters inside. These platters are the magnetic media where the
data actually resides.
So there you have it -- 46 layers of abstraction which are all involved when
I try to checkin my code. That means there are 46 layers in which
something might go wrong.
Actually the truth is that several of these abstractions are almost
perfect. For example, I've actually never had to worry about the layer
between Assembly and Microcode. As far
as I am concerned, Assembly is an abstraction that always Just
Works.
But it would be terribly wrong to ignore all those layers. Yes,
SourceGear's implementation of Vault required us to only write the code for a
few of the layers above. However, when it's time for QA and Tech Support,
all 46 layers are fully in play. Stuff Happens. When a customer has
a problem with Vault, the actual problem could be almost anywhere. We have
to figure out what's gone wrong, even if it's in one of the layers we didn't
create. Ask our tech support team how often layer 29 causes trouble. :-)
How to Kill Your
Project
When you build software, you're going to end up making a lot of decisions
about abstractions:
- Which abstractions do you want to build on?
- Where will you get the implementations of those abstractions (platforms,
libraries, components)?
- How trustworthy are those implementations?
You have lots of alternatives. For example, you can often make a
tradeoff by choosing to work at a lower level of abstraction. By doing so,
your development process will move more slowly, but more of the risks will be
under your control. For example, if I had a really small magnet and really
fine motor control skills, I could skip layers 1 through 45, drive to my office
and modify those platters myself. :-)
The stakes are higher than you might think. You can kill your project
by making the wrong decisions about abstractions. Do you remember the word
processor called WriteNow? This product was my favorite word processor
back when I was a Macintosh fanatic. WriteNow was really fast and had
just the right mix of features.
Today, WriteNow is dead because
somebody got burned by the decisions they made regarding abstractions. You
see, WriteNow was really fast because it was written in 68000 assembly
language. When Apple moved the Macintosh product line to the PowerPC,
WriteNow had nowhere to go.
These choices are hard, and learning from your mistakes is an excellent (but
painful) way to learn. But over the years, I've gathered the following
guidelines which help me make abstraction-related decisions:
Consider your context.
Developing a server
operating system is different from developing an web-based HR application so
employees can check their vacation days. There is no formula which works
well for all kinds of projects. You need to understand what kinds of
risks are appropriate for the kind of software you are trying to
build.
As a general rule, developers of
internal corporate applications tend to use more third party components than
ISVs. If you're writing code for the IT department of a company whose
primary business is not software, then your salary is an expense, not an
investment. Your employer wants you to get the app done FAST, because it
costs less to get it done that way. Corporate IT developers want every
decent abstraction they can get.
ISVs like SourceGear face a different
set of problems. If a third-party component brings even a
minor loss of quality to the app, it can severely affect our sales as
prospective customers look at our competitors. But that same competition
is tugging you in the other direction, reminding you that time-to-market can
be critical. Using third-party apps may be the only way to get your
product to market within the window of opportunity, but the risks need to be
studied closely.
The size of your company should affect
your choices as well. Nothing is more frustrating than being unable to
ship a product because of a bug that you can't fix because it's in a
third-party library. For a very small company, the financial damage of a
situation like this can be severe. These are the times when you wish you
had chosen to put more of your risks inside your own circle of
control.
Place your trust carefully.
I recommend approaching third
party code with a great deal of suspicion. Never assume that an unknown
component or platform will Just Work. A little paranoia will probably
pay off later.
When picking the pieces of your platform, as a general rule, "older is
better". You can walk with less worry on a path which has been well
trodden by many people for years. As an extremely
obvious example, C is old and mature enough to be a platform which will
yield very few surprises.
Evaluating newer technologies is harder. Try to figure out who else
is using the abstraction successfully. Grab the technology and take it
for a test drive. In the end, you may not get enough evidence to lead to
a completely confident decision. If you really need the convenience
offered by the abstraction, you may have to jump out with a little
faith.
Learn to see through the
abstractions.
The most important
point in this whole article is this: You need to understand what's going
on inside all your abstraction layers. Each abstraction presents an
illusion, but the best decisions happen when you can see through the
illusion.
If you have a deep understanding of all
the technology abstractions that are involved with your software, then you
have two big advantages:
You
can quickly isolate problems.
You
can develop an intuition which will help you avoid those problems in the
first place.
The first point is fairly
obvious. Troubleshooting goes much better when you know what's going
on. Have you ever watched someone try to solve a problem in the presence
of several abstractions they didn't understand? They feel
helpless. Usually, they start making wild guesses about where the
problem could be. I call this "stab in the dark debugging". :-)
More importantly, if you can see
through most abstractions then you can develop an intuition to make much
better technology decisions. Choosing the right libraries and components
in your platform can prevent lots of problems before they ever
happen.
Don't assume that this kind
of deep technical knowledge becomes less valuable as you climb the management
ladder. Understanding this stuff can be a huge advantage in many kinds
of decisions, right up to the most executive levels. I believe the
technical prowess of Bill Gates was a major reason why Microsoft beat every
competitor in the eighties and nineties, even though Bill probably wrote no
code, no specs, and no design documents.
Failures and Successes
It wouldn't be fair to only mention the mistakes of others when I've made so
many excellent and instructive mistakes of my own. :-)
My most recent blunders in this area happened when we built SourceOffSite Collab on a pile of
abstractions which was way too short.
- We built our own implementation of the "server pages" concept because ASP
didn't meet our requirements perfectly. Collab includes its own
web server which processes pages we call "giglets". In between the <%
and %> we process JavaScript using the Mozilla engine which has
been modified with special Collab-specific hooks. In retrospect, we
should have found a way to work around the limitations of Microsoft's standard
dynamic page generation technology.
- We also implemented a complete system for XML-based procedure calls.
We rationalized this one because XML-RPC and SOAP just weren't quite
perfect for our needs. Hindsight now brings us to the same
conclusion as above -- changing our requirements to fit the established
platforms would have been the wiser choice.
Better decisions would have gotten SOS Collab to market sooner and we would
have fewer code maintenance problems now.
Not all of our decisions went badly. We made a great choice when we
decided to build Vault using .NET.
From the beginning, I hoped that .NET was "Java done right". I've used
Java extensively, and I loved the productivity gains we got during the beginning
and middle of the development cycle. But things got ugly at the end.
All those layers of abstraction started contributing to our bug list. I've
been involved in a couple of projects which completely failed because Java was
chosen. (Yes, this is merely my opinion, and yes, there were other
factors in the failure of those projects.)
Given our bad experiences with Java, our decision to use .NET took a fair
amount of courage. Early experiments looked promising, but we knew that we
would have to wait for the endgame to really know if .NET could really be
trusted all the way through.
As I write this, SourceGear Vault
1.0 has been shipping for over two months. We have no regrets.
For an abstraction pile as large as the one described above, it's remarkable
that this product works at all. :-)
But the fact that it works well is nothing short of amazing.
We have test applications which continuously try to abuse Vault in ways that are
abusive and profane. If something goes wrong in any layer of
abstraction, the whole test will come to a halt. But we can let these
tests run for days at a time without any problems whatsoever.
This success stands as a testimony to how incredible .NET really is. We
built a reasonably full-featured version control system in 14 months, and it
works. Sure, we had some trouble. Layers 25, 37 and 40 didn't always
behave like they should. But layer 11 was problem-free, quite unlike its
Java counterpart. Considering the productivity gains we received, I never
expected things to go so smoothly.
Note: In response to the controversy generated by this article, I
posted some followup
remarks.
|