A class should have only one reason to change.
Robert C. Martin
I’m going to start a five blog post journey about the five principles behind SOLID. I’ll describe these in my own words and give examples of what breaks the rule and how to fix it. Now, what I’m not going to prescribe, is that you must be SOLID all the time. Because that is not true. Sometimes, you need to break a few rules.
The first rule I’m going to be talking about is ‘S’. S for Single Responsibility Principle. The name of the concept gives it away. What we’re talking about, is that each class should be responsible for one thing, and one thing only. None of this “oh but I need this class to do A and B”. No. You can split these functionalities up into separate classes, and you’ll be happy in the long run.
Now, not everyone agrees with these principles. A lot of engineers haven’t even heard of them. That’s okay. But, let me explain them in my way anyway. Because I find these principles useful and I tend to mostly follow them when I can.
Breaking the principle
Let’s take a practical example. Let’s say you have a list of objects, and you want to apply some business logic to them, and that will also map them, and then we want to add them to a stored list on your class. It doesn’t sound too bad, right? It’s testable. It’s simple. All your logic is in one place. But what happens if everyone thinks like that? What happens when things start to get more complicated?
public class MyObject { public int MyInt { get; set; } } public class MyTransformedObject { public string MyInt { get; set; } } public class MyBusinessLogicClass { private readonly List<MyTransformedObject> objects = new List<MyTransformedObject>(); public void AddToList(List<MyObject> list) { list.ForEach(i => i.MyInt += 10); var localList = list.Select(i => new MyTransformedObject { MyInt = i.MyInt.ToString() }); objects.AddRange(localList); } }
Obviously, this example is not code you’d write. Let’s take Martin’s quote “A class should have only one reason to change.” In this case, you have the change in the mapping, the change in the business logic, or the change in how you add to the list. Because you’re doing three things at once, you’re breaking the Single Responsibility Principle.
Fix using the principle
Step one: Let’s take out the mapping.
public class MyObject { public int MyInt { get; set; } } public class MyTransformedObject { public string MyInt { get; set; } } public class MyObjectMapper { public MyTransformedObject Map(MyObject i) { return new MyTransformedObject {MyInt = i.MyInt.ToString()}; } } public class MyBusinessLogicClass { private readonly List<MyTransformedObject> objects = new List<MyTransformedObject>(); private readonly MyObjectMapper mapper; public MyBusinessLogicClass(MyObjectMapper mapper) { this.mapper = mapper; } public void AddToList(List<MyObject> list) { list.ForEach(i => i.MyInt += 10); var localList = list.Select(mapper.Map); objects.AddRange(localList); } }
Step two: Let’s extract the business logic.
public class MyObject { public int MyInt { get; set; } } public class MyTransformedObject { public string MyInt { get; set; } } public class MyObjectMapper { public MyTransformedObject Map(MyObject i) { return new MyTransformedObject {MyInt = i.MyInt.ToString()}; } } public class MyBusinessLogicClass { public void DoLogic(MyObject iMyObject) { iMyObject.MyInt += 10; } } public class MyListClass { private readonly List<MyTransformedObject> objects = new List<MyTransformedObject>(); private readonly MyObjectMapper mapper; private readonly MyBusinessLogicClass businessLogic; public MyListClass(MyObjectMapper mapper, MyBusinessLogicClass businessLogic) { this.mapper = mapper; this.businessLogic = businessLogic; } public void AddToList(List<MyObject> list) { list.ForEach(businessLogic.DoLogic); var localList = list.Select(mapper.Map); objects.AddRange(localList); } }
Step Three: Let’s finally let these guys be responsible for the lists
public class MyObject { public int MyInt { get; set; } } public class MyTransformedObject { public string MyInt { get; set; } } public class MyObjectMapper { public MyTransformedObject Map(MyObject i) { return new MyTransformedObject {MyInt = i.MyInt.ToString()}; } public IEnumerable<MyTransformedObject> Map(List<MyObject> list) { return list.Select(Map); } } public class MyBusinessLogicClass { public void DoLogic(MyObject iMyObject) { iMyObject.MyInt += 10; } public void DoLogic(List<MyObject> list) { list.ForEach(DoLogic); } } public class MyListClass { private readonly List<MyTransformedObject> objects = new List<MyTransformedObject>(); private readonly MyObjectMapper mapper; private readonly MyBusinessLogicClass businessLogic; public MyListClass(MyObjectMapper mapper, MyBusinessLogicClass businessLogic) { this.mapper = mapper; this.businessLogic = businessLogic; } public void AddToList(List<MyObject> list) { businessLogic.DoLogic(list); var localList = mapper.Map(list); objects.AddRange(localList); } }
Now you could take it even further and have AddToList only add to the list and have it take the correct parameters, but you don’t need to do that. Because now the only way this class should change is that you change how you add to the list, right? The business logic (of adding 10) won’t change this class. The mapping won’t change the class. It’s all been taken out.
S for Single Responsibility Principle
I do hope that this will help someone someday. Keep your classes/methods slim and simple. Extract logic into their own places. Think about what would the responsibility of your class be. Is it a data access class? Should I be mapping in this class? Is it a repository class? Should I have that extra layer of database logic so I can change my database in the future? Think about these things and keep your classes lean. One responsibility. One purpose. One reason to change.
Leave a Reply