To many times, I see or hear people creating complex solutions for problems. Even worse, creating additional complexity to solve problems they have created with their overly complex initial solution. In this short article, I want to try and share my "Opinion" about simplicity in our code and projects. I am undertaking the development of a few projects at the moment, and each complex in their own way, I want to solidify my idea of simplicity and apply it to these projects. Let's jump into it.
Currently I am working on a handful of projects, each presenting unique challenges to my idea of simplicity. To name one in particular
- Helios: Rising - Game, This is a game and engine developed in C using the raylib framework/library. It is intended to be a familiar experience for gamers who have played the "Command & Conquer: Generals" games. I am joined by Fredrick Johansson of https://loopaware.com for this project. And, hopefully in the future a few additional bright minds.
How something complex can be simple.
Games are super complex. In a single piece of software, we need to simulate interactions of entities (objects) in a real or semi-real time, add interaction from a player, manage 2d or 3d objects spatially, hopefully rendering them 60 times per second, play audio and sound synchronized to input, interactions, and other types of events, and potentially allow multiple people running copies of this software on other computers the ability to join in. Games are ridiculously complex! But, does that mean our solutions need to be equally complex?
I really don't think a solution should be as complex as the problem, or it isn't actually a solution
I am quoting myself, talking to myself there. I really like what I am trying to say though, even if it doesn't come across so clearly.
In the game code this can be achieved by using simple ideas, that don't exactly address the problem at hand, but fix the problem at its root. Here is the most common, easily fixed is the complexity of compiling the program. I have seen all sorts of crazy in project compilation. We have hundreds of build systems, because developers want to simplify the process of defining a build script, instead of fixing the project's layout or requirements. Developers also create crazy "Distributed" systems, and complain about software they call "Monoliths". However, this is more a problem of their understanding of distributed computing and not the problem of "Scalability" or "Fault Tolerance" as they like to say it is. It is entirely possible to build a Monolith that is both scalable and fault tolerant, without all the complexity of this microservice stuff. We can tackle some of these specifically.
To create stable software, you need to know exactly what code is in your program, and that the instructions cannot change in an unpredictable way. This goes against the very nature of the modern use of Dynamic Libraries. A DLL or SO or DYLIB, are all ways modern developers like to push the responsibility of they program onto other developers. This is not the true or original use of the Dynamically Loaded Library! The dll was originally made to reduce the amount of memory consumed by having multiple applications using the same library. AND further allowed developers to not need to compile and link code repetitively on slower machine, quickening the development cycle (Code, Build, Run, Crash, Repeat)
Modern developers' idea of a DLL is that another person or company is responsible for the code in there, and if they make a bug fix, it can be downloaded to the target machines, without the application developer having to do anything. The next time the application loads, the OS will find the newer version of the library and use it, providing any benefits from the latest release. While enticing, this is a very problematic approach to development. Likewise, in your build process: you should not rely on third party libraries binary distributions or repos outside of your codebase. This is a recipe for disaster later, if you need to go back to your project, and modify it.
All third-party code should be in a directory structure under your project's directory structure. In many of my projects, you will find a 3rdparty directory at the root level of the repo. This give me two main benefits in one.
- I am compiling the code in my build process, simplifying the process of compiling for multiple platforms.
- You are guaranteed a specific version of the library every time you build your program. Regardless of any updates or API changes to the library in later releases. (You can still choose a Dynamic Library with this structure, but I recommend against that)
Finally, this simplifies setting up builds in new environments, or just new machines. Making it simple to compile without finding you have 30 missing dependencies, that are no longer maintained by your new linux distro, or they don't have to old version of the headers needed, or on windows, you are unable to find any copy of the precompiled dll or static library .a or .lib that you need.
Trust me, it saves time in the long run. And how many times, is your program actually going to benefit from 3rd-party dll updates vs causing headaches for you and your users?
Monolithic Design Vs. (Polylithic?) Microservice Design
In a monolith you usually have a single process which can be threaded but is usually not a Concurrent or Distributed program (except in a Client/Server design). The haters will say "This isn't scalable", "Monoliths hinder teams with cross-cutting responsibility in the code", "Monoliths Slow Development of Programs", "Monoliths have to be developed in a single language regardless if it is the right fit for the specific need of that piece of the application". All of which are quantifiably incorrect.
You can write and do develop monolithic code in multiple languages, even if you don't know it. If you are writing a game using Unity, or C++ and use Lua or SDL in your C++ game. You have just developed in multiple languages in a monolith. In Unity you develop in C#, but the engine is mainly written in C++. With SDL and Lua being written in C99 and you developing a game in C++, you are benefiting from multiple languages. (AND YES, C and C++ are two languages that are Very Different)
Programs can scale and be monoliths. And, they can have "Horizontal Scaling" at that. This is called Concurrency and is possible in a single codebase, without creating a million and a half little services, that together act like a single application. You can literally develop you application to handle transactions entirely, and then allow them to distribute transactions across multiple instances. Or, allow each instance to know which transactions it is entirely responsible for. This can even work similar to the beloved Hyped Up Microservice Architecture.
Part 1 Conclusion
I think that is quite enough for this article. I will however, continue this with some concrete examples of code that is simple, to solve complex problems.
Until next time, keep on coding and simplifying your codebase. Please remember throwing code at a problem is usually not a good or sustainable solution.