Refactoring (Updated)
Thursday, 24. July 2008, 00:27:55
One night, I was listening to Coast to Coast AM and they had Neal Adams as the guest. Neal Adams is best known for his work on Batman and Green Arrow. He was asked how come movies have such a hard time with their heroes, but comic books really have done a good job? What Neal said really surprised me. He said back in the early days, every single idea no matter how good or bad was tried. EVERYTHING!!! Not one single idea was left unturned. If an artist had an idea, it was tried. So it did not take long that new and original ideas were hard to come by until it became very difficult for new artists to get something published. He also said that none of the heroes we know today exist as they did originally. So basically, everything stems from really bad ideas.
How can this be? How could every single comic book hero come from bad ideas? Neal discussed more of the process. They would have limited trial runs. Many of these became limited editions collectors items if the comic was later picked up for general publication. If they did not do well, they were scrapped. Those that did well, the heroes evolved over time depending on the readers' response to them. He mentioned so many examples that I can hardly remember any of them. One example that everyone knows about is that Superman could originally only jump over tall buildings. Today, he can fly and even turn back time.
How can this be? How could every single comic book hero come from bad ideas? Neal discussed more of the process. They would have limited trial runs. Many of these became limited editions collectors items if the comic was later picked up for general publication. If they did not do well, they were scrapped. Those that did well, the heroes evolved over time depending on the readers' response to them. He mentioned so many examples that I can hardly remember any of them. One example that everyone knows about is that Superman could originally only jump over tall buildings. Today, he can fly and even turn back time.
I want to switch gears and talk about a Google Tech Talk I saw a while back. It's called Competing on the Basis of Speed. It was surprisingly good and isn't what I thought it would be. One idea was to implement multiple solutions at once. And we're talking ideas that translate to software here. I remember two of the examples given. One was a car manufacturer who developed many different engines for the same car. I don't remember the exact number, but it was in the double digits. Only one of these engines would be used in the final product. Aren't all these other models wasted? No. For one, you learn everything you can about producing those kinds of engines. Some of these were used in future models of vehicles. They also were able to streamline their engine production that would have been impossible if they only chose to build one engine. What's more, they were able to make a critical decision (choosing the right engine) as late as possible when they had the most information available.
The other example was a scanner that could read handwriting or something like that. They could produce a simple algorithm in a short amount of time, but it did not work very well. Or they could produce a complex algorithm would take quite a few months to develop. The correct answer is to develop both. If you can't get the complex algorithm done in time, then the profits from the bad algorithm can be used to pay for the development of the better algorithm. Sometimes, not perfect is better than nothing. It can still save a lot of time from people who have to convert the information.
I haven't watched the video in a while, so the exact details may be off a little. But the gist of it is the same. Convincing a company to fund something that will be thrown away or wasted is not an easy one. Also, funding two separate implementations when only one can be used instead of sequentially implementing them is not intuitive. But there are plenty of success stories that demonstrate that trying many solutions works better than one. Comic books sure tried everything they could. Businesses usually succeed the second or third time around. Not only because they learned a lot, but because they have a second or third idea. If you only have one idea and it doesn't work, you're out of luck. It's also said that students who go back to University or College the second time around have a huge probability of graduating. This is more about commitment, but sometimes they change majors. This does not mean you need to study many different areas at once, but that until you test the waters, you won't know for sure what is right for you.
1. Try multiple solutions.
2. Wait until the last possible moment to make a critical decision.
3. Refine the best solution.
That third step is the refactoring step. If you haven't done the other two steps, you'll end up doing them in the later steps anyhow whether you like it or not.
In Project V, I ended up doing this several times already. I'm programming this myself, so it's not like I had much of a choice, but having the resource to do otherwise, it's a good lesson to learn. I think I've tried at least five different type systems. Each one was not necessarily better than the last. But each one got closer to what my goals were. In fact, each one helped to clarify what those goals were. It's a known fact that you always have a better idea of what you want once you see something in action. Just ask real estate agents. People know what they like once they see it. But before then, it's difficult to pinpoint. Just think of a work of art. How do you know of the art you like before someone even conceives of it? It just doesn't work that way. And ironically, this often infuriates programmers when they suffer from the same problem. For example, software development will always improve, but try to get a programmer who believes the self fulfilling prophecy that there is no Silver Bullet as to what he or she would like best as a new development platform that has not been created yet. It can't be done. If it could, we could predict the future (ahem).
Another example where I tried different solutions was the GUI. It's funny tracing the evolution of it. It ended up having quite a few similarities with the way Windows handles its GUI. And Windows looks similar to the way the Amiga OS's Intuition GUI system was handled which predates Windows by at least 10 years. My final refinements have more to do with Project V than with a strict GUI system though and that's the way it should be when you refactor. However, there is one thing that does bother me. I should not have had to try different implementations of GUI's. This forced me in doing step #1 which in turn forced me to do #2 as well whether I liked it or not.
With comic books, the failures aren't done anymore. If you offer a suggestion and try to get published, the editors and publishers will know what has been tried before and will not go for a second round. It's pointless. They already know what the outcome will be. There may be exceptions, but the general case holds. In programming, failures are thrown away and not remembered except for those who were directly involved.
I have one last item I want to talk about. For this one, we go back many years to when I lived in a basement apartment that if you called it a hell hole, it would have been a compliment. I was hardly ever there, so my time in that city was still a great deal of fun. I remember one time when my landlord was surprised the stove worked at all when I told him that you could only turn on one element at a time. If you know what kind of stove this is, you should be laughing your head off right now. In any case, one of the walls of the foundation had a crack. Water leaked in every time it rained. He had tried everything. He brought in any expert he could find. He even bought stuff from Japan. If there was anything on the market for filling cracks in foundations, he had tried it. Nothing worked.
He's explaining all this to me and I'm trying my best not to laugh. Actually, I think I did laugh and he said it was ok and that it was indeed a stupid situation. So he asks me if I know of anything that could fix this 100% no matter how crazy the idea. He's ready to try anything. I tell him that you could lift up the house off the foundation, break apart the section that has the crack, re-pour that section and bring the house back down. He looked at me with the most dumbfounded look I have ever seen. He said this was way too ridiculous. "What would the neighbours say?!", he exclaimed. This, from a man who owns a neon purple house. That has another story, but we won't go there. Needless to say, even though he said he didn't care what the solution was, he obviously did.
Yet, this isn't about sticking with what we are comfortable with. You could probably make that point, but that's not the goal here. No, what we're talking about here is that it is possible to replace the foundation without affecting the integrity of the rest of the structure. I don't think most people would even think of raising their house. I also don't think most people would think of replacing the foundation of their software. Further, I don't think it would be possible in most cases.
Why is it not possible? To understand this, you need to look at how connections are made when you build something. It doesn't matter if you're building software or something physical. This is a universal property. One kind of construction involves piling onto what's already there where there will always be one end that is not connected to anything. Even if you connect the open end, the new piece will have an open end and so on. This is commonly known as a hierarchy. Tree data structures are like this. No matter how many nodes you add, there will always be leaf nodes that aren't connected to anything. There are physical structures like this too. The great pyramids are like this. There is only ONE rock at the top that isn't connected to anything, but it still satisfies the criteria that you have to remove everything on top before getting to the foundation. Defensive walls and towers like the Romans built also had this property. If you tore down the foundation, the wall came crumbling down. It's one of the weaknesses of this structure. If the foundation goes, so does the rest.
The other structure is one where everything you install is meant to be connected on all ends. Phone lines and all telecommunication fibres share this feature. If you want to replace a segment, the entire network doesn't have to fail. Networks may indeed have leaf nodes, but it's not necessary. Most communications network have redundant paths, so that even if you use a hierarchy, it's not monolithic. Road networks are like this too. You can block a road and even though there will be congestion, at least some traffic still gets through or one can use an alternative route. Houses are also built like this. The floor joists are connected at both ends to the outside walls. The walls are connected to the roof. And the roof is connected to all four walls. Windows are connected on all sides and can be replaced. The foundation is likewise connected on all sides. The advantage of this kind of construction is that you can replace any part and it doesn't affect the rest of the structure.
Now let's look at our programming languages. A function is something that calls or invokes something else. It is definitely a tree structure where only existing functions can activate other functions. As such it is a building block of the first kind of construction where you need to have functions that don't call anything else. It's impossible to write software where you have functions that always call something else and does something significant. Returning right away is not using the abilities of the function itself and does not count. So every single paradigm and programming language that uses functions or methods lacks the ability to replace its foundation. Lisp, Java, C, C++, Pascal, BASIC, Erlang, Python, Smalltalk, Ruby, Oz, COBOL, FORTRAN, Modula-2/3, Haskell, JavaScript and more are all in that category. Include all stack based languages as well. All these languages have the built-in weakness that the foundation of the software you are building is the most critical part and must be built first, yet it must be constructed when you have the least amount of information.
The other category for software development is data flow. There are a few examples out there. Monads and arrows are such examples. Nodes in Lightwave and other 3D packages are other such examples. The Internet itself COULD be an example, but mostly everything online uses the request/reply paradigm. Overall, there aren't that many implementations that use this kind of construction.
Here's a quote from Alan Kay:
The Internet is like the human body. It’s replaced all of its atoms and bits at least twice since it started even though the Internet has never stopped working. Attacks on the ‘Net aren’t really attacks on the ‘Net, they’re attacks on machines on the ‘Net. Very few software systems, maybe none, are built in ways that sustain operation in spite of being continually rebuilt and continually growing.- Alan Kay
Alan Kay noticed the feature of these two different kinds of construction, but does not understand why this is. He just thinks it's all about how you organise your software and designs. However, he is quite right that there are very few software systems built in the ways he describes.
Lift your software up and away from functions. Replace the parts that use functions, and then lower your software back onto its new foundation. Can you do it? Or is that too ridiculous? What will the neighbours think indeed!
(NOTE: This blog entry isn't about functions or data flow. Just in case anyone thought that, it's the wrong message.)
update:
Ok, I'll state it point blank. The title of this article is called Refactoring. The real world has already determined what kind of construction techniques are amenable to change and which are not. It's been known for thousands of years. And no amount of wishful thinking that software is somehow different to the rules of the Universe will make this fact untrue. You only use the first type of construction when you want something to stay there, as is, for a very long time. That's why the pyramids, water towers, lighthouses and defensive walls and towers are built like this. Sometimes, you can change the top (like adding weapons, archers and cauldrons of boiling tar to the top of a defensive wall), but the majority of the structure is not meant to change. And sometimes, none of it is meant to change.
Software isn't meant to stay static, yet most (all?) languages use the first type of construction. It doesn't matter what you use as building blocks of your software just as long as you make sure that the properties of what you are constructing are similar to the second type of construction.
The real world has already tackled most problems. Most of these have been tackled thousands of years ago. Remember that the Romans had a running water system that was unmatched until the 1950's. Unless we want to write the equivalent of the Great Pyramids where our software remains unchanged for thousands of years, then all we're doing is rehashing thousand year old ideas that are terrible when used in the wrong place. Refactoring has been done in almost every field you can think of. There are certain rules that cannot be broken. So far, programming is the textbook example of what happens when one uses the wrong kind of construction. Not sure what it says about the ones who thought up these languages in the first place though.
So to those who think we stand on the shoulders of giants, I'm sorry to inform you that those giants are standing on quicksand. There's a reason large software produces certain limitations.
I break my software into declarative and procedural aspects which are bound together by a standardized type system. The goal is to write as much code as possible in the declarative mode. When the 'foundation' (the procedural implementation) needs to be changed, the declarative code does not change.
This worked quite well when I recently did a complete rewrite of how I implemented widgets in a GUI system.
By anonymous user, # 24. July 2008, 08:08:51
I second the statement about using the higher level declarative methods in the design. If you say what you want instead of how to do it things are much more likely to get done, and it is easy to improve them over time and keep the same objective.
I have also found that the more points of variation that get built into the system the easier it is to change. Conceptually I build services and protocols using Java interfaces and data structures, they may be function calls when one gets down to it, but as long as I keep thinking about the services and not the function calls change is much easier, I can even replace the foundation (which ends up being the persistent data layer) without having any effect on anything not directly interacting with it, which I try to minimize.
One can also think about programming in existing languages using data flow, which unfortunately will require external notes on how the data is flowing, but the metaphor is still good, and allows for great flexibility to replace anything surrounded by interfaces.
I agree with what you say, except for being unable to do it in today's languages. Today's languages do not make it hard to build flexible systems, but they don't make it easy to know how to build flexible systems, and don't make it hard to build inflexible systems.
By anonymous user, # 24. July 2008, 11:49:22
I'd have to disagree. Data doesn't DO anything. Your data is actually the top layer, not the bottom. In any software, you can usually change the data, but changing the code is usually more difficult. I'm talking about removing parts of your software and replacing it with something else or even merging two pieces of software together at runtime. And adding on more functionality on top through extensions has its limits. I'm afraid you have your foundation and top layers backwards.
I agree, they make it near impossible. To think otherwise is to disregard every law of the Universe even when it smacks you in the face. This isn't to be rude, but to start accepting facts when they are right in front of us. The reports that software size has certain real limitations are not made up. After a certain size, things start to get more and more inflexible. Why? Because the larger the system, the more "layers" you have in your hierarchy of functions.
Think of a pile of wood. If it's a small one, it's not too hard to get to any particular piece and replace it even if it's at the bottom and put everything back. But a really large pile will make it impossible to put everything back just right if you dare go that deep. The amount of pieces that are affected are just too numerous and unpredictable. Plus, you can make the entire thing collapse. Humans have the ability to go down several levels in code. There have been studies about what humans can remember in one go. For example, if you can remember more than 9 numbers, you are in the minority. Replacing a function at the core of your code would require remembering things much more difficult than numbers. Also, you have no way to know everything that you will be affecting. After a certain level, things start to have a cascade effect.
When you put all this together, it's obvious why you get into limitations after your software grows to a certain size. Several layers from the top, no problem. Deeper than that, and you start to get into unforseen consequences.
With the network approach, you're always on the first level or nearly so. Even if you use a hierarchy, it's only to group thing. Much like how the surface of the Earth is grouped by a hierarchy with nations, states, provinces, districts, and cities. But you're always on the same layer no matter what. Cities don't stack on top of each other. But with functions, they have to be one on top of the other. Each one affects everything on top of it (everything that is invoked).
Until one expands their horizons away from conventional languages, it will be impossible to see what's out there. Current programming languages are nothing more than a game of Jenga. Playing near the top isn't too risky, but change the bottom a few times and everything falls down. The worst part is that we don't even organise our code this way, so there's no way to avoid this hierarchy in conventional languages. This means we have no way of knowing if we're playing with the top or bottom of our hierarchy.
By Vorlath, # 24. July 2008, 22:21:36
When I mentioned the data layer, it happened to be the schema in a relational database, the parts that interact directly with the database, and formats of external files, the data that does not do anything is the most persistent feature of a system, and the hardest to change the structure of, especially when there is existing data.
Most of the problems I end up working with can be broken down to data and transformations (rather than computation algorithms) which are well suited to network models of programming, which as a consequence keeps the number of layers very small. Unfortunately the languages I have used force me to actually move the data around instead of declare how it should flow.
The key for me is to keep picturing the problem that is being solved as being data centric, and the implementation is just a detail. Due to limitations in current languages the clarity of moving and transforming data does not get preserved from design to implementation, and when I try to preserve it the languages do not allow for the implementation to be optimized automatically, so I end up needing to worry about such things as frequent context switching overhead, and need to mix up transformations to make them less granular.
The best environment I have used for data network programming so far is UNIX pipelines, as long as the right format is crossing each boundary the problem is broken up and the hierarchy is quite a bit flatter, and parallelism comes real cheap from pipelining.
So yes, the languages of today do not make it easy to build a network and hence keep it easy to change things, but higher level design can help to keep the problem of a strict hierarchy from being a real problem.
I have a feeling that you want to directly build the program (or solution to whatever problem you are solving) at the higher level, which is good, but I am stuck thinking about the higher level while still building at the lower level.
UNIX pipelines also remind me of the concept of design by contract, where functions (or transformations) are defined to take a particular set of input and make a particular set of output, but this doesn't help much when with making changes either, except for replacing the implementation of one contained unit, the hope is that if the contract is preserved then nothing else will break. For example a sort is a sort, I don't really care which algorithm is in use, as long as the sorted order comes out the same, and it does not stop me from changing the sorting algorithm later, at run time or compile time.
By anonymous user, # 25. July 2008, 15:29:57
Sorting algorithms are things that get used last as far as your call tree or invocation list is concerned. So changing the algorithm should not be too big a deal. But not everything is like that. A concept that isn't very popular right now is about algorithms that use other stuff (instead of you invoking *it*). We have a few attempts at defining these with platforms, frameworks, callbacks, extensions, visitor pattern, etc. But they all work backwards to the way we would intuitively build these things if it were not for the way programming languages let you do things (or force you to do them).
By Vorlath, # 26. July 2008, 04:21:18