Thursday, June 15, 2023

Maintainable and Portable codebases increase developer velocity by reducing complexity

When teams are developing 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 its good to use that knowledge to implement some simple practices to allow the team to move faster together, with more quality.

Starting with some simple tools that implement Maintainability and Portability checks; we can automate these actions to really enable development productivity. The collaboration on defining these and evolving these quality attributes will have a significant effect on the quality and velocity of your projects. 


Portability is the ability of your project to work on your local machine, and different testing and production environments

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.

Automation
Automating small problems help the developer concentrate on the bigger tasks. It always helps productivity; less time and more consistent than manual checks. 

Maintainability

Common code formatting and structure helps team members understand each others work. This results in a more effective PR that can be reviewed and merged easily.

For a 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 gives me more robust code and reduces complexity.

Type safety

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. For javascript, we have TypeScript now that adds a lot of type safety over top of vanilla js. 

Automation


Python
  • run pre commit for everything,
  • use the same hook to run on main branch on the repository when merging pull requests

Node Typescript
  • Prettier
  • ES lint 

Java
  • Prettier
  • Java compilation when building the byte code files will do the static type checks due to the nature of the language

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. 

Maintainable Structure

When packaging application code for development, there is the application, and then there is all the artifacts around the application. Data files, unit tests, different configuration are need to build the app, and then there is the app code

YAGNI: Beyond not putting test data into production, it really helps reduce complexity to build the leanest version of the codebase for deploy to production. This helps quality by reducing complexity of the artifacts used to debug when something goes wrong, or to add a new feature.

Maintainable structure is more about the functional nature of the application. The programming language to implement the system may use some common conventions, but its still an application that can be decomposed into a set of features

package by feature

Many applications have a directory structure that reflects the implementation pattern of the system architecture and not so much the functionality that it has. Directories like "Model, View, Controller" and "Api, Data, Logic" or some variation of what the structure of the components are. 
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 team this really helps as the teams are really operating in their own part of the larger system with better understanding of the coupling between them.

Lets 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 that are used by many features.

For an API endpoint that uses some logic to read and write to a datastore; the pattern is a basic robustness pattern 

Responsibilities
  • request/response boundary
    • parameters
    • validate request
    • serialize response
  • logical objects that have behaviour
    • called from boundary object
    • unit test these with objects
  • data entities
    • model and model behaviour
  • workers, caches etc
    • tasks and clients for dependencies

Lets say we make a python flask api, by using these in files called "api.py, search.py, data.py, clients.py" give the reader an easy time to understand whats in the directory and what it does

Test

By having a functional test with an http client you can test the api boundary objects and for any logical classes and components the tests become clearer because they test the functionality.


Portability


When developing an application there is going to be a need to build the app locally, and deploy to your production environment for the users to use the app. There could be testing, staging, and other environments along the way. Enabling some good practices for the configuration of the app makes developing and testing a lot easier and reduces the stress of a deployment.

  • A new person should be able to pull, run tests, start app with just .local.env, and can hook into secrets in a vault when in dev, staging and production environments
  • Keep environment config versions and use the environment to use a specific file.

Dependencies should be mocked when developing locally, or you could end up with some side effects like incurring charges for an api every time you run your unit tests!

Automation

Use pipelines in bitbucket or github to automatically deploy your main branch to a development server, for larger production deployments there is Harness and other tools can deploy to many nodes to reduce complexity of setting up many production instances.

Test it

How do you validate? use a automated test to add a quality step to your deployment scripts. The deploy is finished when its validated, not just deployed

Testing dependencies config

No matter how careful you are about the portability of the app, things are going to happen with many people working on the application. Using more than a developing and production environment will go a long way to ironing out the issues before deploying to production

To go even safer, its a good idea to use your load balancer in production to route to validated instances. Enable this by deploying to a new node and smoke testing the live production instance before adding it to the load balancer. 

Conclusion


By automating some good habits standards, a nice safety net is created that allows a flow of increased productivity. Group culture is positively affected as the standards are agreed and evolved over time. More time is spent adding value to the product, instead of working the codebase. 




No comments:

Post a Comment