Software Design Pitfalls - Design Philosophy  11/6/2022

New programmers often struggle with good code design and partitioning. Experience shows, sometimes in very unkind ways, that it is much easier to create a program which is a monolithic entity. Every class, every module, every run-time element having unfettered access to every other entity. Actions implemented multiple times in various places, and often intertwining disparate program components. Oh, the elegance of your solutions to vexing problems is so obvious and indisputable. That is, until some poor soul must wade into the code to fix some bug or add a tiny little feature. Too often, this task will fall to you, and you will find puzzlement and vexation as you attempt to comprehend the Gordian knot the original coder created, 'That Idiot!'  Even if that Idiot was you.

Why Does this Happen?

Our experience helps us to eventually understand the value of writing software in conceptual modules, which even if you never do re-use somewhere else, provide neat encapsulation and well thought-out and defined APIs so that behavior is more readily 'remembered' and properly understood. 

Sadly, some will go overboard with that lesson, wanting to encapsulate even the tiniest element. I once worked at a place where an over-zealous programmer, fully intoxicated by the new-fangled OOP hype, implemented a 'bit' class, a 'nibble' class, a 'byte' class, a 'word' class, a 'long-word' class and others to deal with arbitrary length and aligned bit-fields. The whole idea that we could encapsulate bit manipulations for hardware register values using OOP abstractions behind these classes was ludicrous, not because it was impossible, but because it obscured the intended action. It was verbose and obtuse. It left the programmer casting spells in 'C++' code to do even the simplest of bit-manipulations. It was hard to write (correctly!), hard to debug and hard to understand when reading the code.

Now, don't get me wrong, I understand the reasons that more naive programmers, or those in a hurry, will intertwine many objects and modules. It is because it is expedient. It simplifies the design effort that would be needed. It can make more complex behavior easier to implement because it is fully integrated into and throughout your application rather than partitioned and contained. However, this also makes extracting a behavior or function, such as to use in another application, incredibly difficult.

What to do?

The first thing to understand and accept is that a perfect partitioning of complex behaviors into completely discrete modules is not generally possible. Some compromise and interlinking will inevitably be necessary for useful systems.

The next thing to remember is that careful design, partitioning, encapsulating and implementation takes time and effort. Sometimes you simply do not have the time, but you are better served to realize this tradeoff and make informed decisions. That knowledge also can inform your initial design & prototyping of a system. Keep in mind, that it is usually better to start by adding a new module of code to do the 'job'. It is always easy to think, 'I can just add a couple properties and a couple methods to the existing window and it will all work great.' But again our experience tells us that 'a couple properties and a couple methods' will inevitably grow as nuances of behavior must be handled. Then remember that today's new behavior, will be joined by another next week and another next month, each bring 'a few' properties and methods. Before long there will be too many and too much going on to deal with. If you take the time and effort to start it clean and keep it clean, then you may have a chance.

Can You Give Me an Example?

For many years it was my common process to begin adding to directly the 'Application' numerous global properties. The first one was an ApplicationID string which allowed other functions, windows and modules to have a unique application identifier to use which was consistent and only needed to be set once. For example my 'preferences' module needed a unique name to use to locate and store/retrieve the 'preferences' file.  App.ApplicationID filled the bill.

Then I would add, in the application's Open event handler, a call to my preferences module, among others, to configure their own equivalent of the ApplicationID used internally. Other, later modules, would still need an initialization or configuration call at startup, but they might fetch the ApplicationID from the app directly, adding entanglement among the modules.

There is nothing wrong with that approach, but it tends to snowball and eventually 'App' has so many properties and methods that it may be difficult to keep straight what applies to what.  It also endangers understanding where something comes from.  For example, looking at code later, it might seem that App.ApplicationID would be something built-in to Xojo, so where do your set it?  One of the inspector panes?

Again, not that this is terrible, but it is better to either add a 1) create an application subclass with your common properties and methods added, or 2) create a mix-in class which holds these common properties and provides the required methods to use them.

If you go with the first option, you will have linkages between your application subclass and all the other classes. An advantage is that you don't have to consistently and and repeatedly implement the needed properties and methods as they are in your application subclass.  Just bring that class to your new project.

If you go with the second option, then linkages still occur, but between a much smaller and functionally-focused mix-in class (which can subsequently have sub-classes to add even more functionality)

Of course, some combination often makes sense in the larger picture and where the partitions are best placed is unique to circumstances.

Can You Give Me a Concrete Example?

I can do that. A commonly desired feature of almost any reasonably serious or mature program is a 'Window' menu. There are various implementations, including one in Xojo's provided examples. However, see my separate article on my implementation along with the WHY behind each design decision.