Reputation: 42260
The Single Responsibility Principle states that:
A class should have one, and only one, reason to change.
The Open/Closed Principle states that:
You should be able to extend a classes behavior, without modifying it.
How can a developer respect both principles if a class should have only one reason to change, but should not be modified?
Example
The factory pattern is a good example here of something that has a single responsibility, but could violate the open/closed principle:
public abstract class Product
{
}
public class FooProduct : Product
{
}
public class BarProduct : Product
{
}
public class ProductFactory
{
public Product GetProduct(string type)
{
switch(type)
{
case "foo":
return new FooProduct();
case "bar":
return new BarProduct();
default:
throw new ArgumentException(...);
}
}
}
What happens when I need to add ZenProduct
to the factory at a later stage?
Upvotes: 5
Views: 1268
Reputation: 764
I think it depends on your interpretation of the SRP. This stuff is always somewhat subjective. Ask 100 people to define "single responsibility" and you'll probably get 100 different answers.
Using the scenario in Ravi's answer, a typical solution might be to have a ReportGenerator
class which exposes a GeneratePdf
method. It could then be later extended with an additional GenerateWord
method if required. Like yourself though, I think this has a whiff about it.
I would probably refactor the GeneratePdf
method into a PdfReportGenerator
class and then expose that through the ReportGenerator
. That way the ReportGenerator
only has a single responsibility; which is to expose the various report generation mechanisms (but not contain their logic). It could then be extended without expanding upon that responsibility.
I'd say that if you find a conflict, it might well be an architectural smell that warrants a quick review to see if it can be done in a better way.
Upvotes: 1
Reputation: 14580
This feels like a discussion of the semantics of 'extend a classes behaviour'. Adding the new type to the factory is modifying existing behaviour, it's not extending behaviour, because we haven't changed the one thing the factory does. We may need to extend the factory but we have not extended it's behaviour. Extending behaviour means introducing new behaviour and would be more along the lines of an event each time an instance of a type is created or authorising the caller of the factory - both these examples extend (introduce new) behaviour.
A class should have one, and only one, reason to change.
The example in the question is a factory for creating Product
instances and the only valid reason for it to change is to change something about the Product
instances it creates, such as adding a new ZenProduct
.
You should be able to extend a classes behavior, without modifying it.
A really simple way to achieve this is through the use of a Decorator
The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern.
public interface IProductFactory
{
Product GetProduct(string type);
}
public class ProductFactory : IProductFactory
{
public Product GetProduct(string type)
{
\\ find and return the type
}
}
public class ProductFactoryAuth : IProductFactory
{
IProductFactory decorated;
public ProductFactoryAuth(IProductFactory decorated)
{
this.decorated = decorated;
}
public Product GetProduct(string type)
{
\\ authenticate the caller
return this.decorated.GetProduct(type);
}
}
The decorator pattern is a powerful pattern when applying the SOLID principles. In the above example we've added authentication to the ProductFactory
without changing the ProductFactory
.
Upvotes: 3
Reputation: 4164
I have a class StudentOrganiser
class which takes IStudentRepository
dependency. Interfaces exposed by IStudentRepository
is say GetStudent(int studentId)
Class obeys SRP because it does not have any logic related to manage the connection with repository source.
Class obeys OCP because if we want to change repository source from SQL to XML, StudentOrganiser
need not to undergo any changes => open for extension but closed for modification.
Consider if StudentOrganiser
was designed to not take dependency of IStudentRepository
, then method inside class itself must be taking care of instantiating new StudentSqlRepository()
If later on requirement would have come to also support StudentXMLRepository
on the basis of certain run time condition, your method would have ended with some case switch
kind of paradigm and thus violating SRP as method is also indulged in actual repository deciding factor. By injecting repository dependency we taken off that responsibility from class. Now StudentOrganiser
class can be extended to support StudentXMLRepository
without any modification.
Upvotes: 0
Reputation: 31407
A class should have one, and only one, reason to change.
This basically means, your class should represent single responsibility and shouldn't be modified thereafter to accommodate new feature.
For example, if you have class, which is responsible to print report in pdf format. Later, you wanted to add new feature to support printing report in other formats. Then instead of modify the existing code, you should extend it to support other format, which also implies extend a classes behavior, without modifying it
Upvotes: 2