Earning Maintainability badge
Writing long lines of code can be a simple task depending on the use case, but maintaining the code can be really hard. Debugging, refactoring, extending and improving it will be difficult if it is not properly maintained.
Every day, new software is developed and deployed. As requirements change, software must be updated. To update or implement new features in the software, it must be properly maintained (i.e. organized). This is the crux of the maintainability badge.
Common pitfalls while writing maintainable code
- The code is not organized logically.
- Code is duplicated (i.e. it violates the DRY principle).
- All the code rests in a single file.
- LOC is not evenly distributed across all files.
- The code has nested if and for statements.
- The code contains complex methods.
Refer details and examples below.
The code is not organised logically
When all code is in one place, it is very hard to maintain. Therefore, it is critical to keep folder structures intact. There are two well-known methods for organizing code efficiently.
- Stack style- separate modules at various layers of the tech stack
- Entity style - As logical boundaries become clearer, refactoring becomes easier.
Fig1. stack style
Fig2. entity style
Code is duplicated
One of the core principles of writing maintainable code is Don’t Repeat Yourself(DRY). Duplication makes it difficult to maintain code. If you need to change some functionality, you must find all of the places where the code has been duplicated and change it there. This is both time consuming and error prone.
Example for code duplication:
Bad code:
public class SettingPanel { public UserContext userObj; public SettingPanel(UserContext currentUser){ this.userObj = currentUser } public void addUser(User newUser){ if (this.userObj.getRole() == "ADMIN"){ /* Add new user **/ }else{ /* throw exception ***/ } } public void deleteUser(String userId){ if (this.userObj.getRole() == "ADMIN"){ /* Delete that user **/ }else{ /* throw exception ***/ } } <br>
Here, we can see that the piece of code that checks if the role is an admin is duplicated throughout this class.
It can instead be rewritten as:
public class SettingPanel { public UserContext userObj; public SettingPanel(UserContext currentUser){ this.userObj = currentUser } private boolean isAdmin(){ if (this.userObj.isAdmin()){ return true; } return false; } public void addUser(User newUser){ if (this.isAdmin()){ /* Add new user **/ }else{ /* throw exception ***/ } } public void deleteUser(String userId){ if (this.isAdmin()){ /* Delete that user **/ }else{ /* throw exception ***/ } } <br>
There are complex methods in the code
To ensure code has correct logic, developers often end up writing a large number of complex conditional statements and looping statements. This makes debugging code extremely difficult.
Example of complex code:
public class TheTieBreaker { public static void main(String[] args) { // contains players of the team HashMap < Integer, Player > team1 = new HashMap < Integer, Player > (); team1.put(0, new Player(0, "Kirat Boli", 5, 10, 25, 10, 25, 1, 14, 10)); team1.put(1, new Player(1, "N.S Nodhi", 5, 15, 15, 10, 20, 1, 19, 15)); HashMap < Integer, Player > team2 = new HashMap < Integer, Player > (); team2.put(0, new Player(0, "DB Vellyers", 5, 10, 25, 10, 25, 1, 14, 10)); team2.put(1, new Player(1, "H Mamla", 10, 15, 15, 10, 20, 1, 19, 15)); // contains list of teams List < HashMap < Integer, Player >> teams = new ArrayList < HashMap < Integer, Player >> (); teams.add(team1); teams.add(team2); teams.get(0).get(0).setStrike(true); teams.get(1).get(0).setStrike(true); // contains the total score of each team int[] teamScores = new int[2]; // commentary StringBuffer[] commentary = { new StringBuffer(""), new StringBuffer("") }; Ball ball; int ballCount = 0; // loops 2 times, for each team for (int i = 0; i < 2; i++) { List < Ball > balls = new ArrayList < Ball > (); // loops 6 times, 1 over for (ballCount = 1; ballCount < 7; ballCount++) { ball = new Ball(); ball.setCount(ballCount); PlayerAndBall ballOutput = null; if (teams.get(i).get(0).isStrike()) { ballOutput = Methods.getBallOutput(ball, teams.get(i).get(0)); ball.setScore(ballOutput.getBall().getScore()); ball.setPlayerId(ballOutput.getBall().getPlayerId()); balls.add(ball); teams.get(i).get(0).addBall(ball); if (ball.getScore() == -1) { teams.get(i).get(0).setOutStatus(true); commentary[i].append("0." + ballCount + " " + teams.get(i).get(0).getName() + " gets OUT.\n"); break; } else { teamScores[i] += ball.getScore(); commentary[i].append("0." + ballCount + " " + teams.get(i).get(0).getName() + " scores " + ball.getScore() + " runs.\n"); } // changes the strike of the batsman depending upon the runs if (!ballOutput.getPlayer().isStrike()) { teams.get(i).get(0).setStrike(false); teams.get(i).get(1).setStrike(true); } if (i == 1 && teamScores[1] > teamScores[0]) break; } else if (teams.get(i).get(1).isStrike()) { ballOutput = Methods.getBallOutput(ball, teams.get(i).get(1)); ball.setScore(ballOutput.getBall().getScore()); ball.setPlayerId(ballOutput.getBall().getPlayerId()); balls.add(ball); teams.get(i).get(1).addBall(ball); if (ball.getScore() == -1) { teams.get(i).get(1).setOutStatus(true); commentary[i].append("0." + ballCount + " " + teams.get(i).get(1).getName() + " gets OUT.\n"); break; } else { teamScores[i] += ball.getScore(); commentary[i].append("0." + ballCount + " " + teams.get(i).get(1).getName() + " scores " + ball.getScore() + " runs.\n"); } // changes the strike of the batsman depending upon the runs if (!ballOutput.getPlayer().isStrike()) { teams.get(i).get(1).setStrike(false); teams.get(i).get(0).setStrike(true); } if (i == 1 && teamScores[1] > teamScores[0]) break; } } } // printing the match result if (teamScores[0] > teamScores[1]) { System.out.println("Lengaburu won by " + (teamScores[0] - teamScores[1]) + " runs"); } else if (teamScores[0] < teamScores[1]) System.out.println("Enchai won with " + (6 - ballCount) + " balls remaining."); else System.out.println("TIE AGAIN."); // printing players scores System.out.println(); System.out.println("Lengaburu"); for (int i = 0; i < teams.get(0).size(); i++) { if (teams.get(0).get(i).isOutStatus()) System.out.println(teams.get(0).get(i).getName() + " - " + teams.get(0).get(i).getTotalScore() + " (" + teams.get(0).get(i).getBalls().size() + " balls)"); else System.out.println(teams.get(0).get(i).getName() + " - " + teams.get(0).get(i).getTotalScore() + "* (" + teams.get(0).get(i).getBalls().size() + " balls)"); } System.out.println(); System.out.println("Enchai"); for (int i = 0; i < teams.get(1).size(); i++) { if (teams.get(1).get(i).isOutStatus()) System.out.println(teams.get(1).get(i).getName() + " - " + teams.get(1).get(i).getTotalScore() + " (" + teams.get(1).get(i).getBalls().size() + " balls)"); else System.out.println(teams.get(1).get(i).getName() + " - " + teams.get(1).get(i).getTotalScore() + "* (" + teams.get(1).get(i).getBalls().size() + " balls)"); } // printing commentary System.out.println(); System.out.println("Commentary"); for (int i = 0; i < commentary.length; i++) { if (i == 0) System.out.println("Lengaburu innings:"); else System.out.println("Enchai innings:"); System.out.println(commentary[i]); } } }
In the above example, we can see a lot of conditional statements, which increases the size and complexity of the code - especially when we need to add more logic in the future. However, by following design patterns, we can organize the code to avoid conditional statements.
The above code can be rewritten as:
public class TheTieBreaker { private Team batting; private Team bowling; private Match match; public TieBreaker(Team batting, Team bowling) { this.batting = batting; this.bowling = bowling; match = new Match(batting, bowling); } public void playInning(int targetScore, int maxOvers) { int score = 0 for (int over = 0; over < maxOvers; over++) { int runs = this.match.simulateOver(score, over, maxOvers, batting.OnStrike()); score += runs; } printResult(score, targetScore); } public void printResult(int score, int target) { // logic to decide what need to be the result System.out.println(result); } } public class Match { // init state variables // constructor etc public int simulateOver(int currentScore, int currentOver, int maxOvers, Player player) { // logic to simulate an over printCommentary(ball, ballScore, player, currentScore); // do something return runs; }} <br>
Code has nested if and for statements
Nested for and if statements in the codebase makes it difficult to troubleshoot any issues. This can be avoided by using proper data structures and design patterns.
LOC is not evenly distributed across files
While developing an application, the codebase is often organized in different files for better maintainability. But one trap that developers fall into is concentrating the majority of their logic in one or two files. While it is not necessary to have all files with the same LOC, it is advised to distribute your code more evenly across different files. Doing this makes it easier to find and modify specific parts of the code, which can save time and effort when working on a project.
All the code rests in a single file
It is generally recommended to write code in multiple files instead of just one. Using multiple files can make it easier to manage and maintain your code. When your code is organized into multiple files, you can make changes to individual files without having to dig through a large and complex codebase. This can make it easier to fix bugs and add new features to your program.