Maintainability is a key Architecture Quality attribute of a well-constructed system. Maintainability is the quality attribute that helps other people on your team work on the same project and be able to add value without too much overhead to understand how it all fits together. Collaboration on defining and evolving these quality attributes will significantly affect the quality and velocity of your projects.
Why is this important?
When you get on a project and see the code base, or just look at code you wrote 5 years ago. What is the feeling you get? Does it feel professional, or does it give you a feeling like nobody cared?
Professional quality creates momentum in the team and quality practices become good habits. When teams are developing a product with the same codebase, teammates will bring their own opinions on best practices for code formatting and quality. Everyone has a lot of experience and curiosity, so it's good to use that knowledge to implement some simple practices to allow the team to move faster together, with more quality.
The quality outcome is teammates can make changes easily, and enhancements and fixes flow easily
What are the cures?
Code ownership
It is the professional responsibility of the developer to produce the high-quality code that they can. This is important when developing on your own, but it is essential when developing on a team. The team owns the code and the upkeep of that quality becomes a matter of trust between team members.
Adhere to common standards
The code has formatting and practices from the language itself. These patterns and practices become familiar and reduce the mental overhead of ‘figuring out’ the code when adding enhancements or fixing bugs.
Trust in teamwork
When you are on a pager-duty call at 3am to debug an incident. If the code is easy to read and understand, then the fix is easier to find. Do it for your teammate who is on call this week. This will also help Pull Request reviews as the code is easier to read, by our own convention.
How do I do this effectively?
Agree on the context and goals and automate the tools to fulfill them.
Agree on the concepts and goals, but don’t get lost in the details. If you have been in a code standards meeting, you might run into a situation where a developer is not only advocating to put a squiggly on a new line or leave it at the end. Presenting a higher-level goal of ‘commonly accepted practices’ and getting agreement on those will prevent you and your teammates from getting lost in the details. One thing I have noticed whenever implementing this policy is that the subject didn't return to the conversation. The discussion moved on to higher-level concerns
- Don’t sweat these details, there are more valuable problems to solve.
Automate with github actions
With these checks running locally in every commit, the same commands are run to ensure your new branch integrates with the work on the main branch.
Starting with some simple tools that implement maintainability checks, we can automate these actions to really enable development productivity. Automating small problems helps the developer concentrate on the bigger tasks. It always helps productivity; less time and is more consistent than manual checks.
Automate all of this by adding consistent checks in the CI and CD process
- GitHub actions that do not allow merging until the checks pass
- Using git for doing the tasks
- git is an amazing piece of software engineering for source control and versioning. Also, it will run tasks or 'hooks' to run specific functionality before and/after a commit.
- run pre commit for everything,
- use the same hook to run on the main branch of the repository when merging pull requests
Maintainable Python
Let us use a Python example to show what maintainable checks can be added to your developer workflow to take care of the maintainability and allow collaboration on a higher level
For this exercise, we are going to upgrade an open-source package with the tooling
https://github.com/jseller/python-edi
For Python, we will use the following tools:
- black, flake8, mypy, bandit, vulture
- Run locally with pre-commit
- Run the same checks in our git host actions
- Take a look at this PR as an example. https://github.com/jseller/python-edi/tree/upgrade
- For adding this functionality to a project, I wouldn't do all of this at once, but split this up and run the tools file by file before committing to the whole project
Formatting and Linting with black and flake8
- Common code formatting and structure help team members understand each other's work. This results in a more effective PR that can be reviewed and merged easily.
For the first version, have a code formatting and lint tool. The output is much easier to review when the formatting doesn't need to be understood. Running a linter on your code will always give me more robust code and reduce complexity.
Type safety with mypy
For dynamic languages like Python and javascript, a type-checking action is really handy to increase code quality and make more robust functions and classes. Using type hints, a tool like mypy will check the validity of the variables being used to prevent TypeError exceptions.
Security checks with bandit and vulture
Security holes and unused code can be scanned.
Languages will naturally have conventions
For JavaScript, we have TypeScript now that adds a lot of type safety over top of vanilla js.
Running tools like Prettier and ES lint on legacy javascript code will bring that quality up to a higher level.
Frameworks like React and Angular enforce conventions. Agree with that opinion and move on to focus on building value.
Code Structure
The programming language used to implement the system may use some common conventions, but it's still an application that can be decomposed into a set of features
Clean code that is common and familiar, code structured in files and directories that follow common conventions in structure and naming
Package by layer or feature?
When packaging application code for development, there is the application, and then there is all the artifacts around the application. Data files, unit tests, and different configurations are needed to build the app, and then there is the app code
package by feature
Many applications have a directory structure that reflects the implementation pattern of the system architecture and not so much the functionality it has. Directories like "Model, View, Controller" and "API, Data, Logic" or some variation of the components' structure.
This gets complex as I would have to add and edit files to many different directories to add a feature. The new SearchCatalog feature would need some changes to a file in api, or a new file and so on with the different parts of the pattern. Not all features are implemented with the same pattern, so having a strict structure can cause issues.
Instead, try packaging by the feature itself. In a large system, this looser coupling within the files will help understand the feature as a whole
Let's make a feature called "SearchCatalog" and implement your pattern for the data models, views and logic classes that make the feature work. By better understanding the interfaces, this makes understanding the dependencies a little easier, and can also result in nice shared libraries used by many features.
Packaging and deployment
- Using common packaging has a lot to do with Portability, but the code that does those things needs to have some maintainable characteristics
- package app code
- check versions of dependencies, and automate dependency security checks (GitHub and GitLab do this automatically now)
- update when deprecation warnings are in the logs
- deployment as code with Terraform
- shell scripting can get out of hand ‘to make it work’
Feature Flags
Using the pattern allows an easier addition of Feature Flags as there are fewer places to add them and all the code for a feature is in its own directory. As a result, provides a more elegant way for new code to live in the application.
Design
Consistency in the implementation helps, but is there also maintainable design? There has always been an effort to standardize design with diagramming standards like UML and conventions like C4. The textual design has not found a decent standard that has been widely adopted.
For easier collaboration with QA and documentation (if you have someone doing this or you are doing this yourself), link to specs that created the functionality RFCs and ADRs for design. Not sure that is worth the effort and as long as don't lose your architecture in Jira or an issue tracker
Conclusion
By establishing good habits and automating the application of those standards; a nice safety net is created that allows a flow of increased productivity.
The team's culture is positively affected as the standards are agreed upon and evolved over time. More time is spent adding value to the product, instead of working the codebase.
No comments:
Post a Comment