More insights and thoughts about finding the proper answer for the above question.
Part 1 ended with my code working, but only partially (2 out of 3 inputs). After an extensive process of refactoring in which I was able to fix one part but to break another, I understood that I had to dismiss my ego and doubt my algorithm implementation, from scratch. It wasn’t easy, but it had to be done.
Reflecting back and challenging my own work, turned out to be the best thing I could have done. I asked myself all over again — am I loyal to the algorithm’s happy flow? Do I see the bigger picture? Do I really need to hold 2 different queues? Am I “sending” the packets in the right place? Answering these questions made me realize that some of my implementations needed to change, both major and minor. Conducting this rollback phase gave me confidence and pointed out exactly what I need to fix and refactor. I even enjoyed the debugging process (which I usually dread).
Finally, all 3 inputs were correct, the runtime from the original project was tremendously faster and memory usage reduced as well.
Before recapping, I wanted to share some specific lessons learned from striving for best-practices (aka perfection) and not just being done from the work on this project.
STL’s queues were the primary reason I decided to implement the project in C++ and not C, they offered the exact functionality I needed. Well, almost exactly. At some stage in the flow, I needed to iterate over the queue, which wasn’t something that you do with queues. When looking for a solution online, I saw mostly answers like “if you need to iterate, you don’t need queues, use lists”, but lists won’t give me all of the queues benefits. I decided to create a function that will take a “snapshot” of the queue as a list, in order to iterate over it, without it affecting the original queue:
When overviewing memory allocations in my code, and ensuring I was “cleaning” after myself, my (great) mentor introduced me to the RAII design pattern and the usage of shared_ptr. I decided to use it and embrace this way of thinking — where there is no current usage of a resource, it should be freed from memory. This also required refactoring and another set of tests, but again, this was what I was looking for, learning new methods and doing things as perfectly as I can.
In order to keep best practices in place, I decided to split my files to give different files (and headers) to each object in my code. You probably think — what’s the big deal? Here where the compiler comes in. I almost quit fighting the compiler that was throwing unclear errors at me, but I was in a “perfect” state of mind. Thanks to my mentor, I conducted this process step by step in order to eliminate the issue and understand what really bothered the compiler. It took time, but I learned a lot about the compilation process, and I can confidently say that compilation errors don’t scare me anymore :)
I was referring to them constantly, and I wanted to share the last drawing of the architecture, which I followed until the finish-line:
Code Architecture, Drawing: me
Is my project perfect? Probably not, there are many other things that could make it better (wiser data structures, compilation optimization, tests and documentation that are missing, etc.) but I meet my goals and it’s done. I think that as a software engineer, I should adopt the approach of always aiming to achieve “perfect”. However this project showed me that this requires time and effort, which in real life, we don’t always have. Is done better than perfect? No, but you define what perfect means.
You can find my code here, would love to hear your suggestions and inputs as it will help me become a better software engineer.