📜 ⬆️ ⬇️

Experience lectures on the introduction of design patterns

Allow a small preface - I will mark in it the purpose of the article.
I teach junior undergraduates an introduction to design patterns on Saturdays. Here, I want to share my experience, describe the plan of the first few lectures. Most of the readers, I believe, the material I have presented myself has been familiar for a long time, but perhaps the order and manner of presentation will seem curious.
Too often, alas, they tell us something, but they don’t say why they need it, or they even say it, but as if in passing. Say, usually speaking about C #, they will tell you what the base class and interface are, what syntax you need to use to write them, give an example where the base will be the Bird class and the Duck and Eagle heirs, but why? it is necessary, what a benefit from the whole - potentially complex - hierarchy of classes is achieved, they do not say: it is as if in the shadows, implied by itself. And that is why many students, who have not yet had time to fill their cones, have an inverted picture of the world in their heads - they have a good idea of ​​what tools they have in their hands, but understand why they are invented and what they are applicable to.
That is why I have compiled several case studies on which one can show why some approaches are needed. True, it is necessary to accept convention - we will beat from a cannon on sparrows, and even on imaginary targets. But we will shoot and we will precisely get into the enemy parapet, happen what.
I’ll say right away that we will quickly move from very simple questions to quite complex ones (let's say, to the linker), so read to the end, if not from the beginning.


Let's start with the fact that we solve the simplest way to any puzzle. Well, let's say we display n first prime numbers. We will act completely straightforward - we now have nothing to do with optimal algorithms.
static void Main() { Console.WriteLine("Input n"); var n = int.Parse(Console.ReadLine()); var primeNumbers = new List<int>(); int current = 2; while (primeNumbers.Count != n) { if (primeNumbers.All(x => current % x != 0)) { primeNumbers.Add(current); Console.WriteLine(current); } current++; } } 


Well, great. The program works. What's next? Well, let's say we publish it - let people use our module to calculate primes. We will gradually develop, improve, hone - and we are pleased, and people benefit.
True, then the question arises - will we give people the use of an exe-file that requires keyboard input? Usually, modules are drawn up in the library, hiding the unnecessary and exposing only the method the user needs. Let’s estimate his signature:
')
  public int GetPrimeNumber(int n) { ... } 


Suppose so. True, this is not exactly what we did before. We deduced n numbers, and here only the last. Well, add another method, now we have two of them. Immediately put them in a new class - just to lay together and contact them was convenient.

  public class PrimeNumberCalculator { public int GetPrimeNumber(int n) { ... } public int[] GetPrimeNumbers(int n) { ... } } 


Module module, however, can not forget about our program. It should work no worse than before. Let's do the mess:

 public class PrimeNumberCalculator { public int GetPrimeNumber(int n) { return GetPrimeNumbers(n).Last(); } public int[] GetPrimeNumbers(int n) { var primeNumbers = new List<int>(); int current = 2; while (primeNumbers.Count != n) { if (primeNumbers.All(x => current % x != 0)) { primeNumbers.Add(current); Console.WriteLine(current); } current++; } return primeNumbers.ToArray(); } } class Program { static void Main() { Console.WriteLine("Input n"); var n = int.Parse(Console.ReadLine()); new PrimeNumberCalculator().GetPrimeNumbers(n); } } 


Everything seems to be good. But our PrimeNumberCalculator class depends on the Console . What does it mean, generally speaking, depends? That means it uses. Our class, if we suddenly assume that the console stops working, will not work either. He requires it as a prerequisite. Dependence on the class Console is not very visible now, because the Console is a static class. We do not create an instance of it explicitly through new , but use it almost as a namespace. But still there is a dependency, you must remember about it.
We emphasize it. Create a new class that will simply invoke the functionality of the Console , but it will not be static itself. There is not much sense in this yet - we just want to emphasize the dependencies of the class Calculator
 public class MyConsole { // ,        ,       public void WriteLine(string value) { Console.WriteLine(value); } } public class PrimeNumberCalculator { .... public int[] GetPrimeNumbers(int n) { .... new MyConsole().WriteLine(current.ToString()); .... } } 


When we write a new MyConsole , we resolve the dependency - we get at our disposal the object we need to work. Generally speaking, it is considered bad form to resolve dependencies like this, directly creating the desired object. Soon we will talk about why. But first another question.

Suppose that the user to whom we provided our published module (which would consist of only one class so far) wants to write a program very similar to ours - he also wants to bring a sequence of prime numbers somewhere. One problem - it works on the web. He has no console at all.
Well, then why would he not take the ready-made array compiled by the GetPrimeNumbers method and not print where he wants? Although the file, at least in the html-markup. Is this possible? It is possible, but then the behavior changes - we get the number on the screen as soon as it is calculated, and our user will be forced to spend time on calculating all the numbers first, and only then typing. Not the same thing - especially if you ask the program to give out a 100-thousand prime number.
I will not distract the reader with a specific implementation of the output "somewhere, but not in the console." The simplest is to print to a file. So let's assume that the user of our module wants to print to a file.
What do we do?

There is a small digression: many of the readers probably got so sick with the PLO that they hardly remember, as they thought before. And the most frequent, the most straightforward solution that students offer me is to simply create a new method, something like “take the first en primes, typing to a file”. Moreover, the logic of output to the file and the logic of counting are mixed in one method.

A new step is to create some “writer to file”, which will be very similar to the previous “writer on the console”, and its goal in life will be to disconnect the output logic from the prime number counting algorithm. So, suppose we have:

 public class FileWriter { public void WriteLine(string value) { //      } } 


Then the frontal solution looks like this:
 public class PrimeNumberCalculator { public int GetPrimeNumber(int n) { return GetPrimeNumbers(n).Last(); } public int[] GetPrimeNumbers(int n) { var primeNumbers = new List<int>(); int current = 2; while (primeNumbers.Count != n) { if (primeNumbers.All(x => current % x != 0)) { primeNumbers.Add(current); new MyConsole().WriteLine(current.ToString()); } current++; } return primeNumbers.ToArray(); } public int[] GetPrimeNumbersToFile(int n) { var primeNumbers = new List<int>(); int current = 2; while (primeNumbers.Count != n) { if (primeNumbers.All(x => current % x != 0)) { primeNumbers.Add(current); new FileWriter().WriteLine(current.ToString()); } current++; } return primeNumbers.ToArray(); } } 


It is clear that it is impossible to live like this: at least for two reasons. The first is that we shamelessly duplicate the code. If we come up with a better algorithm, we’ll have to edit in two places. If we find and correct the error, we will fix it, probably in one place, and in the second we will forget. In a word, duplicate code is so-so occupation. The second reason is that we allow the user to output only to the console and to a file. And if he wants to go somewhere else? We have to change our code, compile and upload a new version, so what? We will satisfy all the whims of all users and create a class with two hundred methods that can print anywhere? Nonsense, rubbish. You can not do it this way.
The question arises - what to do. But in general - what would we like? Write a counting algorithm, which at the right time pulls some writer. And any. Whatever. Although the writer in the file, even though the writer on the console, even though the informer in the KGB .
Well and - hurray! - we have a solution. Even a few (base class, abstract class, interface). I, in such cases, prefer the interface, understanding it as a contract. The class performing the interface undertakes to comply with the terms of the contract (contract), and I, the user, no matter who exactly executes the contract, if only the work was done.
So, let's go:

 public interface IWriter { void WriteLine(string value); } public class ConsoleWriter : IWriter { public void WriteLine(string value) { Console.WriteLine(value); } } public class FileWriter : IWriter { public void WriteLine(string value) { //      } } public class PrimeNumberCalculator { public int GetPrimeNumber(int n, IWriter writer) { return GetPrimeNumbers(n, writer).Last(); } public int[] GetPrimeNumbers(int n, IWriter writer) { var primeNumbers = new List<int>(); int current = 2; while (primeNumbers.Count != n) { if (primeNumbers.All(x => current % x != 0)) { primeNumbers.Add(current); writer.WriteLine(current.ToString()); } current++; } return primeNumbers.ToArray(); } } class Program { static void Main() { Console.WriteLine("Input n"); var n = int.Parse(Console.ReadLine()); var writer = new ConsoleWriter(); new PrimeNumberCalculator().GetPrimeNumbers(n, writer); } } 


Well? We almost reached our goals. The user can choose from a set of writers that we have provided him or create a new one, specifying that the “writer” interface performs. By the way, note how we allow dependency on the writer — passing the object into the method. It would be possible, for example, to transfer to the class constructor, but this is a matter of taste and expediency.

But this is another question. Suppose we want to combine the functionality of several writers. Let's say I need a "writer on the console and in the file." Or "the writer on the console and in the file, and in the web (whatever that means)". What to do?
The first impulse is obvious: let's create this new writer, and let him write to all three sources. One problem - we are again duplicating the code. That is, we have already written all three separately, and now we will mix logic. Well, this is solved as follows:

 public class WriterToConsoleAndFile : IWriter { public void WriteLine(string value) { new ConsoleWriter(value).WriteLine(value); new FileWriter(value).WriteLine(value); } } 


Everything works, but there are drawbacks. We have already said that resolving dependencies by simply creating an object is a bad form. What's wrong with that?
Yes, look. When we wrote “a new writer to the console,” we clarified that it was in the console, but not anywhere. It could not be noticed by another one without changing our code, we were strictly dependent on the writer in the console. Moreover, from the writer to the console created in this way. Maybe I want to create the same class, but to change its settings - by default I’ll make a blue background and yellow letters. That is, I do not even use polymorphism (that is, I don’t pass the writer through the interface / base class), but I still get more freedom of choice, I’m giving more options to the user. Let him create the object himself and set it up at his own discretion. My job is to use the received tool. So, from the words - to the case.

 public class WriterToConsoleAndFile : IWriter { private ConsoleWriter _consoleWriter; private FileWriter _fileWriter; public WriterToConsoleAndFile( ConsoleWriter consoleWriter, FileWriter fileWriter) { _consoleWriter = consoleWriter; _fileWriter = fileWriter; } public void WriteLine(string value) { _consoleWriter.WriteLine(value); _fileWriter.WriteLine(value); } } 


Now it remains to be noted that we do not use the special properties of the “writer to console” or “writer to file” at all. We have no need to change the path to the file and generally know about its existence. We do not want to change the font color of the console - it was not we who did this, but the one who created the ConsoleWriter instance. This means that IWriter is enough for us . We will not ask more than you need. Here is:
 public class WriterToConsoleAndFile : IWriter { private IWriter _consoleWriter; private IWriter _fileWriter; public WriterToConsoleAndFile( IWriter consoleWriter, IWriter fileWriter) { _consoleWriter = consoleWriter; _fileWriter = fileWriter; } public void WriteLine(string value) { _consoleWriter.WriteLine(value); _fileWriter.WriteLine(value); } } 


But now, after all, other writers can transfer us to the designer. Not "in the file" and "in the console", but whatever. Well, that's good for our purposes. We have just created a class that can unite any two writers. Where there RSP!
Now there is only one step left: from two to an arbitrary number. Here he is:

 public class ManyWriters : IWriter { private IWriter[] _writers; public ManyWriters(IWriter[] writers) { _writers = writers; } public void WriteLine(string value) { foreach (var writer in _writers) { writer.WriteLine(value); } } } 


Hurray, we got a linker.
What else? Still not enough? Yes, not bad, but one thing. Now it may not be so easy for us to assemble a writer. Create and customize one, create and customize another, then combine them with the help of the linker ... Difficult, right? You can exercise for the sake of creating a file with the settings, where it will be written, what writers we want to use.
And our new class will do what reads the settings file, creates the necessary writers, sets up the settings, puts them in one ManyWriters, closes the IWriter interface so that nobody else knows what program we know exactly which writer we use, and pass its on the way out ... And it will be a factory.

Source: https://habr.com/ru/post/276699/


All Articles