By Markus Sprunck; Revision: 1.0; Status: final; Last Content Change: Nov 29, 2012;
The following design and coding anti-patterns are explained with samples of a real software I developed in Turbo Pascal about 20 years ago. Last week, I rewrote it to a pure Java application and published the result in the article An Example of Finite Element Method Simulation in Pure Java and HTML5 with Paper.js Library.
My first idea was - that this would be a job for an afternoon. Actually, I needed significantly more time. The reasons for the extra effort have been some poor coding practices I have had as young developer. Although the old Pascal software worked properly and created the expected results, the maintainability was quite poor. After such a long time - even you own code could have been written by a stranger. Maybe, I needed even more time than for writing everything new from scratch, but I leaned some lessons I'd like to share with you.
The approach of rewriting was straightforward:
My old program had not one line of test code. This is not a nice situation, if you like to change the old source code. But I never intended to change the old source code. So, "Why do I need unit test code to rewrite a software in another language?" The answer is easy, automated unit test cases also serve as a kind of documentation and/or specification. Unit test describe the expected behavior in a better way than most of the written documentation.Top 5 Reasons for Not Using JavaDoc in the Next Project), but almost no comments are really a bad idea.
As workaround, I had to run the old software in an emulated environment and recorded the actual results as expected behavior for the rewritten code. This experience encouraged me again to say: "Always write some basic unit test, even if your software works well. Some day you will be happy about your unit tests."
Don't forget that JUnit was born in late year 1997. My first contact with test automation was about at that time. There are still too much Never-Write-Test-Code-Developers working.
You may be lazy with writing comments, but in that case the code should speak to you in a meaningful way. This means that all classes, methods and variables need speaking names. Sometimes there is still the need for additional comments.
A good sample are the following variable names. When I started to rewrite the old source code, I didn't know what the difference between g, ge, Ae or EE was. Maybe it have been clear during writing the code, but after some years these
name make absolutely no sense.
// Pascal - Sample Code #1:
PRIVATE ny : real ; E : real ; g : g_type ; ge : ge_array_type ; Ae : flaechen_array_type ; KB : K_Bandmatrix_type ; EE : e_array_type ;
In this case even the data typed didn't help, as you may notice from the names.
Just have a look at the following code sample. If you are able to understand what is implemented here within a minute you can be proud. At the end of the code sample you will find a short explanation and the same in a not optimized solution.
// Pascal - Sample Code #5: Obfuscation by optimization happens quite often.
The logic of the code up there is:
a XT Y X
where X and Y are two matrices and a (equal to area * thickness) is a scalar value. The same in Java with a simple helper class for matrix operations looks like this:
// Java - Sample Code #6: A higher level of abstraction may improve the readability
return X.times(Y).times(X.transpose()).mult(area*thickness);Notice, the excessive unfolding of loops was a common practice 20 years ago. Today it is not a good idea. The modern compilers and in Java case the JIT compiler are more powerful than most manual optimizations. In some cases it is even slower to have a manual tuning of the code, because you make the work for the compiler more difficult. This is not true for inefficient algorithms, they have to be avoided in any case.
// Pascal - Sample Code #2: Never make hard coded limitations for data structures.
Another problem for maintainability is the mixture of business logic and user interface. In the following code piece you see that the visualization class and event handling was in one class with the business logic. This is especially dangerous for validations and the allowed order of execution.
// Pascal - Sample Code #3: Avoid mixing business logic and user interface code, especially for verification and program flow.
PmyEditWindow = ^TmyEditWindow; TmyEditWindow = object(Teditwindow) Prozente : PProzentview; Wertedialogdata : WB_DialogTyp; PROCEDURE WertebereichsDialog; constructor Init(VAR Bounds: TRect; FileName: FNameStr; ANumber: Integer; AInputfileGelesen : boolean ); constructor Load(VAR S: TStream); PROCEDURE HandleEvent(VAR Event: TEvent) ; virtual; FUNCTION GetPalette : PPalette ; virtual; . . . END;
The following coded solves the linear equations for the FEM analysis. For this output files are created, externally solved and the result will be imported again. Replacing this code with a solution without file system access increased the performance by a factor of 100 without any additional effort.
// Pascal - Sample Code #4: File access is almost always a performance bottleneck.
BandMatrix_Speichern('K2.mtx',KB); Prozente^.NeueProzente:= 50; Prozente^.drawview; Vektor_speichern('F2.mtx',F_aus ); Prozente^.NeueProzente:= 60; Prozente^.drawview; DoneSysError; DoneEvents; DoneVideo; DoneMemory; SetMemTop(Ptr(BufHeapPtr, 0)); SwapVectors; PrintStr('Tippe EXIT um GRAPHIK fortzusetzen.'); Exec(GetEnv('COMSPEC'), '/C solve.bat'); SwapVectors; SetMemTop(Ptr(BufHeapEnd, 0)); InitMemory; InitVideo; InitEvents; InitSysError; application^.redraw; Prozente^.NeueProzente:= 100; Prozente^.drawview; Vektor_lesen('F.mtx',F_aus ,'D.mtx',d_aus ); Prozente^.done;
File system access for data exchange and/or logging is very time consuming.