SOLID - are the Single Responsibility Principle and the Open/Closed Principle mutually exclusive?
Asked Answered
M

4

5

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?

  • Surely this violates the open/closed principle?
  • How can we prevent this violation?
Missis answered 9/3, 2018 at 15:46 Comment(9)
@Ravi butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod from Uncle Bob, who is renowned amongst the developer community.Missis
Can you outline a contradiction? An example might be handy.Jacy
@Jacy see update.Missis
In your example, I don't see how adding ZenProduct would be a contravention of either principle, as you're merely performing routine maintenance/improvement - neither principle suggests that once the class is written it can never be changed, and indeed this sort of change is one of the key reasons to use a factory in the first place. However if you believe that this is the case then you should make GetProduct() a virtual method, thereby still satisfying both principlesEmbargo
@Embargo The OCP states that "A class should be closed for modification". Therefore modifying it to add ZenProduct violates the principle. I agree that extending the factory would prevent this violation, however it's a lot of overhead, plus you'd also have to violate the OCP elsewhere, because you'd have to modify a class to use the new factory.Missis
I believe the keyword here is 'should'. So as a real life example, if this is your own code then get in there and change it. But if you're providing this to another team/customer, then they may need to have the ability to alter this behaviour without having to ask you, the provider, to make that change, and in this specific case can be achieved by making the method virtual. OCP is very contentious IMHO and if you are working on your own complete set of code then it's the least interesting of the SOLID principlesEmbargo
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.Dahle
@Dahle doesn't introducing new behaviour to the class violate the SRP?Missis
@series0ne yes it would, but your example isn't adding new behaviourDahle
D
3

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.

Dahle answered 16/3, 2018 at 15:46 Comment(1)
What a coincidence. I'm watching a pluralsight tutorial on the decorator pattern right now!Missis
A
2

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

Amitie answered 9/3, 2018 at 16:25 Comment(3)
I agree with this, but there are still LOTS of examples where the principles contradict each other.Missis
@series0ne Probably, but I couldn't recall any example right now, where they would contradict each other. :-)Amitie
Please take a look at the update. I've added an example.Missis
J
1

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.

Jacy answered 9/3, 2018 at 17:22 Comment(0)
C
0

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.

Contrition answered 9/3, 2018 at 17:39 Comment(2)
I think you might be a little confused here. You are not extending StudentOrganiser by injecting a different IStudentRepository implementation.Jacy
@Jacy : I am clear :) updated my answer to explain the pointContrition

© 2022 - 2024 — McMap. All rights reserved.