Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?
like image 213
Matthew Layton Avatar asked Mar 09 '18 15:03

Matthew Layton


3 Answers

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.

like image 123
qujck Avatar answered Oct 07 '22 07:10

qujck


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

like image 40
Ravi Avatar answered Oct 07 '22 08:10

Ravi


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.

like image 1
0b101010 Avatar answered Oct 07 '22 07:10

0b101010