When using a design workflow where you are reviewing the code in multiple stages there are two ways you can structure your git branches. The first method is to simply have one branch based off of master and you simply push your changes in chunks; first the header file, then the UTs and then the implementation. The second option is to use integration branches:
Merge branch is created with the intention to merge to master
Header file review is held on "merge_to_master"
Another branch is created which is based off of "merge_to_master" for UT review with the functions stubbed out
A final branch is then based off of the branch created for UT review where you fill in the stubs with the actual code
The pro for using integration branches is that everything is organized and separated into their own branch so the reviewer can simply focus on reviewing whatever is in that particular branch. However, this method also adds complexity when merging the code into master as you have to "reverse merge" from your outermost branch inwards to master. This can be seen in the diagram below:
We decided that the slight increase in organization of changes did not negate the added time a developer would need to ensure that the branches are merged correctly to avoid conflicts and merge issues. Which is why the first option of using a singular branch with chunked pushes is used.
There are many phases in the software development cycle, and we propose consolidated and perhaps a little simplified version of these to promote good software principles.
What if we task you to build a skyscraper? Would you go straight to constructing the foundation with cement without sharing your plans with anyone? We have seen developers dive straight into code upon given a problem, and it has many possible effects:
Developers and code reviewers have no idea what is being built
Increases code coupling (modular code is better)
Developers usually end up creating "The Blob anti-pattern" amongst many other Anti-Patterns
Create a maintenance nightmare
The list really keeps going, but we do not want to bore you...
The rest of this section discusses ideal approach to software development, which is ultimately meant to accelerate development. It is undoubtedly faster to develop code by following proper software development philosophies than to believe that one can cut corners to develop code faster.
The following diagram visually illustrates the developer workflow:
0. Planning and Requirements
Before beginning work on a module, it is important to confirm that there is a desire for it in the first place. The end-result must be adding value or somehow benefit the company/team, time should not be wasted. In a company setting collaborate with sales/planning/business teams to come up with a product roadmap. This roadmap can then be used to breakdown the product into feature requests and from there modules that need to be created. This way time is not wasted on developing a module that is not needed for a particular product/feature.
Sibros uses an in-house Requirements tool that is used to verbally communicate the requirements before the work is started. This ensures that we will be engineering what we really need with specifics of what we are trying to accomplish.
Outcome: A detailed plan is created for each product to determine what features are required. From the desired features necessary modules are planned and then implemented.
Typically, the first step is requirement gathering and analysis. Assuming that you are well aware of what you are tasked upon, we can dive into the design portion. The design of the code is a place to be an artist, and to just sketch things out. This is the step to separate specific functionality into modules because there are several advantages:
Facilitates code re-use
Increases code review efficiency
Promotes divide and conquer strategy
Modular code is easier to maintain
Easier to write code and its unit tests
You may also think about writing README.md file or equivalent to create artifacts that may be useful towards the future. For example, there may not be a need for readme_circular_buffer.md because your code module itself should describe its use, but there may be a desire for a README for more complex components, such as readme_uds_firmware_update_server.md.
After being an artist in code, it is worth the time to then be a visual artist too. Designing flow charts and block diagram to visually display the code design is a great method to iron out your design, explain the design to others, and create meaningful documentation that others will easily be able to digest. It helps to create documentation that can be understood by less technical members of the team too, say PMs or sales teams. Making use of existing standards for visual documentation will help even more in creating understandable documentation. Creating block diagrams that follow UML standards is a common way to do this.
The follow-up to this is transferring these to technical specification documents. These can be for product specs, briefs, presentations, etc. Maintaining a strong technical specifications document that both technically describes the workings of a product as well as visually described them is a vital asset to a company. It helps increase the legitimacy of the work and the company, and help iron out and discover core designs and possible issues.
Outcome of this step should be design diagrams and a solid understanding of what needs to be done. These diagrams should provide strong hints towards designing code modules and interfaces that will be designed in the next step.
Software Coupling and Cohesiveness
Cohesion defines the amplitude to which elements of a class or code module belongs together. Cohesion is the relationship within the module.
High cohesion should be the aim which means that a cohesive component or code module should focus on a single function with little interaction with other code modules.
Low coupling helps create modular code. Coupling is the relationships between the modules. To put simply, a code module should minimize including or referencing multiple other code modules.
This milestone is to design header files or code interfaces that capture your designed artifacts into what will ultimately end up shaping your code. The interfaces design the data input, output and functionality of your code, but should be agnostic to the implementation. This step should only be concerned with the outwards facing interface to other modules. There are many important concepts to bear in mind. One very important one is to not invent APIs that might be "nice to have", as this might violate the YAGNI principle discussed here. The core idea is to not try to define and implement APIs that may seem useful but no-one would ever actually need right now. Not doing this saves time in the design, testing and implementation phase. You would be shocked how much time you save by not over-defining interfaces.
Outcome of this step should be code reviews with your team members such that they understand how the design is turning into code modules.
3. Write the tests (TDD style)
Our Unit-Test article cover the benefits, and the Test-Driven-Development (TDD), so we will not repeat the content here, but in this step, we focus on writing tests that purely designed based on the header files. Your implementation of the code can be purely stubs that do not do anything meaningful at this state. That being said however, it is not always possible to completely ignore implementation at this state. An example of this would be that you do not know all aspects of the module that you need to test until implementing some aspects of it. Due to this, it is okay and often necessary to do write your unit tests and implementation in parallel.
Outcome of this step is unit-test code and to get your test code reviewed. This is important because usually developers would only review the production code that is meant to be deployed, however, reviewing the unit-test code is equally important because even the test code is something that must be maintained well, and we each deserve to teach each other the tips and tricks of testing.
4. Implement and review
This is the last step of proper software development process. After you complete this step, you will definitely reason why this has to be the last step rather than the first step that many take. There are numerous advantages at this stage:
You have only written minimal code to pass your tests (TDD)
Code reviewers are well on-board with your code modules, and the review will be faster
You will not need to re-architect your code, and change unit-tests, and redo things because those things have already been reviewed in prior steps
And yes, the list goes on and on ...
Outcome of this step is the implementation that should be very well modularized. Through the investment of the previous steps, this part of the code review should be relatively straight forward. What helps is the fact that your design has been reviewed, and you will not have to re-write and change things drastically. Even further, since you already reviewed the header files, the code reviewers would have focused just on the implementation which may trivial to address.
4.5. System Test and Validation
This step could also be called Integration Testing. Once the individual module has been developed and unit tested, it is important to then do large-scale system testing. Integrate the module into the the main system and use it with real versions of the other components that will use it (not mocks). This allows you to ensure that the system functions as designed. This generally will reveal issues that are hard to find otherwise, mainly between the connection of the multiple modules. Consider possible issues with race conditions and thread timing that are hard to find in a single threaded environment with mocked dependencies.
Outcome: This step will also help contribute to better performance analysis, as you will get to see how the new module affects the applications system performance. Integration testing is a vital component of development. While unit testing is helpful for guaranteeing the sub-module behaves correctly, integration testing guarantees the system behaves correctly.
Reminders to be more efficient
Follow the established rules
Code to your company's coding standards
Make sure things like copyright headers, and appropriate code template is in place
Anything that can go wrong, will go wrong
If you believe you may have to come back to improve the code, then you will certainly have to come back. Be an efficient developer and solve issues as you go along and do not accumulate technical debt.