Earning Object Modelling (OOPS) badge

This article gives you pointers on how to write well modelled code.

Object Modelling(OOPS) overview

Object-oriented programming is an approach for modelling concrete, real-world things. It models real-world entities as software objects, which have some data associated with them, and can perform certain functions.

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
We will use a deprecated Geektrust coding challenge to explain concepts. You can see the coding challenge here -> Cricket coding challenge

The usual suspects - common pitfalls while writing OOPS code 

Summary of pitfalls:

  1. Writing code that is procedural
  2. Classes created without behaviour or very less behaviour (Value Objects)
  3. Classes that don't represent the real world
  4. Wrong behaviour in the class
  5. Breaking encapsulation
  6. Breaking law of delimiter
  7. Usage of static methods
  8. One class doing too many things - SRP

See details and examples below.

1. Writing code that is procedural

These code submissions don't apply OOPS concepts at all. Real world objects are not identified. 

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;
  }
}
			
2. Classes created without behaviour or very less behaviour (Value Objects)
Even if classes are identified, associating the right behaviour to the state is core to OOPS. Having classes and then not having any or little behaviour associated with the class is a poor OOPS application.
The above class just encapsulates data, but there is no behaviour associated with it.
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;     
  } 
}
		
3. Classes that don't represent the real world
What classes you identify goes a long way in making your code have strong OOPS, making it readable, and importantly - maintainable. PlayingCricket is not a real world object. Player, Match etc are examples of real world objects
Class that does not represent the real world
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;
  }
}
				
MatchLost is not a real world domain model. It is the state of a Match, which is a real world domain model.
4. Wrong behaviour in the class
Code does not become well modelled just by having class and behaviour. The right behaviour in the right class is key. No point in having Player class and then having another class which implements scoreRuns.
Try and spot which behaviour should not be in this class
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
    }
}
				
Behaviour in the right place - a good class name is usually a noun, and its behaviour being verbs
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();
    }
}
		
5. Breaking encapsulation
When a shopkeeper asks you for money, will you give them your wallet to take the money out? If you do, then you are breaking encapsulation. The same logic applies to classes and access to methods. In general, public getters and setters result in breaking encapsulation.

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;
    }
}
			
Code that does not break encapsulation
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
    }

}
			
6. Law of delimiter
This law, also known as 'don't talk to strangers', says you should avoid invoking methods of a member object returned by another method.
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)
			
Code that does not violate the law
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)
			
7. Usage of static methods
Static methods allow you to execute code at class scope instead of object scope. So implementing core behaviour of objects using static methods takes away the object orientedness of the solution.
Sample code which breaks OOP because of using static methods.
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()
    }
        
}
			
8. One class doing too many things - SRP
Single responsibility principle is one of the core tenets of OOPS. One class, one responsibility. The temptation to put everything in one class tends to break OOPS.

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
    }
}
			

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us