SOLID and Open Closed principle on Monster Method

Overview

In this series of articles, I will cover all the SOLID patterns and their use in examples.

The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.”

The SOLID principles consist of the following five concepts:

Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle

ocp

Open Closed Principle

A software module (class or method) should be open for extension but closed for modification.

Monster Method Exercise

In previous article Open Closed principle you can find exercise how to apply open closed principle with inheritance or using composition. Recommended is to use composition. Here I will explain apply open closed principle on Monster Method.

I reuse code from previous article where I focus on Single Responsibility principle

Original code

Here you can see the monster method in one class. First, think about the different responsibilities that this method takes care of. And the requirement is pretty simple, separate the text altering logic into its own class. Then customer starts to think what about some change request: In altered text, find word 'question' and replace it with text '❓'.

public class TextManipulator {
    public void findOccurrences(String fileName, CharSequence charSequence) {
        ClassLoader classLoader = TextManipulator.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream(fileName);
        if (inputStream == null) {
            throw new IllegalArgumentException("file not found: " + fileName);
        }

        int counter = 0;
        StringBuilder alteredText = new StringBuilder();
        try (InputStreamReader streamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
            BufferedReader reader = new BufferedReader(streamReader)) {

            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(charSequence)) {
                    counter++;
                }
                alteredText.append(line + "("+counter+")\n"); // (1)
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println(
            String.format("File '%s' contains %s occurrences of charSequence '%s.'", fileName, counter, charSequence));
        System.out.println(alteredText);
    }
}
  1. On the first place, is good to mention that these method contains multiple responsibilities. This line contains two responsibility:

    • appending text itself alteredText.append

    • building string from line, count and end of line character line + "("+counter+")\n"

Separate altering logic

First of all, I will create an interface that can be implemented using different scenarios

public interface LineAlternator {
    String alterLine(String line, LineData data);
}

First implementation of appending logic from monster method. New Object LineData can handle count logic.

public class AddCountInLineAlternator implements LineAlternator {
    @Override
    public String alterLine(String line, LineData data) {
        return line + "(" + data.getCount() + ")\n";
    }
}

public class LineData{
    public LineData(int count) {
        this.count = count;
    }

    private final int count;

    public int getCount() {
        return count;
    }
}

And now I want to use that AddCountInLineAlternator. And this can first refactor in monster method.

public class TextManipulator {
    public void findOccurrences(String fileName, CharSequence charSequence) {
        // ...
        LineAlternator lineAlternator = new AddCountInLineAlternator();
        StringBuilder alteredText = new StringBuilder();
        // ...
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(charSequence)) {
                    counter++;
                }
                // start of refactored lines
                String alteredLine = lineAlternator.alterLine(line, new LineData(counter));
                alteredText.append(alteredLine);
                // end of refactored lines
            }
        // ...
    }
}

Change Request improvement

Then I want implement improvement logic. In altered text, find word 'question' and replace it with text '❓'. And I will reuse existing interface and new class (SimpleColonizedQuestionWordLineAlternator).

public class SimpleColonizedQuestionWordLineAlternator implements LineAlternator {
    @Override
    public String alterLine(String line, LineData data) {
        return line.replace("question", "❓");
    }
}

and then usage will be similar to previous case:

public class TextManipulator {
    public void findOccurrences(String fileName, CharSequence charSequence) {
        // ...
        LineAlternator lineAlternator = new SimpleCollonizedQuestionWordLineAlternator();
        StringBuilder alteredText = new StringBuilder();
        // ...
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(charSequence)) {
                    counter++;
                }
                // start of refactored lines
                String alteredLine = lineAlternator.alterLine(line, new LineData(counter));
                alteredText.append(alteredLine);
                // end of refactored lines
            }
        // ...
    }
}

However, as you can see I am starting to have similar usage for same interface, so there should be some decision logic which Alternator should be used and when. For examples what should happen when I want to have both implementation at one time.

MultiAlternator and Factory

When I want to use multiple similar implementation of same interface, array can be a good way, but still keep in mind open closed principle. Where that array should be placed ?

I suggest that you separate this logic. That is why I created another class MultiAlternator. This class implements LineAlternator and manages the array of all alternators in it.

public class MultiAlternator implements LineAlternator {

    public MultiAlternator(List<LineAlternator> alternators){
        this.alternators = alternators;
    }

    private List<LineAlternator> alternators;

    @Override
    public String alterLine(String line, LineData data) {
        String alteredLine = line;
        for (LineAlternator alternator : alternators) {
            alteredLine = alternator.alterLine(alteredLine, data);
        }
        return alteredLine;
    }
}

Usage of that array is not so difficult and can be increased or decreased by any time. Inside constructor is defined which LineAlternator I want to use and that all.

class Factory
{
    private LineAlternator ALTERNATORS = new MultiAlternator(
            new ArrayList<LineAlternator>(
                    Arrays.asList(
                    new AddCountInLineAlternator(),
                    new SimpleColonizedQuestionWordLineAlternator()
                    )
            )
    );
}

When I call the same line of code in TextManipulator String alteredLine = lineAlternator.alterLine(line, new LineData(counter)); It will call the override method of MultiAlternator, and it will use all the alternators that I have specified in the array.

And for more readability I put these ALTERNATORS into Factory class, which will handle that decision logic for me.

class LineAlternatorFactory {
    private static LineAlternator DEFAULT_ALTERNATOR = new MultiAlternator(
            new ArrayList<LineAlternator>(
                    Arrays.asList(
                    new AddCountInLineAlternator(),
                    new SimpleCollonizedQuestionWordLineAlternator()
                    )
            )
    );
    public LineAlternator alternator(){
        return DEFAULT_ALTERNATOR;
    }
}

public class TextManipulator {

    private final LineAlternatorFactory lineAlternatorFactory = new LineAlternatorFactory();

    public void findOccurrences(String fileName, CharSequence charSequence) {
        LineAlternator lineAlternator = lineAlternatorFactory.alternator();
        // ...
        StringBuilder alteredText = new StringBuilder();
        // ...
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(charSequence)) {
                    counter++;
                }
                String alteredLine = lineAlternator.alterLine(line, new LineData(counter));
                alteredText.append(alteredLine);
            }
        //...
    }
}

Class Diagram

As you can see in Diagram interface LineAlternator has three implementation and override same method.

class diagram.drawio

There are many other things that can be fixed and broken in the monster method. I hope the exercise has shown you a way to achieve clean code.

Michal Slovík
Michal Slovík
Java developer and Cloud DevOps

My job interests include devops, java development and docker / kubernetes technologies.