Week 4 — Refactoring Codes & Software Architecture
Code Refactoring & Cleaning
This week, I refactored my codes regarding to the print transaction flow.
Here are some examples of what I did.
Refactoring Status Codes to Communicate with MS
First, I made the status codes without really think about what might happen with the Microservice, and not communicate it well with the developer. Hence, my status code is ‘meh’. Then, it just got worsened by my fellow developer, which added PROCESSED status code after DONE. This is definitely not intuitive.
So, I refactored it. I made sure that the codes are intuitive enough to read, such as -1 for ERROR, and everything else is sequential. This also implemented the boy scout rules, meaning, leave the place cleaner than when you arrive.
Other than refactoring the implementation, we also need to refactor the tests, to make sure the code still functions after the refactor. This includes adding the new fields, adjusting to the new object attributes, etc. These tests need to be solid, and self-checking, so we didn’t waste time just to check them ourselves, and possibly find bugs in our old codes. (At least this is what I got from Martin Fowler)
One example of a major refactor I did was flattening Transaction and PrintHistory object into one, PrintTransaction. This is done because initially, I thought these two objects were to communicate with different consumers, Transaction is to microservice as PrintHistory is to frontend, and later discover that these objects are better flattened, to improve database queries, reduce coupling, and easier to maintain. These two objects can also be categorized as “Lazy Class” by Fowler, because Transaction object turned out not being able to serve its purpose without PrintHistory, it is dependent which made it kinda useless on its own.
First, I moved my tests. I adjusted the tests to the new model, altered the necessary fields, and renamed the tests to match with the name of the new model, view, and serializers.
Then, I refactor the whole PrintTransaction model. I deleted the old ones, and created a model consisting of all fields from both old models. Then, I adjusted the serializers and the views according to the model.
Despite the huge revamp, this should still be considered refactoring. How? It does not change how the backend communicate with the microservice nor the client, except a few status code adjustment above. We still provide the same endpoint to the API consumers, requested the same fields, and provided the same response.
Errors and Exceptions Handling
Most errors inside the backend are handled by Django Rest Framework middleware, so we do not have to implement them ourselves. However, I do understand that there are ways to implement exception handling in custom way, which is by adding a custom middleware.
This middleware catches whenever a code raises a certain error, and returns a DRF Response object, with a message and status code. We also need to list in in Django project settings, to register some sort of listener throughout the app.
We don’t implement these custom middleware because the provided Django middlewares are sufficient to handle exceptions in our projects. Some errors we raise, and believe that Django handles are:
- ValidationError() —raised by Serializer classes when a field is not validated, returns 400 Bad Request with the details of the validation errors
- AuthenticationError() — raised by Authentication classes when all forms of authentication fails to authenticate a request, returns 401 Unauthorized
- Http404() — raised by get_object_or_404 method if an object is None or not found, returns 404 Not Found as a REST Response
and many others.
Clean Codes as Explained by Guest Lecturers
- Use linting tools — we do, we use pycodestyle for our linting tools, to make sure our code is indented correctly to improve readability
- Make each method maximum of a screen length — I always try to keep my methods under 30 lines, which can be seen on my Gitlab account
- Make your code self-explanatory — I always try to use variable and constant names as descriptive as possible, and keep them consistent throughout the app (such as naming all transaction-related objects with PrintTransaction prefix all over the models, serializers, views)
- When your code is self-explanatory, comments are unnecessary
Design Pattern inside Println
In Println backend, we use Django REST Framework. DRF is using Model-View-Serializers pattern. As much as it sounded like MVC pattern, these two are not related.
- Template Pattern: we use ViewSets to help defining our views to the consumers, as ViewSets provide some methods for us to override, such as: retrieve, list, create, update, partial_update, destroy. We do implement many of them in our ViewSets, like these:
- Observer Pattern: as explained above, many Django classes observe and listen to our signals, such as errors, or when a certain event happens, every subscribing classes or methods are notified automatically. An example of these is Middleware.
- Facade Pattern: Serializer can be considered as a facade to the Model. This allows Views to perform certain actions to the Model, without actually modifying the Model itself. We do implement Serializer, such as its create, validate, and update functions.
There are so many other patterns, like Object Relational Structural Pattern, implemented in Django, and the list goes on and on, so I decided to only explain three of them.
Software Architecture
A software architecture is defined as a structure of a software system, including the software elements itself and the relations between elements.
There are many architectural patterns in software development, such as monolithic (where one app handles all), client-server (server provides a resource, client requests it), and one of the most well-known today, also implemented in Println, is microservices architecture.
Microservices architecture is a software architecture based on Service-Oriented Architecture (SOA), which divides its software into several small loosely-coupled services. Each service is modular and only responsible for a certain task domain. Currently, we have only one microservice, which is the print job service. This service is responsible for handling print jobs coming, printing them to the printer attached to its computer, and updating the status of the job.
Our implementation of microservices architecture is not yet perfect, since our backend app is not yet separated into small services. This is because in our current stage of development, we have yet to see that separating the codes are worth the time, and our application is still small. However, in large application systems, such as Tokopedia, Traveloka, etc., applications are separated into several services with specialities, such as payment, account, bookings, etc.
Other than microservices architecture, Println also seems to implement client-server pattern, because resources in the user are accessed via front-end, requested to the back-end. Our implementation of REST APIs also supports this argument. REST API helps us to separate concerns on front-end and back-end, so that client and server can communicate with low coupling, which improves the maintainability of Println.