Feature Envy

Description (What)

When one method accesses the data of another object more than it does itself

How to Locate It (Where)

Look for methods where an object's fields or methods (objects either from the class' fields or passed through parameters) are used more than it's own classes fields or methods.

How It Manifests (Why)

It might be easier to use another object's fields or methods directly in the method of an object.

How to Fix It (Possible Treatments)

In Example 1, Move Method treatment is used. This treatment is applied when a method that is being called a lot by another class' method is actually better suited to that class itself. Doing this reduces the number of calls to another object and keeps changes easier and restricted to a single class.

In Example 2, Move Method treatment is also used for the same reasons.

Other treatments are also possible based on the specific scenario, they can be found here

Examples

Example 1

Before:

The data of Record class, specifically genre, name and duration is accessed far more by the class RecordPlayer than RecordPlayer is accessing its own data

Observed Code Smells:
- Feature Envy (lines 57-61, 63-66)

After:

Used Move Method to move getFormattedDuration() and getCurrentRecordDetails() to the Record class as it is better suited.

Refactoring Applied:
- Feature Envy
    - Move Method (getFormattedDuration, getCurrentRecordDetails)
Observed Code Smells After Refactoring:
- None

Example 2

Before:

The method generateUsageReport in class SignalProcessor currently generate report by accessing data from

methods in class RemoteController, which makes a Feature Envy code smell.

We can move the functionality of generating report back to the class RemoteController and remove the

related getter methods in class RemoteController.

Observed Code Smells:
- Feature Envy (lines 57-61, 63-66)

After:

Applied Move Method to move the envied feature back to the class from which the data comes from.


Refactoring Applied:
- Feature Envy:
    - Move Method (line 50-77)
Observed Code Smells After Refactoring:
- None

When to Ignore

In cases where there is use of the Strategy Pattern or Visitor Pattern. Also in cases of Data Access Objects

More

More about Feature Envy

001  import java.time.Year;
002  
003  enum Genre {
004   POP,
005   ROCK,
006   HIPHOP,
007   RNB,
008   EDM
009  }
010  
011  enum PlaybackState {
012   STOPPED,
013   PLAYING,
014   PAUSED
015  }
016  
017  class Record {
018   private String name;
019   private Genre genre;
020   private int duration;
021   private Year releaseYear;
022  
023   public Record(String name, Genre genre, int duration, Year releaseYear) {
024   this.name = name;
025   this.genre = genre;
026   this.duration = duration;
027   this.releaseYear = releaseYear;
028   }
029  
030   public String getName() {
031   return this.name;
032   }
033  
034   public boolean isReleased() {
035   return this.releaseYear.isBefore(Year.now()) || this.releaseYear.equals(Year.now());
036   }
037  
038   public int getDuration() {
039   return this.duration;
040   }
041  
042   public Genre getGenre() {
043   return this.genre;
044   }
045  
046  }
047  
048  class RecordPlayer {
049   private Record currentRecord;
050   private PlaybackState state;
051  
052   public RecordPlayer(Record initialRecord) {
053   this.currentRecord = initialRecord;
054   this.state = PlaybackState.STOPPED;
055   }
056  
057   private String getFormattedDuration() {
058   int minutes = this.currentRecord.getDuration() / 60;
059   int seconds = this.currentRecord.getDuration() % 60;
060   return String.format("%d min %02d sec", minutes, seconds);
061   }
062  
063   public String getCurrentRecordDetails() {
064   String details = currentRecord.getName() + " - " + currentRecord.getGenre() + " - " + getFormattedDuration();
065   return details;
066   }
067  
068   public void playRecord() {
069   if (this.state == PlaybackState.PLAYING) {
070   System.out.println("Already playing");
071   } else {
072   System.out.println("Playing: " + currentRecord.getName());
073   this.state = PlaybackState.PLAYING;
074   }
075   }
076  
077   public void stopRecord() {
078   if (this.state == PlaybackState.STOPPED) {
079   System.out.println("Already stopped");
080   } else {
081   System.out.println("Stopped playing: " + currentRecord.getName());
082   this.state = PlaybackState.STOPPED;
083   }
084   }
085  
086   public void pauseRecord() {
087   if (this.state == PlaybackState.PAUSED) {
088   System.out.println("Already paused");
089   } else {
090   System.out.println("Paused: " + currentRecord.getName());
091   this.state = PlaybackState.PAUSED;
092   }
093   }
094  
095   public void changeRecord(Record newRecord) {
096   if (this.state != PlaybackState.STOPPED) {
097   System.out.println("Please stop playing to change the record");
098   } else {
099   this.currentRecord = newRecord;
100   this.state = PlaybackState.PLAYING;
101   System.out.println("Started playing: " + this.currentRecord.getName());
102   }
103   }
104  }
105  
106  public class FEBE1 {
107   public static void main(String[] args) {
108   Record stairwayToHeaven = new Record("Stairway to Heaven", Genre.POP, 482, Year.of(1971));
109   Record saySo = new Record("Say So", Genre.RNB, 238, Year.of(2019));
110   RecordPlayer sony = new RecordPlayer(saySo);
111   sony.pauseRecord();
112   sony.playRecord();
113   sony.changeRecord(stairwayToHeaven);
114   sony.stopRecord();
115   sony.changeRecord(stairwayToHeaven);
116   System.out.println(sony.getCurrentRecordDetails());
117   sony.playRecord();
118   }
119  }