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
- Example 1
- Example 2
- Before
- After
001 import java.time.Year;002003 enum Genre {004 POP,005 ROCK,006 HIPHOP,007 RNB,008 EDM009 }010011 enum PlaybackState {012 STOPPED,013 PLAYING,014 PAUSED015 }016017 class Record {018 private String name;019 private Genre genre;020 private int duration;021 private Year releaseYear;022023 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 }029030 public String getName() {031 return this.name;032 }033034 public boolean isReleased() {035 return this.releaseYear.isBefore(Year.now()) || this.releaseYear.equals(Year.now());036 }037038 public int getDuration() {039 return this.duration;040 }041042 public Genre getGenre() {043 return this.genre;044 }045046 }047048 class RecordPlayer {049 private Record currentRecord;050 private PlaybackState state;051052 public RecordPlayer(Record initialRecord) {053 this.currentRecord = initialRecord;054 this.state = PlaybackState.STOPPED;055 }056057 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 }062063 public String getCurrentRecordDetails() {064 String details = currentRecord.getName() + " - " + currentRecord.getGenre() + " - " + getFormattedDuration();065 return details;066 }067068 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 }076077 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 }085086 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 }094095 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 }105106 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 }