S: Single Responsibility
Each class should have only one purpose, not filled with excessive functionality. For example, consider a FileMerger.java class.
public class FileMerger { public void mergeAndUploadFile() { File newFile = mergeFiles(files); uploadFile(newFile); } public File mergeFiles(List<File> files) { // merging logic } public void uploadFile(File file) { // upload logic } }
Here we have a FileMerger, but it also handles uploading this file to some database for example. This breaks the single responsibility principle, since FileMerger is also handling file uploading. Instead we should segregate responsibility to a FileMerger class,
public class FileMerger { public File mergeFiles(List<File> files) { // merging logic } }
and a FileUploader class.
public class FileUploader { public void uploadFile(File file) { // upload logic } }
O: Open-Closed Principle
Classes should be open for extension, closed for modification. We should not have to rewrite an existing class to implement a new feature. If we use abstraction, we can have an interface with a baseline implementation, that can be overridden by new implementations. Let’s consider our FileMerger class again. Suppose we want it to merge a number of different files:
public class FileMerger { public File mergeFiles(List<File> files, MergeType mergeType) { File mergedFile; if (MergeType.JSON.equals(mergeType)) { mergedFile = new File("Merged json"); } else if (MergeType.TXT.equals(mergeType)) { mergedFile = new File("Merged txt"); } return mergedFile; } }
This is breaking the open-closed principle, because we must overwrite the class with every new file type we want to merge. Instead we can use abstraction, make our initial class an interface, and make our concrete classes implement the interface.
public interface FileMerger { File mergeFiles(List<File> files, MergeType mergeType) } public class CSVFileMerger implements FileMerger { @Override public File mergeFiles(List<File> files) { return new File("merged CSV's"); } } public class TXTFileMerger implements FileMerger { @Override public File mergeFiles(List<File> files) { return new File("merged TXT's"); } }
Now we can implement new types of file mergers without modifying the base FileMerger class.
L: Liskov-Substitution Principle
A sub-class should be able to fulfill each feature of its parent class and could be treated as its parent class. We can have an abstract class, which can be extended. Methods and member variables can be inherited from the abstract class, but by using the extends keyword, we can add new functionality to the subclass.
I: Interface Segregation Principle
Interfaces should not force classes to implement what they can’t do. Large interfaces should be divided into small ones. Using the FileMerger example again.
public interface FileMerger { File mergeFiles(List<File> files); File deepMerge(List<File> file); }
This FileMerger class would break the interface segregation principle, as we are forcing implementations of FileMerger to also implement a
deepMerge()
function, when .txt files don’t require a deep merge like .json or .yaml files do. A better approach would be to create two interfaces,public interface FileMerger { File mergeFiles(List<File> files); } public interface DeepMerger { File deepMerge(List<File> files); } public class JsonFileMerger implements FileMerger, DeepMerger { @Override public File mergeFiles(List<File> files) { // some logic } @Override public File deepMerge(List<File> files) { // some logic } }
D: Dependency Inversion Principle
High level modules must not depend on the low level module, but they should depend on abstractions.