After little bit of polishing and cleaning, prototype code:
- has 238 lines of code,
- imports rpg_objects.js file on average in about 250ms
- it is perfect import (writing file gives the same content as original)
- imported outline is in optimal shape
- prototype has been written and polished in less than 8 hours
In Leo’s current implementation of import commands:
- there is 25 files in
leo/plugins/importerswith total of 4634 LOC plus 2125 LOC in
leo.core.leoImportmodule, which gives total of 6759 LOC. All this numbers are pure code lines (empty lines as well as comment lines were excluded).
- it took more than 30s to import rpg_objects.js file
- haven’t tested if the writing imported file would produce same content
- form of imported outline was (IMHO) useless to say the least. It required considerable human effort to reshape outline to optimal shape
- this code was under heavy cleaning and simplifying in April 2017. According to the discussions in group this cleaning and simplifying took much more than 8 hours. How much time has been spent on this code since the beginning one can only imagine
How is this possible? And what can we deduce from this story?
When writing class is suboptimal
About Object Oriented programming were written numerous books, articles, scientific papers,… It would be foolish to say that Object Oriented style is wrong or useless. There are use cases when it is very good idea to apply OO paradigm for writing code. However, there are some cases when we could say that using OO is anti-pattern.
One of such cases is using classes for processes that most naturally could be described using verbs, like read, parse, write, transform, import, export, … you get the idea. When we think about reading or importing source file it looks strange to have a static object that performs this processes. In some languages it is a necessity to write class for about everything, but Python is not that kind of language. Python allows writing classes, but it is not required. In Python we can use different approach if it better suits problem at hand.
Importing a source file is a process. It should not take long time to finish, and once it’s done, there is no need to keep any of the data that was used during process. When process ends, all data used, calculated or given to process become unnecessary. That should tell us that using class for such task is suboptimal idea.
There are many examples in Leo’s code base that we can see this anti-pattern. To import file, in current Leo implementation, one should initialize object, then call some methods on that object, and at the end result is often left in an attribute of this object. When one tries to understand this methods finds everywhere some missing parts, something that is defined or set elsewhere not in a method that is currently on screen. That makes understanding of code very hard.
For example importing source file has two natural arguments file name and position where imported file should be after import. Now, initialization sets file name as attribute, opens file and reads its content, which is also set as attribute to importer. After initialization, if control should go to some other method (there are several kinds of import that importer can perform) it could happen that second method read file from disk again. To prevent this every method will have keyword argument
toString. If one method delegates part of job to some other method, all those keyword arguments are passed every time. Then there are usually few switches passed as keyword arguments too. They must be propagated all the time. And in every method that receives those switches there must be somewhere
if switch do this,
else do that. All this stuff is just a ballast that complicates all methods and makes reading and understanding code hard laboring activity.
When the process is done, all those fields remain attached to importer object, waiting for next initialization when they all need to be reset.
On the other hand pattern that I demonstrated in this prototype shows clearly that we can live with much less ballast. Input arguments are relevant for one invocation and isn’t it natural that all helper functions share input arguments. Also whatever data calculates one of the helper functions, its results are relevant for just one invocation and therefore can be freely shared with all other helper functions. There is no way that anything from outside can change process once it has started. All the data needed for finishing process are kept locally and disappear once process has finished. If we want to change any helper function nobody will notice, no other code could possibly be broken.
This approach should be used as much as possible even when designing classes. Each method should keep all necessary data locally. As self attributes should attach only those data that have somewhat persistent value, even when method finishes execution. All data that we can’t see the point in keeping around between two executions, should be passed as input arguments in strictly one method which ideally should never pass those arguments anywhere outside itself.
Following this simple logic, ensures most effective prototyping process. Prototyping, in its nature, is making series of small experiments, getting results from those experiments as quickly as possible and reacting accordingly. Prototype designer is constantly inventing new hypothesis, performing small experiments to check them, and analyzing results again. Repeats this cycle as many times as is necessary to come up with the solution.
Found solution need not to be general. If it solves one class of problems, and if it is written using this procedure, it is just another few small experiments away from any other class of problem. Prototypes designed this way are very flexible and can be quickly changed to other purposes as well. And best thing is that you don’t need to restrict yourself to single universal instance of the prototype capable of solving all different types of problem. No, it is quite easy to copy and paste whole prototype and reshape it to solve other type of problem. In the end there will be some common code in both prototypes, but it is not big deal. Even duplicated common code if it is enclosed in a function body is pretty safe. It won’t be hard to make changes later if it become necessary.
At the end
I don’t know how (un)successfully have I explained this way of designing prototypes. I do hope that at least some of readers will try to make their next prototype using this style. If you try and get stuck or have any question regarding this text, feel free to ask in leo-editor-group. Comments and suggestions are also welcome and should be posted to leo-editor-group.