.. post:: Nov 26, 2023 :tags: programming :author: Philipp Beisel Data Oriented Programming Practice - 1 ================================================== The last post concluded with an example. I now will refactor it to satisfy the style that I have found to suit me well. In fact, since I stopped thinking in an *object oriented* way and started to explore this approach - we *could* call it *data oriented* programming - I think I became a more effective and also a happier programmer. After we refactored it, actually using (i.e. calling) the code looks like this: .. code-block:: c++ ... int main() { { using namespace ASCIIMapping; Data data{}; data.params.width = 50; data.params.height = 20; run(data); } } ... And the refactoring is very simple, we just put everything into a namespace that describes what the code is doing - in our case *ASCIIMapping*. There we create a struct named *Parameters*. Then a struct named *Data* which has one variable of type *Parameters* and additionally all the data that we need for the code to run. And finally a function called *run()* that takes a reference to a *Data* instance: .. code-block:: c++ #include #include #include namespace ASCIIMapping { const size_t mpN = 6; struct Parameters { size_t width{}; size_t height{}; std::array mapping = {' ','.',':','o','=','@'}; }; struct Data { Parameters params{}; std::vector inputMem{}; std::vector outputMem{}; }; void run(Data & on) { auto width = on.params.width; auto height = on.params.height; //input data on.inputMem.resize(width * height); //fill with some values for(int hI = 0; hI < height; hI++) { for (int wI = 0; wI < width; wI++) { on.inputMem[hI * width + wI] = std::sin(10.0f * 3.1415f * (float)wI / (float) width) * std::sin(12.0f * 3.1415f * (float)hI / (float) height); } } float * input = on.inputMem.data(); //output data size_t outputSize = width * height + height; on.outputMem.resize(outputSize); char * output = on.outputMem.data(); //map to ascii art for(int hI = 0; hI < height; hI++) { for(int wI = 0; wI < width; wI++) { auto inputIdx = hI * width + wI; auto outputIdx = hI * (width + 1) + wI; output[outputIdx] = on.params.mapping[ ( std::max(0.0f, std::min(1.0f, input[inputIdx])) + 0.05f ) * (mpN - 1) ]; } output[hI * (width + 1) + width] = '\n'; } //print fwrite(output, sizeof(char), outputSize, stdout); } } This approach might appear old-fashioned to some. I agree on that, maybe sometimes the old way is the better way? Why call it *data oriented* ? ----------------------------- Because it emphasizes the separation between functions and the data they operate on. In object oriented programming, an object would encapsulate its data and offer methods to manipulate or handle the data. I have tried to use this approach for quite some time now, and I apparently just happened to mess up almost every time. The separation of data and functions clears the mind and helps me focus to actually solve the problem, instead of debating why the name of this class is inappropriate, or which design pattern to use in that case. Note on namespaces ------------------- I have found namespaces to be very useful in this context: I put all the *meaning* of what the code does into the naming of the namespace. This method allows for having a struct simply called *Data* and one called *Parameters*. Namespaces allow us to set up the context. We can then use the created namespaces conveniently with the *using namespace* directive if we want to. But we can also be very specific and give the whole chain of namespaces to make sure the reader knows what we are talking about. In the next post I would like to present more beneficial properties of the given approach.