In a previous project that I worked on, we had to connect to backend API using Qt framework in order to download some files and display some content of these files, this thing might look easily done using QNetworkAccessManager and QNetworkReply. But we had other things to consider:
Size of the downloaded files was large.
No file should be stored in the device running the application.
Backend
I will be using Flask as a dummy backend, I want to keep things simple by just adding tow routes, /api/files to get a list of files to download, and /api/download/file/<variant> to download a file, the dummy_backend.py file will be:
fromflaskimportabort,Flask,jsonify,send_fileapp=Flask(__name__)DUMMY_FILES=dict({"File1":{"summary":"Summary text of file 1"},"File2":{"summary":"Summary text of file 2"},"File3":{"summary":"Summary text of file 3"}})@app.route("/api/files")deflist_files():returnjsonify(DUMMY_FILES)@app.route("/api/download/file/<variant>")defdownload_file(variant=None):ifnotvariantinDUMMY_FILES:abort(404)returnsend_file(f"./dummy_files/{variant}.txt",as_attachment=True)if__name__=="__main__":# debug=True will cause flask to send detailed exception traceback in
# the response body. this is useful when trying out requests from a browser
app.run(host="0.0.0.0",port=3000,debug=True)
Also there will be dummy_files directory with three files called File1.txt, File2.txt, and File3.txt.
If you are familiar with Qt you would know that this is just a simple process, we have just created QNetworkAccessManager instance to make api calls, returned QNetworkReply and just connected to finished signal.
Adding Proxy pattern using signals and slots (Bad approach)
By definition, Proxy is a structural design pattern that lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.
In our case, proxy will be used both before and after the request gets through to the backend API, inside our proxy we will first check if this file was downloaded before, if it wasn’t then call the end point, store the received data and next time return it.
But things here aren’t as simple as you think, what should you store? You might think it’s the received QNetworkReply. However there are many restrictions on this:
finished signal won’t be emitted if you use the reply, because this signal is emitted only once.
QNetworkReply is a sequential-access QIODevice, which means that once data is read from the object, it is no longer kept by the device. It is therefore the application’s responsibility to keep this data if it needs to.
Solution to this problem would be to use signals and slots. APIHandler class would now listen to finished signal, and emit different signals with the content read from the reply. To do so we will add three signals to APIHandler class, these signals will be:
api_handler.cpp will now look like this:
Now it’s the proxy responsibility to connect to these signals, and forward then to gui, Let’s create APIProxy class. api_proxy.h will look like this:
api_proxy.cpp will contain:
APIHandler now will read data and emit signals, GetFilesList() will become:
Finally mainwindow.cpp will now listen to signals from APIProxy, it will look like this:
We can now see that the end point is called only once, therefore files are downloaded only once.
Too many signals, in our project we had about 30 api endpoints, and it was very painful to add all these signals.
You call a function somewhere, and the slot is in another place, I mean in our case we are calling GetFilesList() in the constructor, but the code related to slot is somewhere else.
You don’t actually what part of your code called the function responsible for the slot, let’s say you want to download some file and display parts of it in some case, in other case you want to do something with it’s contents. You have to add some thing to specify who made the call, and this gets very smelly and hard to track.
Let’s see the next approach.
Adding Proxy pattern using APIReply class (Better approach)
We will add APIReply class as a wrapper for QNetworkReply it will be a class with the exact same methods and signals that we are using in our application, and now we will connect to signals from our APIReply class, similar to our first use case without proxy, let’s jump to the code. api_reply.h will contain:
api_reply.cpp will contain:
APIReply class contains two constructors, one that takes QNetworkReply, connects to finished signal, reads the data and stores it locally, and another constructor that takes the previously read QByteArray and emits the finished signal immediately. This way in mainwindow.cpp we can use exactly the same way as our implementation without proxy.
In this article we went through describing the problem of using Proxy design pattern with QNetworkReply, we have shown two different ways to solve the problem, one is complex and smelly, the second one is more clean and much better.
I hope you liked this article, please stay tuned for more.