Search code examples
javaclassinterfacesolid-principlessingle-responsibility-principle

Single Responsibility Principle Refactor


I have some code I'd like to re-factor so it does not violate Single Responsibility Principle (SRP).

I understand that the below class could change for multiple reasons:

  • Business rules for analyze could change
  • Metadata schema could change
  • Upload method could change

However, I'm having a tough time figuring out how I can re-factor into separate classes.

Engine.java

package com.example;

import java.util.List;

public interface Engine {
  public List<Recording> analyze(List<String> files);
  public List<Recording> getMetadata(List<Recording> recordings);
  public List<Recording> upload(List<Recording> recordings);
}

CallEngine.java

package com.example;

import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CallEngine implements Engine {

  final static Logger log = LoggerFactory.getLogger(Main.class);

  public List<Recording> analyze(List<String> files) {
    log.info("Analyzing recording files per business rules...");

    List<Recording> recordings = new ArrayList<Recording>();
    return recordings;
  }

  public List<Recording> getMetadata(List<Recording> r) {
    log.info("Retrieving metadata for calls...");
    List<Recording> recordings = new ArrayList<Recording>();
    return recordings;
  }

  public List<Recording> upload(List<Recording> r) {
    log.info("Uploading calls...");
    List<Recording> recordings = new ArrayList<Recording>();
    return recordings;
  }
}

Solution

  • The SRP is primarily achieved through abstracting code behind interfaces and delegating responsibility for unrelated functionality to whichever implementation happens to be behind the interface at run time.

    In this case you need to abstract the responsibilities out behind their own interface.

    For example...

    public interface Analyzer {
        public List<Recording> analyze(List<String> files);
    }
    public interface Retriever {
        public List<Recording> getMetadata(List<Recording> recordings);
    }
    public interface Uploader {
        public List<Recording> upload(List<Recording> r);
    }
    

    And have them as inject-able dependencies of the Engine implementation.

    public class CallEngine implements Engine {
        private Analyzer analyzer;
        private Retriever retriever;
        private Uploader uploader;
    
        public CallEngine(Analyzer analyzer, Retriever retriever, Uploader uploader) {
            this.analyzer = analyzer;
            this.retriever = retriever;
            this.uploader = uploader;        
        }
    
        public List<Recording> analyze(List<String> files) {
            return analyzer.analyze(files);
        }
    
        public List<Recording> getMetadata(List<Recording> r) {
            return retriever.getMetadata(r);
        }
    
        public List<Recording> upload(List<Recording> r) {
            return uploader.upload(r);
        }
    }
    

    Their run time implementations can be changed without affecting the overall responsibility of the dependent class implementation which makes it far more adaptive to change.