What is the principle of openness and closeness?

I study SOLID principles. Tell me an example that clearly illustrates this principle, mentally I understand that the class should be closed from changes, but open for expansion, here with the extension, tell me.

I read books, but I do not understand this point: I just do not understand if the class is closed from changes, then how it can be extended

Author: A K, 2018-10-04

1 answers

So, the principle states that

Program entities (classes, modules, functions, etc.) should be open for expansion, but closed for modification

So, it can be seen that we are talking about:

  1. Classes
  2. Modules
  3. Functions
  4. etc.

All of them should be open for expansion, but closed for modification. It sounds great, but to understand this principle by its name is quite difficult.

Let's start with "closed for modification". This means that the only reason you can change the code of a class \ function\module is to directly change the function embedded in it. Everything. There should be no more reason to change this code. It is at this point that the principle of the uniqueness of responsibility intersects.

Next, what does it mean to be open for expansion? This means that if you need your class \ function\module to be able to perform the built-in functions in the new environment, they should this is supported without changing their code.

Let's look at an example for clarity.

Extending a class by delegation

Let's say we have a class for sorting the array

public class BubbleSorter
{
    public void Sort(int[] data)
    {
        int n = data.Length;
        for (int i = 0; i < n - 1; i++)
            for (int j = 0; j < n - i - 1; j++)
                if (data[j] > data[j + 1])
                {
                    int temp = data[j];
                    data[j] = data[j + 1];
                    data[j + 1] = temp;
                }
    }
}

Let's take a look at this class. Let's try to understand what expansion points it has. Extension points are the requirements that are most likely to occur in your application (or have already occurred) during different scenarios of using your code.

For example, we see, that this method only sorts numbers. But with a 99% probability, we will need to sort something else besides numbers. Also, the sorting is only in ascending order, but most likely we will need to sort in descending order. In fact, we again intersect with the principle of the uniqueness of responsibility - we determine what responsibilities a class currently has and try to divide them. Let's use the existing interfaces in dotnet and rewrite the code a bit:

public class BubbleSorter<T>
{
    IComparer<T> _comparer;

    public BubbleSorter(IComparer<T> comparer)
    {
        _comparer = comparer;
    }

    public void Sort(T[] data)
    {
        int n = data.Length;
        for (int i = 0; i < n - 1; i++)
            for (int j = 0; j < n - i - 1; j++)
                if (_comparer.Compare(data[j], data[j + 1]) > 0)
                {
                    var temp = data[j];
                    data[j] = data[j + 1];
                    data[j + 1] = temp;
                }
    }
}

Now our sorter became a little more flexible. Now, in case we need to sort strings instead of numbers, we won't have to make a change to our sorter. If we need to sort in descending order instead of ascending order, we won't have to change the sorter code. Thus, we can extend our class with new functionality without actually changing the class itself.

The same is true for functions. For example,

var sorted = data.OrderBy(x=> /* ваше направление сортировки */ );

The same is true for software modules (for example, when you can reuse the same module in different scenarios).

However, the above does not mean that you need to immediately rush to your classes and start implementing similar techniques in them. As always, before you do this, you need to think with your head. For example, you can see that the code above only sorts arrays. moreover, it sorts on the spot (changes the original array). I deliberately left it out, as I wanted to show that we should not go to extremes. Don't be blind follow the principle, you need to think carefully before you write something. Because if you follow the principle thoughtlessly, you can end up with too abstract code, in which no one will understand anything.

In my case, I decided that I don't need to sort other data types other than arrays. I assumed that I wouldn't need this in the current project (consider it part of a synthetic example).

But we have only considered one of the expansion options class-transferring part of its responsibility to another object that can be replaced dynamically. What else can we do with the class?

Class extension by inheritance

Suppose we have a typical writer class in a CSV file

public class CsvWriter<T>
{   
    protected void WriteBody(IEnumerable<T> obj, TextWriter stream){
        foreach(var ob in obj) stream.WriteLine(ob);
    }

    protected virtual void WriteInternal(IEnumerable<T> obj, TextWriter stream)
    {
        WriteBody(obj, stream);
    }

    public void Write(IEnumerable<T> obj, TextWriter stream)
    {
        WriteInternal(obj, stream);
    }
}

With a trained eye, you can see that in this case, the class itself decides exactly what the object entries in the file will look like. This, of course, is a candidate for the allocation of a separate responsibility, but we do not care about it now. what's worrying is that our class doesn't write the file header. That is, he can somehow write down the columns, but there is no title for each column. What should I do? From the signature of the class functions, you can see that it has a virtual method that completely determines the order in which data is written to the file. We can easily write a class descendant and add a file header to it without changing the base class.:

public class CsvWriterWithHeader<T> : CsvWriter<T>
{
    protected virtual void WriteHeader(TextWriter stream){
        stream.WriteLine("I AM HEADER!");
    }

    protected override void WriteInternal(IEnumerable<T> obj, TextWriter stream)
    {
        WriteHeader(stream);
        WriteBody(obj, stream);
    }   
}

As a result, the functionality is expanded, the original class is not touched.

Extending the class by aggregating

So, for example, we have an interface and a class for the repository. You may even have several implementations of such a repository.

public interface IRepository
{
    void SaveStuff();
}

public class Repository : IRepository
{
    public void SaveStuff()
    {
        // save stuff   
    }
}

And, of course, there is some client of all this

class RepoClient
{
    public void DoSomethig(IRepository repo)
    {
        //...
        repo.SaveStuff();
    }
}

And one day your boss tells you that you need to log every call to every implementation of your repository. But you don't want to change all the implementations because of this (and you already know why). What should I do? Of course roll a new one the wrapper implementation

public class RepositoryLogDecorator  : IRepository
{
    public IRepository _inner;

    public RepositoryLogDecorator(IRepository inner)
    {
        _inner = inner;
    }

    public void SaveStuff()
    {
        // log enter to method
        try
        {
            _inner.SaveStuff();
        }
        catch(Exception ex)
        {
            // log exception
        }       
        // log exit to method
    }
}

What is she doing? In fact, it accepts a decorated object and proxies calls to it with logging. And if your code used to look like this:

var client = new RepoClient();
client.DoSomethig(new Repository());

Then now it will look something like this:

var client = new RepoClient();
client.DoSomethig(new RepositoryLogDecorator(new Repository ()));

Have we changed our repository implementations? No. Have you added functionality to them? Yes.

Some more info about similar tricks with classes here


As a result, I want to note that we have only touched on a couple of options, how to extend the functionality without affecting the implementation. In fact, there are more options, it is important to understand the principle here - you write a class\function\module, and they do only what they were created for, and we extend their functionality without affecting their source code. And do not forget about the principle of the uniqueness of responsibility - as you should have noticed, it is very closely related to the principle of openness / closeness.

I also repeat that it is necessary to have the flexibility of your data structures., which ensures compliance with the requirements of the project, and does not generate excessive flexibility, since excessive flexibility complicates your abstractions for nothing.

If you still read up to this point, then I will reveal to you a great secret - each of the methods above uses a particular pattern. Try to think about which pattern is where, run through the main patterns from the most famous pattern book (from the gang), think about which ones expand the functionality of the class, and then, hopefully, a mosaic it will start to add up.

 11
Author: tym32167, 2018-10-05 07:36:25