
SOLID Principles and How I Was Breaking Them as Junior Developer
Every good programming blog needs at least one post about SOLID principles, right? SOLID is a set of rules one should follow, when writing object-oriented code and yes - as junior developer I broke all of them many times. In this article I’m sharing my mistakes, so that you can learn and correct them yourself. To check proper implementation for each principle scroll to the very bottom.
S. - Single Responsibility Principle

In programming we try to divide our code into smaller parts, usually into modules, classes or functions, because then it’s easier to understand and maintain. During this process, the most basic criteria we should take into consideration is responsibility. Each module, class and function should do only one thing. Take a look at a common mistake I made when I was learning to code. Can you fix it?
public class Formatter { private WebAPIClient webAPIClient; private Logger logger; public Formatter(WebAPIClient webAPIClient, Logger logger) { this.webAPIClient = webAPIClient; this.logger = logger; } public void formatInputAndUploadResult(String input) { String formattedInput; if (input.length() > 5) { formattedInput = "LONG_INPUT_FROM_MOBILE_APP" + input.trim(); } else { formattedInput = "SHORT_INPUT_FROM_MOBILE_APP" + input.trim(); } webAPIClient.uploadData(formattedInput, new OnSuccessListener() { @Override public void onSuccess() { logger.log("Operation successful!"); } }); } }
O. - Open-Closed Principle

In more complicated projects, whenever a developer modifies existing code, he takes a gamble that things will break in other places.

How can we mitigate this risk? Well, business requirements evolve all the time, so it’s not possible to just say “No” to a product manager, who asks for new additions to the feature we’ve just finished coding. The solution is to minimize changes to existing code in favor of extending it and building upon it. Do you remember what polymorphism is? It’ll help us here. Can you spot what’s wrong with below example and why? Imagine we need to add another user type. How can we make this code more maintainable then?
public class UserSavingManager { private Database database; private AdminFormatter adminFormatter; private OwnerFormatter ownerFormatter; private MemberFormatter memberFormatter; public UserSavingManager(Database database, AdminFormatter adminFormatter, OwnerFormatter ownerFormatter, MemberFormatter memberFormatter) { this.database = database; this.adminFormatter = adminFormatter; this.ownerFormatter = ownerFormatter; this.memberFormatter = memberFormatter; } public void saveUserType(UserType userType, String data) { if (userType == UserType.ADMIN) { String formattedData = adminFormatter.format(data); database.save(formattedData); } else if (userType == UserType.OWNER) { String formattedData = ownerFormatter.format(data); database.save(formattedData); } else if (userType == UserType.MEMBER) { String formattedData = memberFormatter.format(data); database.save(formattedData); } } }
public enum UserType { ADMIN, OWNER, MEMBER }
public interface OwnerFormatter { String format(String data); }
public interface MemberFormatter { String format(String data); }
public interface AdminFormatter { String format(String data); }
public interface Database { void save(String data); }
L. - Liskov Substitution Principle

This rule again touches upon polymorphism (which we are achieving through inheritance). In essence, Liskov Substitution Principle forces us to rethink our abstractions (or simply: base classes, that we’re inheriting from). Why? Because whenever we use a class as a parent (e. g. class Bird
) and we’re extending from it (class Eagle extends Bird
, class Penguin extends Bird
) we cannot forget, that each child class can be used interchangeably in every place the parent class is expected (e. g. function beginOverseasBirdMigration(Bird bird)
can take both Eagle
and Penguin
classes). So if Bird
class had abstract method fly()
, returning a distance it flew after a day, and bird migration requires flying, what would a poor Penguin
do? Penguins can’t fly, but they’re birds. And based on function beginOverseasBirdMigration(Bird bird)
, which inside itself calls fly()
on each Bird
object, they fit into this code perfectly and, in effect, everybody assumes they can migrate from one continent to the other. Unfortunately, implementation of fly()
in Penguin
always returns 0. Can you see a problem here? This abstraction (base class) does not make sense. Now let’s see real-life mistake I made numerous times in the past. I’m sure you can fix it.
public class GroupMember { void leaveGroup() { // Override inside each child class } }
public class GroupAdmin extends GroupMember { @Override void leaveGroup() { // Admins are members and can leave a group any time } }
public class GroupOwner extends GroupMember { @Override void leaveGroup() { // Owners are members, but if they leave group it will be deleted :( } }
public class GroupController { public void forceMemberToLeave(GroupMember groupMember) { // Unexpected behavior: if group member is GroupOwner, then entire group is deleted! groupMember.leaveGroup(); } }
I. - Interface Segregation Principle

The main characteristic of an interface is that a class implementing it must also implement all of its methods. Creator and user (class) of an interface form a contract with each other and, believe me, it’s extremely helpful trait...which can become a burden. Most of the time badly designed code forces you to do things you don’t want to do. And implementing unwanted methods, forced by some BIG weird existing interface you must use, is typical example of that. The solution? Split big interface into few small ones and leave space for a choice about which one to implement - this is exactly what Interface Segregation Principle stands for. Can you do it?
public interface User { void changePassword(); void browseInternet(); }
public class MainUser implements User { @Override public void changePassword() { // Here MainUser changes password } @Override public void browseInternet() { // Here MainUser browses internet } }
public class GuestUser implements User { @Override public void changePassword() { // unused - guest user can't change password } @Override public void browseInternet() { // Here user browses internet } }
D. - Dependency Inversion Principle

Let me start with the most important thing: Dependency Inversion IS NOT Dependency Injection. You can read more about differences between them in this blog post. Dependency Injection can help us achieve Dependency Inversion, but the inversion itself is a different concept. Every complicated class you’ll create in the future will depend on many other classes, which in turn will depend on others etc. The core idea behind Dependency Inversion is to combine all of these classes in a sweet plug-in design. Imagine a little boy, who likes milk chocolate. It makes him stop crying, so you are happy to give him a little every once in a while. There are many companies creating milk chocolate: Milka, Lindt or Hershey's, and (somehow) different brands produce different results on your boy. Milka makes him sleepy. Lindt makes him laugh, because of oval praline’s shape. And Hershey’s regularly makes him produce a mess on a couch. If the shop nearby runs out of Milka, you can buy Lindt, Hershey's or any other milk chocolate and the result will be the same - the boy will not cry. This is the sort of plug-in architecture I was talking about. Milk chocolate is an abstraction and you provide a concrete implementation (concrete chocolate brand) depending on the use case and availability. It would be a nightmare if your boy only liked Lindt chocolate (depended on one concrete implementation), right? Can you translate above example into code? The mistake below with logger and unit testing was done by me multiple times in the past. Do you know how to clearly separate production implementation from a test one?
public class BackgroundTasksManager { private ProductionLogger productionLogger; private TestLogger testLogger; public BackgroundTasksManager(ProductionLogger productionLogger, TestLogger testLogger) { this.productionLogger = productionLogger; this.testLogger = testLogger; } void executeInTheBackground(String input, boolean isUnitTest) { process(input); if (isUnitTest) { testLogger.log("Unit test finished. Test processing completed."); } else { productionLogger.log("Production processing completed"); } } private void process(String input) { // Here a long running operation is taking place. } }
public class ProductionLogger { void log(String message) { // It logs something here. } }
public class TestLogger { void log(String message) { // It logs something here. } }
SOLUTIONS
S. - Single Responsibility Principle
public class Formatter { public String formatInput(String input) { String formattedInput; if (input.length() > 5) { formattedInput = "LONG_INPUT_FROM_MOBILE_APP" + input.trim(); } else { formattedInput = "SHORT_INPUT_FROM_MOBILE_APP" + input.trim(); } return formattedInput; } }
public class Uploader { private WebAPIClient webAPIClient; private Logger logger; public Uploader(WebAPIClient webAPIClient, Logger logger) { this.webAPIClient = webAPIClient; this.logger = logger; } public void upload(String formattedInput) { webAPIClient.uploadData(formattedInput, new OnSuccessListener() { @Override public void onSuccess() { logger.log("Operation successful!"); } }); } }
O. - Open-Closed Principle
interface UserSavingManager { void saveUserType(String data); }
public class OwnerSavingManager implements UserSavingManager { private Database database; private OwnerFormatter ownerFormatter; public OwnerSavingManager(Database database, OwnerFormatter ownerFormatter) { this.database = database; this.ownerFormatter = ownerFormatter; } @Override public void saveUserType(String data) { String formattedData = ownerFormatter.format(data); database.save(formattedData); } }
public class MemberSavingManager implements UserSavingManager { private Database database; private MemberFormatter memberFormatter; public MemberSavingManager(Database database, MemberFormatter memberFormatter) { this.database = database; this.memberFormatter = memberFormatter; } @Override public void saveUserType(String data) { String formattedData = memberFormatter.format(data); database.save(formattedData); } }
public class AdminSavingManager implements UserSavingManager { private Database database; private AdminFormatter adminFormatter; public AdminSavingManager(Database database, AdminFormatter adminFormatter) { this.database = database; this.adminFormatter = adminFormatter; } @Override public void saveUserType(String data) { String formattedData = adminFormatter.format(data); database.save(formattedData); } }
L. - Liskov Substitution Principle
public interface CanLeaveGroup { void leaveGroup(); }
public class GroupMember { }
public class GroupAdmin implements CanLeaveGroup { // Now we're using composition instead of inheritance private GroupMember groupMember; public GroupAdmin(GroupMember groupMember) { this.groupMember = groupMember; } @Override public void leaveGroup() { // Admins are members and can leave a group any time } }
public class GroupOwner { // Now we're using composition instead of inheritance private GroupMember groupMember; public GroupOwner(GroupMember groupMember) { this.groupMember = groupMember; } }
public class GroupController { public void forceMemberToLeave(CanLeaveGroup groupMember) { groupMember.leaveGroup(); } }
I. - Interface Segregation Principle
public interface User { void browseInternet(); }
public interface SuperUser extends User { void changePassword(); }
public class MainUser implements SuperUser { @Override public void changePassword() { // Here MainUser changes password } @Override public void browseInternet() { // Here MainUser browses internet } }
public class GuestUser implements User { @Override public void browseInternet() { // Here user browses internet } }
D. - Dependency Inversion Principle
public class BackgroundTasksManager { private Logger logger; public BackgroundTasksManager(Logger logger) { this.logger = logger; } void executeInTheBackground(String input) { process(input); // In test the actual implementation of Logger we'll provide is TestLogger. // In production the actual implementation of Logger we'll provide is ProductionLogger. // BackgroundTasksManager now depends on abstraction NOT on a concrete implementation of logger. logger.log("Processing finished"); } private void process(String input) { // Here a long running operation is taking place. } }
public interface Logger { void log(String message); }
public class ProductionLogger implements Logger { @Override public void log(String message) { // It logs something here. } }
public class TestLogger implements Logger { @Override public void log(String message) { // It logs something here. } }