Earning Object Modelling (OOPS) badge
This article gives you pointers on how to write well modelled code.
Object Modelling(OOPS) overview
Identifying the right objects, the data and behaviour associated with them is first step towards writing good OOPS code. See links below to read more on the basic concepts of OOPS.
Java example | Python example | NodeJS example
The usual suspects - common pitfalls while writing OOPS code
Summary of pitfalls:
- Writing code that is procedural
- Classes created without behaviour or very less behaviour (Value Objects)
- Classes that don't represent the real world
- Wrong behaviour in the class
- Breaking encapsulation
- Breaking law of delimiter
- Usage of static methods
- One class doing too many things - SRP
See details and examples below.
1. Writing code that is procedural
Procedural code
package cricket; import java.util.Random; public class Runs { static int striker = 0; static int ball_arr[] = new int[5]; static int[] original_runs = new int[5]; public static void main(String[] args) throws InterruptedException { int total_runs = 0; int target = 40; int wickets = 0; Random r = new Random(); boolean wicket = false, flag = true; int over = 0, over_left=4; int ball = 0; int remaining_ball= 24; String[] original_players = { "Kirat Boli", "NS Nodhi", "R Rumrah", "Shashi Henra" }; String[] players = { "Kirat Boli", "NS Nodhi", "R Rumrah", "Shashi Henra" }; String[] delivary = { "yok", "bouncer", "os", "in", "ls", "offspin", "topspin", "doosra", "carromball" }; while (total_runs<=target && wickets < 3) { if (over < 4) { ball++; remaining_ball-=1; if (ball >= 6) { ball = 0; System.out.println(--over_left+" overs left."+target+" runs remaining."); over++; swap(players); } int num = r.nextInt(8); if (delivary[num] .equals("yok") || delivary[num] .equals("topsppin") || delivary[num] .equals("doosra") || delivary[num] .equals("carromball")) wicket = r.nextBoolean(); if (wicket == true) { wickets++; wickets(wicket, flag, players); ball++; remaining_ball-=1; wicket = false; } int run = r.nextInt(6); if (run % 2 == 0) { //another implementation when run is odd } else { //another implementation when run is odd } } else break; } if ((target - total_runs)< 1 && over < 4) { System.out.println("Lengaburu won by " + (players.length-1-wickets) + " wickets and "+remaining_ball+" balls remaining"); } else { System.out.println("Enchai won by " + (target-total_runs) + " runs and "+remaining_ball+" balls remaining"); } System.out.println("Score card"); for (int i = 0; i < original_players.length; i++) { System.out.println(original_players[i] + ": " + original_runs[i] + "(" + ball_arr[i] + ")"); } } private static void wickets(boolean wik, boolean flag, String arr[]) { if (wik == true) { if (flag == true) { //some implementation to get wickets } else if (flag == false && striker < 2) { //another implementation to get wickets } } } private static void swap(String player[]) { String temp; temp = player[striker]; player[striker] = player[striker + 1]; player[striker + 1] = temp; } }
Same code written in an OOPS manner. This code doesn't implement the entire logic, but gives a guidance to identify the right domain models, states and behaviours.
class Game { private Team team1; private Team team2; public Game(Team team1, Team team2) { this.team1 = team1; this.team2 = team2; } public static void main(String[] args) { List<Player> lPlayers = //initializes lengaburu players List<Player> ePlayers = //initializes enchai players Team lengaburu = new Team(lPlayers); Team enchai = new Team(ePlayers); Game game = new Game(lengaburu, enchai); game.play(); } public void play() { Innings lengaburuBattingInnings = new Innings(team1, "batting"); lengaburuBattingInnings.batting(); } } class Innings { private Team team; Innings(Team team, String type) { this.team = team; this.type = type; } public void batting() { Batsman striker = team.getBatsmans(0); Batsman nonStriker = team.getBatsmans(1); for (int i = 0; i<overs; i++) { for (int j = 1; j<7; j++) { int runs = striker.bat(); //update runs or wickets //update score //update commentary and all the rest of logic } } } } class Team { private ScoreCard teamScore; private List<Player> players; Team(List<Player> players) { this.players = players; } public void updateScore(int runs, int wickets) { this.scores.scoreRun(runs); this.scores.scoreWicket(wickets); } public getPlayer(int order) { return players.get(order); } } enum BatsmanState { bATTING, OUT, RETIRED, DNB } class Player { private String name; Player(String name) { this.name = name; } } class Batsman extends Player { private int[] probability; private ScoreCard scores; private BatsmanState state; Batsman(String name, int[] probability) { super(name); this.probability = probability; } public int bat() { int r = 0; //find the random runs return r; } public void updateRuns(int runs) { this.scores.scoreRun(runs); } public void out() { this.state = OUT; } } class ScoreCard { int runsScored; int wicketsTaken; public void scoreRun(int runs) { this.runsScored = this.runsScored + runs; } public void scoreWicket(int runs) { this.runsScored = this.runsScored + runs; } }
package cricket; public class Batsman { private String name; private int[] probabilities; public Batsman(String name, int[] probabilities) throws IllegalArgumentException { this.name = name; this.probabilities = probabilities; } public String getName() { return name; } public int[] getProbabilities() { return probabilities; } }
These kind of classes are OK when you are writing a Hibernate application or a Web application where you need to marshal and unmarshal data to and from external entities like a DB or HTTP Request. However in a proper OOPS context a class should have adequate properties and behaviour. The above class is now refactored to have a play behaviour, in which the new states - runsScore & ballsPlayed - are modified.
package cricket; public class Batsman { private String name; private int[] probabilities; private int ballsPlayed = 0; private int runsScored = 0; public Batsman(String name, int[] probabilities) throws IllegalArgumentException { this.name = name; this.probabilities = probabilities; } public void play(int ball) { // Logic to play a ball int runs = //get run from probabilities runsScored += runs; ballsPlayed++; } public int getBallsPlayed() { return ballsPlayed; } public int getRunsScored() { return runsScored; } public String getName() { return name; } public int[] getProbabilities() { return probabilities; } }
package com.cricket.model; import java.util.ArrayList; public class MatchLost implements IGameResult { private String teamName; private int runs; private ArrayList<Player> playedPlayer; private ArrayList<String> resultScore; private ArrayList<Player> notOutPlayers; public MatchLost(String teamName, int runs, ArrayList<Player> playedPlayers, ArrayList<Player> notOutPlayers) { this.teamName = teamName; this.runs = runs; this.playedPlayer = playedPlayers; this.resultScore = new ArrayList(); this.notOutPlayers = notOutPlayers; } public String getGameResult() { return teamName + " lost by " + runs + " runs"; } public ArrayList<String> getScoreCard() { for (Player player: playedPlayer) { String ball = (player.getNumberOfBalls()<1) ? " ball " : " balls "; resultScore.add(player.getPlayerName() + " - " + player.getCurrentScore() + " (" + player.getNumberOfBalls() + " " + ball + " )"); } for (Player player: notOutPlayers) { String ball = (player.getNumberOfBalls()<1) ? " ball " : " balls "; resultScore.add(player.getPlayerName() + " - " + player.getCurrentScore() + " *" + " (" + player.getNumberOfBalls() + " " + ball + " )"); } return resultScore; } }
package com.cricket; import com.cricket.model.*; import com.cricket.util.WeightedRandomGenerator; import java.util.ArrayList; class CricketMatch { private String teamName; private WeightedRandomGenerator scorer; private CricketField cricketField; private final ArrayList<Player> linedUpPlayers; private final ArrayList<Player> playedPlayers; private final ArrayList<Player> notOutPlayers; CricketMatch(String teamName, WeightedRandomGenerator scorer, CricketField cricketField, ArrayList<Player> linedUpPlayers) { this.teamName = teamName; this.scorer = scorer; this.cricketField = cricketField; this.linedUpPlayers = linedUpPlayers; this.playedPlayers = new ArrayList(); this.notOutPlayers = new ArrayList(); } public IGameResult play() { //method implementation } public boolean isGameEnded(IGameResult updatedGameResult) { return updatedGameResult != null; } public IGameResult playOver(int overNumber) { //method implementation } public void printPerBallCommentary(int overNumber, int ball, int score) { System.out.println(cricketField.perBallCommentary(overNumber, ball, score)); } public void handleAndPrintWicketCommentary(int overNumber, int ball, String playerName) { System.out.println(cricketField.perWicketCommentary(overNumber, ball,playerName)); } public boolean shouldChangeStrike(int score) { return score == 1 || score == 3 || score == 5; } public boolean isLastPlayersWicket(int score,int overNumber,int ball) { return false; } public boolean gameWonResult() { return cricketField.getTargetScore() <= cricketField.getCurrentScore(); } public void updateScore(int score) { //method implementation } public void changeOfOnStrikePlayerWithLinedUp(ArrayList<Player> linedUpPlayers, ArrayList<Player> playedPlayers) { //method implementation } public void changeStrike() { //method implementation } }
class Game { private Team team1; private Team team2; public Game(Team team1, Team team2) { this.team1 = team1; this.team2 = team2; } public static void main(String[] args) { List<Player> lPlayers = //initializes lengaburu players List<Player> ePlayers = //initializes enchai players Team lengaburu = new Team(lPlayers); Team enchai = new Team(ePlayers); Game game = new Game(lengaburu, enchai); game.play(); } public void play() { Innings lengaburuBattingInnings = new Innings(team1, "batting"); lengaburuBattingInnings.batting(); } }
Code that breaks encapsulation
package com.cricket.model; import java.util.HashMap; public class Player { private String playerName; private int currentScore = 0; private int numberOfBalls = 0; private HashMap<Integer, Integer> playerProbability; public Player(String playerName, HashMap<Integer, Integer> playerProbability) { this.playerName = playerName; this.playerProbability = playerProbability; } public Player(){ } public String getPlayerName() { return playerName; } public void setPlayerName(String playerName) { this.playerName = playerName; } public int getCurrentScore() { return currentScore; } public void setCurrentScore(int currentScore) { this.currentScore = currentScore; } public int getNumberOfBalls() { return numberOfBalls; } public void setNumberOfBalls(int numberOfBalls) { this.numberOfBalls = numberOfBalls; } public HashMap<Integer, Integer> getPlayerProbability() { return playerProbability; } public void setPlayerProbability(HashMap<Integer, Integer> playerProbability) { this.playerProbability = playerProbability; } }
public class Batsman { private String name; private int[] probabilities; public Batsman(String name, int[] probabilities) { this.name = name; this.probabilities = probabilities; } public int play(int ball) { // Logic to play a ball int runs = //get run from probabilities return runs } }
a.getB().methodB() violates this law because object A should not reach object B directly.
Code that 'talks to strangers'.
class Player { private Team team; public Team getTeam(){ return this.team; } } class Team { private Score score; public Score getScore(){ return this.score; } } class Score { private int runs; public void updateRuns(int runs){ this.runs += runs; } } //actual call player.getTeam().getScore().updateRuns(runs)
class Player { private int runs; public void updateRuns(int runs, Team team) { this.runs += runs; team.updateRuns(runs); } } class Team { private int runs; public void updateRuns(int runs){ this.runs += runs; } } //actual call player.updateRuns(10, player.team)
class Game { public static start() { List<Player> lPlayers = //initializes lengaburu players List<Player> ePlayers = //initializes enchai players Team lengaburu = new Team(lPlayers); Team enchai = new Team(ePlayers); Game.play(team1, team2) } public void play(Team team1, Team team2) { Innings lengaburuBattingInnings = new Innings(team1, "batting"); lengaburuBattingInnings.batting(); } }
The same code refactored to use object scope instead of class scope
class Game { private Team team1; private Team team2; public Game(Team team1, Team team2) { this.team1 = team1; this.team2 = team2; } public void play() { team1.batting() } }
Here's a class with the kitchen sink thrown in
class CricketMatch { private String teamName; private WeightedRandomGenerator scorer; private CricketField cricketField; private final ArrayList<Player> linedUpPlayers; private final ArrayList<Player> playedPlayers; private final ArrayList<Player> notOutPlayers; CricketMatch(String teamName, WeightedRandomGenerator scorer, CricketField cricketField, ArrayList<Player> linedUpPlayers) { this.teamName = teamName; this.scorer = scorer; this.cricketField = cricketField; this.linedUpPlayers = linedUpPlayers; this.playedPlayers = new ArrayList(); this.notOutPlayers = new ArrayList(); } public IGameResult play() { //method implementation } public boolean isGameEnded(IGameResult updatedGameResult) { return updatedGameResult != null; } public IGameResult playOver(int overNumber) { //method implementation } public void printPerBallCommentary(int overNumber, int ball, int score) { System.out.println(cricketField.perBallCommentary(overNumber, ball, score)); } public void handleAndPrintWicketCommentary(int overNumber, int ball, String playerName) { System.out.println(cricketField.perWicketCommentary(overNumber, ball,playerName)); } public boolean shouldChangeStrike(int score) { return score == 1 || score == 3 || score == 5; } public boolean isLastPlayersWicket(int score,int overNumber,int ball) { return false; } public boolean gameWonResult() { return cricketField.getTargetScore() <= cricketField.getCurrentScore(); } public void updateScore(int score) { //method implementation } public void changeOfOnStrikePlayerWithLinedUp(ArrayList<Player> linedUpPlayers, ArrayList<Player> playedPlayers) { //method implementation } public void changeStrike() { //method implementation } }
Here is how the same CricketMatch class is implemented with Single Responsibility in it. Class Team is also given to show what it should do.
class CricketMatch { CricketMatch(Team team1, Team team2) { this.team1 = team1; this.team2 = team2; } public void start() { //Implementation of match start } } class Team{ public void startInnings(int inningsNumber) { //Implementation of each innings of the match for a team } } class WeightedRandomGenerator { public int getWeightedRandomNumber(Map<Integer, Integer> weightGraph) { //logic to get the weighted random number } }