Rewriting is hard, even for small applications
I came across an application that would suit my interest in model railways as a computerised control panel written as a Windows Forms application in VB .Net. Think CAD with control inputs that can connect to a hardware interface. Sadly, the original author had died young leaving the code in limbo for over 10 years. I had the idea to take it, open source it and give it some more life but I couldn't really do it straight-away because:
Rewriting is hard, though. In our head, we think it will be great to be able to build from scratch, therefore leaving behind the bad choices or replacing old design ideas with new ones. Of course, it is never that easy, you generally end up meeting difficult choices, that were made in the old code and now lost in time, and also you meet compromises that are hard to make a call on.
The first step in a good rewrite is asking, firstly, "Do I need to rewrite this" and this boils down to the question of, "What is my end game here?". If my end game is simply to use the functionality that already exists then there is no reason to rewrite it. It works, it might be in a language I don't like but why waste days of time fixing it if I don't need to change it?
In my case, since the end game was open source, a future and further development, my main rewrite aim was simply to bring it as up-to-date as possible and replace some of the poorer design choices with neater code, to make it easier for contributers to help out. Some of these poor design choices included:
Porting VB .net directly to C# is not 100% possible because there are certain things that do not exist in C# such as module variables (they would need a static class). Some of the types are in different namespaces in VB since they were added for backwards compatability and were not added to C#. In general, however, it is relatively straight-forward to turn a VB class into a C# class, "New" into a Constructor, module variables into static classes etc and in fact, both types can exist in the same project - the type of project (vcproj or vbproj) is merely about what types get offered by default in the "New" dialog!
So how to start? If it was a 100 line program, it wouldn't matter. If it was 50000 lines, it would have required a lot of functionality described on paper and understanding gained before work started but this was one of those in-between programs.
Method 1: You can simply start a completely new project and pull in functionality as you go. For example, you start with the main form and add handlers for e.g. New, Open, Close etc to build up a basic working app (You can copy and paste form controls from one project to the other!).
Method 2: You delete one vb type and replace it with a C# type either in the same project or in a separate library. Theoretically, you keep the whole thing working at each stage.
Each has its pros and cons. In Method 1, you get a clean foundation and it is perhaps easier to not put in functionality that is not designed well as you go along. At the same time, it will take you a long time to get something that even resembles a fully-featured app and you might not even spot some major bugs until much later on. One example is when I was simplifying the way the Add/Edit dialog used a class variable to keep track of changes made in the dialog. "We don't need that", I thought, replacing it with a direct reference to the object in the main collection, until later, something crashed and I realised that it was done that way so you can cancel out of the dialog without making the changes. Kind of obvious but easily missed when you only have a skeleton of a program.
Method 2 sounds more friendly but remember the 80/20 rule. 80% of it is easy. Replacing a "Signal" in VB with a "Signal" in C# is pretty easy, copy the properties, reformat for C#, add some braces and semi-colons and it just works. Except of course when it doesn't. When you notice some weird way that a global has been passed around and as soon as you try and move the global to a static class, you suddenly get 500 build errors. Do you spend a load of time and fix them all or might you end up removing a load of them in the wash anyway? Another example was calling the model to validate itself and then the model calling MessageBox.Show() if there is an error. Not something you would normally call from a model. Do we leave it in there for now? Delete it with a TODO? Decide on a refactoring pattern now and start doing that? Fortunately, there is a lot of commonality in the classes which makes it fairly easy to remember how we removed that code before but once you have 5 or more refactorings ongoing, it gets very hard.
So started with Method 2 but decided it did not actually give the benefit of an always-working application, so I went back to Method 1. I have the original code to use as a reference, a new project with just new code in it and a "Scratch" project that I am deleting things from as I go along so I can see my progress.
Two other areas that I will need to battle (but have mostly left for now) is the tight-coupling of a) The hardware interface - which currently uses MERGs RPC and b) The file storage mechanism, which is currently plain text with tabs (what could possibly go wrong!) but which should both be abstracted away. I have made a start on file saving and reading, especially since it would be good to maintain backwards compatibility but I have put this in a separate Serializer class that uses the Visitor pattern to keep it away from the models.
The moral to the story? I don't know. Try a bit, decide whether this needs to be done at a much higher (functional) level or whether you can just hack and whack. Don't be afraid to change tack if it gets too hard and keep regular labelled commits into source control so you can backpedal if you bite off more than you can chew!
- It was written in Vb .Net, which encourages some poor design choices and is a lacking skillset in the community
- It was written in .Net framework 1.1, a very outdated framework (2003), with a large set of features that are really useful but didn't exist back then.
- There were a number of design choices that showed lack of experience (but the author was not a Software graduate so that is understandable)
Rewriting is hard, though. In our head, we think it will be great to be able to build from scratch, therefore leaving behind the bad choices or replacing old design ideas with new ones. Of course, it is never that easy, you generally end up meeting difficult choices, that were made in the old code and now lost in time, and also you meet compromises that are hard to make a call on.
The first step in a good rewrite is asking, firstly, "Do I need to rewrite this" and this boils down to the question of, "What is my end game here?". If my end game is simply to use the functionality that already exists then there is no reason to rewrite it. It works, it might be in a language I don't like but why waste days of time fixing it if I don't need to change it?
In my case, since the end game was open source, a future and further development, my main rewrite aim was simply to bring it as up-to-date as possible and replace some of the poorer design choices with neater code, to make it easier for contributers to help out. Some of these poor design choices included:
- Using the "Add/Edit Item" form to delete items - this meant all the form references existed for the life of the application.
- Use of module variables (VB .Net language for Globals)
- Lots of duplication of code e.g. input validation for forms was generally copied and pasted into each form
- Very complex over-engineered use of collections to store the individual items, such as its screen position, its type and other properties. This meant operations like Add and Delete have to modify multiple collections to keep data integrity.
- Large files including multiple classes instead of separating them out into separate files
- Some good use of inheritance, some poor use of it.
Porting VB .net directly to C# is not 100% possible because there are certain things that do not exist in C# such as module variables (they would need a static class). Some of the types are in different namespaces in VB since they were added for backwards compatability and were not added to C#. In general, however, it is relatively straight-forward to turn a VB class into a C# class, "New" into a Constructor, module variables into static classes etc and in fact, both types can exist in the same project - the type of project (vcproj or vbproj) is merely about what types get offered by default in the "New" dialog!
So how to start? If it was a 100 line program, it wouldn't matter. If it was 50000 lines, it would have required a lot of functionality described on paper and understanding gained before work started but this was one of those in-between programs.
Method 1: You can simply start a completely new project and pull in functionality as you go. For example, you start with the main form and add handlers for e.g. New, Open, Close etc to build up a basic working app (You can copy and paste form controls from one project to the other!).
Method 2: You delete one vb type and replace it with a C# type either in the same project or in a separate library. Theoretically, you keep the whole thing working at each stage.
Each has its pros and cons. In Method 1, you get a clean foundation and it is perhaps easier to not put in functionality that is not designed well as you go along. At the same time, it will take you a long time to get something that even resembles a fully-featured app and you might not even spot some major bugs until much later on. One example is when I was simplifying the way the Add/Edit dialog used a class variable to keep track of changes made in the dialog. "We don't need that", I thought, replacing it with a direct reference to the object in the main collection, until later, something crashed and I realised that it was done that way so you can cancel out of the dialog without making the changes. Kind of obvious but easily missed when you only have a skeleton of a program.
Method 2 sounds more friendly but remember the 80/20 rule. 80% of it is easy. Replacing a "Signal" in VB with a "Signal" in C# is pretty easy, copy the properties, reformat for C#, add some braces and semi-colons and it just works. Except of course when it doesn't. When you notice some weird way that a global has been passed around and as soon as you try and move the global to a static class, you suddenly get 500 build errors. Do you spend a load of time and fix them all or might you end up removing a load of them in the wash anyway? Another example was calling the model to validate itself and then the model calling MessageBox.Show() if there is an error. Not something you would normally call from a model. Do we leave it in there for now? Delete it with a TODO? Decide on a refactoring pattern now and start doing that? Fortunately, there is a lot of commonality in the classes which makes it fairly easy to remember how we removed that code before but once you have 5 or more refactorings ongoing, it gets very hard.
So started with Method 2 but decided it did not actually give the benefit of an always-working application, so I went back to Method 1. I have the original code to use as a reference, a new project with just new code in it and a "Scratch" project that I am deleting things from as I go along so I can see my progress.
Two other areas that I will need to battle (but have mostly left for now) is the tight-coupling of a) The hardware interface - which currently uses MERGs RPC and b) The file storage mechanism, which is currently plain text with tabs (what could possibly go wrong!) but which should both be abstracted away. I have made a start on file saving and reading, especially since it would be good to maintain backwards compatibility but I have put this in a separate Serializer class that uses the Visitor pattern to keep it away from the models.
The moral to the story? I don't know. Try a bit, decide whether this needs to be done at a much higher (functional) level or whether you can just hack and whack. Don't be afraid to change tack if it gets too hard and keep regular labelled commits into source control so you can backpedal if you bite off more than you can chew!