| Max Guernsey, I...'s profileMax Guernsey, IIIBlogLists | Help |
|
April 14 Strengths & Weaknesses of .NET Exceptions"The outrages I have suffered today will not be soon forgotten!" - Stewie Griffen, Family Guy In the exceptions department, at least syntactically (in C#), .NET has something on Java but, all in all, I think it's down a few points. One problem is that exceptions have to derive from the concrete class System.Exception. The second and, in my opinion, bigger problem is that exceptions are not checked at compile time. The Good NewsAs far as I know, Java does not support catching without specifying the type of, or defining a variable for, the exception being caught. C# and, I think, VB.Net do. Sound trivial? It kind of is. It's just a cleanliness issue... at best. All it really gets you is the ability to avoid declaring the uneccessary. Instead of...
...we can just do something like...
Instead of...
...we get...
Wooptie-freakin'-do. Still in the spirit of bone-throwing: Good job, guys. The Bad NewsThe bad news is that the good news was trivial and there are real, significant weaknesses. The fact that we have to place all exceptions somewhere in a single, monolithic hierarchy is a code-quality issue. A lack of compile-time exception-checking defers discovery of problems. Come Join UsThe inability to throw something that doesn't inherit from Exception is not that big a deal. An abstract class can be used to hide implementation details as well as anything. The real problem is that we can't catch based on anything other than a class. All of the sudden, the "little code quality problem" caused by forcing all of our Exceptions into a unitary taxonomy has gotten a lot bigger. This greatly limits our ability to catch categories of exceptions. Knowledge is... um... Liability?In Java you have to either catch an exception or declare that a particular method throws said exception. This is not true in all cases - implementing certain interfaces renders an exception immune to compiler-checking - but it's a fairly minor, "opt-in" flaw. When not circumvented, it provides developers with valuable feed back no later than compile-time... which is pretty early. In .NET, we don't get that. Any exception can be thrown by any method. This means that (realisticly) we can discover if an exception is properly caught no earlier than run-time. The following is a list of cases in which I could see knowledge as a liability:
Please note the position of "Programming" on that list. You Get The Platform You DeserveThis is, in my opinion, the best part. Oh so long ago I was talking with one of the people responsible for the design & development of .NET. I asked them why they chose not to adopt a sort of "Catch or Specify" rule as existed in Java. The reason was something along the lines of the following:
That's right. In order to prevent the dregs of our industry from degrading the quality of their code, Microsoft did the obvious thing: it guaranteed that everyone's code is of equally low quality (in this respect). It's all very Karl Marx. How about this... let's take this to its natural conclusion:
In fact, I propose the following syntax for C# 4.0. It is 100% guaranteed to ensure people don't make mistakes: return: causes the program to exit. ...and in VB.Net that would look like: Return: causes the program to exit. April 07 Throw First, Please"No. Dig up, stupid!" - Clancy Wiggum, The Simpsons One of my many, many pet peeves is when people do something like the following:
It is a lot easier, cleaner, and more maintainable to do something like:
Exceptions are about... Well... ExceptionsExceptions are more than just "an error handling mechanism." They are a way of thinking. Good code operates on a set of stipulations. Cases that fall outside of those assumptions are not errors they are simply deviant circumstances requiring attention from a higher authority. The paradigm shift described above improves both cohesion and encapsulation and, out of that, falls out a secondary benefit: the ability to discover exceptional cases earlier. With nested "if statements," assuming the problem is reasonably complex, it is usually difficult to fail as early as possible. Doing so often leads to massive networks of nested if statements which are difficult to read or maintain. A Change in ThinkingShifting your mind set from handling multiple cases (if/else) to handling one case and "passing the buck" upward otherwise (if/throw) is not easy. Most people won't just snap their fingers and shift gears. Proper use of this technique requires the same delicate touch you might apply to static structure. If you make the leap, though, your code will be a lot cleaner and more flexible. Better EncapsulationConsolidating decision making improves the Encapsulation or, at least, the potential for Encapsulation in your code. With an if/else, you can refactor predication, but not the resulting branch, into its own method, as in the following:
Whereas with an if/throw, you can encapsulate both the decision and the branch.
The Encapsulation Bone's Connected to The Open-Closed BoneAnother clear advantage is the ability to easily recognize new, and discard old, exceptional cases. As your services gain popularity within a system, the set of circumstances in which they can be applied will flex a little. Sometimes they flex inward - potentially eliminating the need for the exception. Other times they flex outward - possibly adding an exceptional case. Being able to easily encapsulate both a decision and a branch allows us to do all sorts of neat things with it. For instance: we could refactor our EnsureConditionMet method into a Strategy. Once we've done that, we can move it into a Decorator, dissolve it into a Chain of Responsibility, Adapt it from some other component that already solves our problem, vary it with a Bridge, and so on... Discover EarlyOnce you've broken out your exception raising technology, you get something else almost for free: early failure. Every line of code that executes up to some point of completion carries with it a little risk. If your exception discovery technology is broken out properly, it becomes easy to see when the earliest point at which a stipulation can be enforced: when you can provide the decider with all of the information it needs. The sooner you throw, the less risk you incur on an task that will obviously not be finished and clean up after what has already been done. ConclusionUse exceptions the Chicago way: throw early, throw often. April 05 Capacity to Deliver as a Currency"No one exceeds their potential. If they do it just means we didn't judge it accurately in the first place." - "Dr. Lamar" from "Gattaca" BasicsFor some reason people spend a lot of time cutting their own hamstrings. They feel pressure to deliver so they work "harder," degrade code quality and, effective, borrow a feature from future releases at an exorbitant rate. Quality - and, I think, any kind of capacity for delivery - is a currency. You can have more of it or less of it. You can spend it to get a feature. If properly deployed it can yield dividends. If properly managed it starts to compound. Sounds like a currency (or, maybe, an extremely liquid security), right? Let's go through a some models. Now, like any good hypothesis pulled from one's gut (I leave it to the reader to guess what route it took), this entry relies on the principle of ceteris paribus. Factors like people's ability to "learn while doing" are left out of the discussion but understood to have a real-life effect. We have two metrics: capacity to deliver and rate of delivery. Capacity to deliver is an abstract measurement of all the things that allow a team to deliver functionality; skill, code quality, methodologies, processes, et cetera. It is denoted by thick, green lines. Rate of delivery is the amount of business value that is delivered in a period of time. It will appear as a thick, red line. Good Manager/Bad ManagerThe simplest, and most prevalent, model for work is also the least likely to be realistic. It is the notion that, ceteris paribus, capacity to deliver is constant and a good manager or project manager "rides" people to deliver. In this model, it is as though capacity to deliver is a ceiling from which the team's rate of delivery is suspended by springs. A "good manager" constantly presses upward, compressing the springs and "squeezing a little more" out of his team. When a spring becomes exhausted, we call it "burn out." The manager tries not to push too hard because he doesn't want to lose a good spring - after all: they may be pushing away from true potential, but they still keep things from falling apart. To keep the spring coefficient from becoming zero, we periodically send springs on vacation or buy them lunch. In times of stress - times when the manager has just "gotta get something out the door" - he pushes harder on the rate of delivery - assuming that his springs will hold and that the green line (capacity to deliver) is constant. This doesn't seem to be a very realistic model and, fortunately, people are starting to realize it. It seems to me, and to many others that the green line (capacity to deliver) is not a constant. It can be manipulated and, actually, it is the most important thing too change. Capacity to Deliver as an Exhaustible ResourceIf we treat capacity to deliver software features as though it is mutable we get a different picture. To do so, we have to redefine capacity to deliver. Instead of being the absolute maximum a team might be able to do over some period of time, let's say it's the most a team could deliver in a sustainable way. In my experience, the red line and the green line appear to be covariant. Each simultaneously affects how the other behaves but the rules aren't anywhere near as simple as the behavior of a spring. I can't say I know all of the dynamics but I can say it's a lot more complicated than linear resistance. One thing I've noticed is that, when the red line is near the green line it pushes capacity to deliver away. So, when the rate of delivery is just below capacity, capacity starts to go up. When a team is delivering above capacity it starts to lose its ability to deliver in the future. Even this doesn't work like a spring, though. It almost works like the opposite of a spring; as though there is a "sweet spot" where the pressure remains highest. It seems like there is a focal point - at least when the rate of delivery is below capacity - where urgency provides incentive to invest surplus resources rather than squander them trolling the Internet for meaningless blog entries. Beneath the sweet spot you will probably get nothing. Sometimes it's because people just don't feel a need to work. Other times people need to work but there isn't enough context to do anything of value. A worthless framework with high-code quality is still a worthless framework. One rule that's fairly obvious: the further above capacity you try to deliver, the more long term damage you do to your future abilities. In other words, the rate of delivery presses down on capacity for delivery as though it were a weight proportional to the distance between the two. Attempt to deliver too much, too quickly and you will break your product's or your team's back... or both. Earlier, I mentioned that the two lines were covariant. Capacity to deliver effects the rate of delivery in a couple of ways. The true maximum rate of delivery is related to the capacity for delivery. Once a team's capacity is sufficiently high, it will be able to achieve a sustainable velocity that is far greater than anything it could have done earlier - even at a "breakneck" pace. Sometimes what happens is the team is given an opportunity to improve its capabilities but, then, the pressure to deliver is increased (based on the "spring-loaded development" philosophy shown above). When this happens, the results can be disastrous. If the rate of delivery is cranked up too high (specifically: high enough to exceed the team's capacity), the team's capacity to deliver will start to degrade and all of the previously invested resources will go to waste. The bottom line is this: Set realistic goals and focus on constant improvement. If you do this, you will actually meet your goals in a sustainable way. If you focus on short-term gains then all you get is a burst of apparent value with a massive, hidden liability. April 04 Completeness vs. Agility"...but I'm so comfortable... too comfortable..." - Tool, "Undertow" NOTE: I use the word "Agility," in this entry, to refer to a hypothetical force that drives us to remain agile, nimble, or adaptable. I am not referring to any specific Agile methodology. Recently, at the gig on which I am currently working, I was pairing with someone who is new to Design Patterns. He was working his way through implementing the Composite pattern and doing a good job of it. At some point it was suggested that he wouldn't need to implement all of the methods on the composite class. The reason for this was that, in the only known context for the composite, about half of the methods would be used. To an extent, I could see the argument so I said nothing. The natural instinct, when building a software system, is to make it "complete." That is: we try to add every feature that anyone might possibly need. Later we find out which features were waste and which were necessary - assuming we deliver anything at all. Agility drives us to mitigate waste by developing what is understood to be the most valuable thing first, then getting feedback on what's really important. So we can see how Agility and Completeness end up competing in a healthy way: the drive to complete things helps us implement the features to which we've already committed, and the drive to remain agile helps minimize the set of features to which we commit at any given point in time. A very nice balance. This client of mine is working in a test-driven environment. I don't just mean they write unit tests before they start to code - although they do that - I mean tests drive everything. They write acceptance tests before they even start thinking about the units of code involved in a problem. These tests are an excellent prototype for the environment in which all relevant classes will be instantiated. It is very unlikely that leaving a class only partially implemented would end up being a problem as the acceptance tests will catch the problem before we call our software done. Still, I don't think that means we should leave a class half-implemented. The same way that a feature is not "done" until it's done, it seems that a class should not be "done" until it fulfills its contract entirely. I think that classes are atoms; they should always be whole and to divide one should be a considerable event with commensurate consequences. I propose that we separate in our minds the concepts of "completeness" and of "wholeness." In so doing we can recognize that nothing intra-universal is ever really "complete" except (possibly) the universe, itself and then only in a grotesque sense. Things can still be "whole," though. For example: "I spent a whole dollar on that candy bar, yesterday." Wholeness could be a single force underneath which all the many flavors of "it's not done until it's done" could be united. If we then recognize that Wholeness is not at odds with Agility the same way Completeness is, we end up with a nice, clean line for where to stop working on a thing. Always favor Agility over Completeness when limiting scope, but never limit the scope in a way that would jeopardize Wholeness. Who knows? It is possible that a deeper analysis would find that one remains more capable of responding to change if one keeps things whole. In other words: Wholeness may be a part of Agility. If that were true then I may have just wasted an hour. |
|
|