📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

Функции. Разбор книги "Чистый Код" Роберта Мартина #1 | Организованное программирование

Организованное программирование | Кирилл Мокевнин50:00

Transcription

Friends, hello. This is the Organized Programming podcast. I am your host, Kirill Makevnin. And today I have no guests, because today's episode is a breakdown. A breakdown that I've wanted to do for a very long time. And today we are breaking down the book "Clean Code." I'm opening the text directly, looking at the things Martin wrote there. And I'm giving my opinion on this topic, because it comes up periodically. People periodically ask, people continue to learn from it, continue to follow, try to follow what's in it. In principle, I've stated my opinion many times. It's understandable that anywhere there are some things that look reasonable, good, and all that, but for me, Martin's book is still perceived as a very unsystematic thing, because it just says: "Look, if it looks like this, then like this, if it looks like that, then you can do it like this." Without setting any fundamental principles beyond that, or, by the way, even not reproducing them correctly in places. I'll show a few examples that lead to a person being filled, it seems to them, with meaning. They get a lot of patterns in their head, approaches to how something should be done. At the same time, because there is no, uh, base of some kind, it all still doesn't lead to really good code and an understanding of what good code is and what bad code is. [music] Again, it's not about the fact that the things it describes are all bad, far from it. It's just that most often they are a consequence of other things that are better to learn, understand, and remember once and then apply everywhere. Plus, of course, there is the specificity associated with the language itself. Because Martin, firstly, he wrote all this a long time ago. When he wrote it, it was very much tied. And still is tied to Java and Java-like languages. Therefore, a large number of principles described here, they, in fact, do not help people on other languages at all. And what's most interesting, Martin himself has tried a lot and delved into many things since then. For example, since then he's tried Ruby, he's tried Clojure, and generally, if you read and look, he's changed his mind on many things. He started saying: "Oh, and this is like this, and this is like that." Plus, of course, those who follow his blog or his Twitter know how much he gets criticized and how other guys do their own breakdowns and express their attitude towards it. Again, it's all very strange here because he is not some super-mega-recognized expert in the technical sense, because the person works for himself somewhere, does some things. That is, in this regard, he is further from industrial programming than many of us, who are engaged in it. And yet, he is most often talked about the most. He somehow earned this fame, received it. And, well, that's why we're breaking all this down. That is, roughly speaking, if this book wasn't talked about much, I probably wouldn't pay much attention to it, but since it is talked about and constantly cited as an example, it's impossible not to talk about it. So. I am by no means a hater of Martin, but because it generates a lot of misconceptions, it needs to be analyzed, of course. By the way, there was such a boom about five years ago, even though the book was written much earlier, there was a boom 5 years ago. Everyone made such videos. Well, I watched them a bit the other day and decided to participate in this as well. At the same time, they analyze many of the initial chapters and very little of what comes at the end. Therefore, I will try to do it differently. I will start immediately with functions, where there are already some interesting points, and try to get almost to the end. We will not touch upon topics that are either super-specific to Java, or, for example, multithreading, because this is not a topic of code from the perspective of abstraction, it is a more technical topic on how to write, not write multithreaded code. Therefore, it does not concern what we are discussing, what we call clean code. Well, let's go. I'm opening it, showing you on the screen what's happening. It seems to me that this episode can be listened to, if you can't watch it, it can even be listened to. But let's see how it turns out. It will be interesting to try this type of breakdown. If, by the way, it's well-received, I will do such things regularly, because there are many books, there is something to read, there is something to discuss. And not just books, articles, after all, too. So, here it is open. Let's see. So, um, the chapter on functions, page fifty-five. Let's go. 555. Functions. Again, I will not read everything. I will only select things that are most interesting or where there is something to talk about, to argue about. That is, if I skip something, either I agree with it, or it just seems uninteresting to me to analyze here, but it's not a fact that I agree with it, so you'll never know in advance, as they say. And yes, I also won't delve deeply into the code examples themselves, because, firstly, these are listings, well, not for one screen, plus it's Java in the sense that not everyone here knows Java. Plus, they are also without highlighting. And reading code from the screen is not great. Therefore, I will mostly skip this part. Maybe we'll look at the code somewhere. But, honestly, if you analyze the code he writes here, there are many questions, because, in general, when we talk about clean code, we also need to say this, the examples they like to give are, of course, quite trivial. That is, it rarely has anything to do with any real applied programming. That is, either it's ducks, cats, which is nowhere and simply doesn't exist. Or it feels like we're writing libraries. That is, code examples with applied code on typical frameworks. If we're talking about web development, frontend, backend, then there are no such things, they almost never happen. It happens differently there. For example, a service for working with some kind of acquiring, something related to payments. Here, he shows things that are more related to slightly different systems or to what is usually used in library code. Here I want to say that these are very different things. And looking at one, it's not that obvious how to write the other. That is, people who know how to write libraries and understand them are not the same people who know how to write applied code. So sometimes, even if he gives a logical example or explains a topic, I think it's quite difficult to apply to our applied code without understanding the whole picture. Precisely because these are more specific examples that in the real life of web developers, well, are unlikely to be encountered, at least in this form. So, well, let's, uh, try to move on. Here he talks about some things related to, like, abstraction levels, naming, and other things. Okay, we'll skip that. Let's talk about compactness. The first rule, functions should be compact. Second, functions should be even more compact. He says that he cannot justify it, but, in general, he wrote various things. I mean him, because I'm reading the book. And he talks about how functions should be made as short as possible now. And, by the way, he also tells an interesting thing, that when we had procedural programming, there was an approach where everything needed to be written with one exit point, that there should be one return. All of this, frankly, is not very relevant in the modern world, because got is not used. Plus, if you make a single exit from a function, it's quite large, there's a problem of this kind, that, for example, you've done the necessary calculation, which, in principle, is everything, the further code is irrelevant, then if you don't exit at this moment, you need to write all the further code taking into account this case, which in principle has already been calculated, and therefore does not need to be calculated. And in the end, the approach with a single return leads to the fact that you will have to think about all cases at every moment throughout the function's code, even if these cases could have already been completed. And in reality, this only makes the code more complicated. That is, it's impossible to make a function simpler with one exit than a function with multiple exits. Simply because a function with multiple exits in simpler cases removes them from your head. That is, return is not just a function return. It means that you can simply not think about it further, otherwise you will have to think about it. Therefore, various additional variables appear, additional data needs to be stored, additional checks need to be made. In short, it's always worse. Therefore, the approach with guards is much better in this regard. That is, if you first handle some cases that can be determined, calculated, and completed as quickly as possible, and make a return. Aha. Below you understand that, minus one possible scenario, it's already easier. And so on until you are left with the scenario where it is the most complex, the deepest, the most advanced. This is a general point. Now, regarding the number of lines. Four lines, all that. So, it's sold a million copies. I specifically looked at what foreign bloggers and various specialists are saying about this topic now. By the way, you know, I even wanted, frankly, to do a breakdown of their breakdowns, because there are quite well-known guys who got together and said: "Well, let's discuss Martin, what's what, what do you agree with, what not." And this was very recently. I don't remember his name. He's a famous guy who is now very much into P. Yes. Well, I know that I'll immediately get comments. I've just never pronounced his name out loud, so I haven't memorized how it's pronounced. In general, the idea is very simple. The fewer functions, the more you, at some point, on the one hand, break them down into smaller functions, generally extracting some logic that by meaning should be extracted and hidden, so as not to think about it, on the one hand, we improve understanding, but frankly, there comes a point quite quickly when breaking down into functions, no matter how readable you make it, no matter what great arguments you write, leads to the fact that you have to jump around too much to get the whole picture, and it becomes much more difficult for you. This is a classic story related to distribution. That is, the more you distribute, the more complexities arise in synchronizing everything. In this case, in the head, it is often much easier to write one normal function of about 100 lines, and at the same time, inside the function, you have independent blocks that somehow work, because refactoring such code is usually much easier. Of course, there are a billion conditions and principles here about where we work, with what, what languages, how we work with side effects, and so on. That is, of course, you can refute everything I've said, and provide counterexamples and prove me wrong. But in any case, industrial experience shows that functions can be very large, and even more important than their size is how it is actually related to the task being performed. That is, how much is it one level of abstraction inside. Well, simply put, there are functions that do some things that, well, computationally, might be large, but at the same time affect external systems little or are purely internal logic. And in such a case, it's not so important, in fact, what size it is, because it doesn't affect anything much and your context doesn't get diluted, and you, for example, don't interact with any systems. And it happens that a function is super short, but it's involved everywhere or changes too much neighboring state. Well, of course, this is a completely different situation. Therefore, perhaps I want to warn against what? Do not consider it dogma that functions should be super short. One way or another, most people who have tried to strictly, blindly follow this approach, when a function is barely a couple of lines, all the cases I know, all the acquaintances who did this, all the people who wrote about it, said that, damn, at some point it becomes too difficult and you just lose the forest for the trees and generally don't understand what's happening. I probably have such an example in this regard. In frontend, it's great to talk about this. Here's a component, right, it's a function, and in React, for example, and it can be, well, say, 500 lines. Is that bad or not? No, not bad at all, because you just, roughly speaking, form a DOM tree, a piece of HTML, if you simplify it a lot. And it doesn't need to be extracted at all, if everyone is fine with it, if this component is only used this way. If parts appear inside it, for example, we want to use them independently or they are repeated inside or repeated in other components, then we extract them. But it rarely affects the complexity level. By the way, he also gave an example about navigation, he wrote somewhere, but there are no particular problems with navigation now, because in any editor everything is highlighted perfectly, various identifiers, LSP transitions, in general, beauty. So there are no particular problems here. In short, the size of a function is not critical. The abstraction you extract is critical. And if it's, well, sufficiently isolated within itself, in principle, it can even be quite messy inside. Nothing terrible. The main thing is that we have extracted it correctly. But here, the only thing I want to add is about extracting functions. I've even talked about this in presentations, it's primarily not about code reuse, but primarily about semantic abstraction. Why is this important and why is it not the same? Because a code editor, for example, can actually give you a pig. It, for example, sees some piece of code that is reused, and it can offer to say: "Oh, let's extract this function, look, it's used like this." But from a semantic point of view, it's not a function. That is, you will have, roughly speaking, within one function, completely different things connected that are not related to it. It turns out that you've made a function, but in fact, it's just a block of code extracted, although in itself it's not a semantic block. Therefore, a function is almost always about defining what problem and task we are solving. I'm afraid to say words like "single responsibility" because I really dislike that wording. Why? Because it creates the impression of "do good, and good will come" and as if we all understand what we're talking about. Again, experience shows that when you take different programmers, sit them next to each other, I mean, on the contrary, they don't see each other, you give them the same task and say: "Here, please, implement it for me." And they say: "Yes, we've read Solid, we've read, you know, other approaches." And they say: "Here, you know, one reason, many formulations, one reason for changes, single responsibility, does one thing." And then, when you start looking, it's always a very situational thing that depends on the context. That is, for example, what do we consider one task? Your class can be an entire application. And this is how it's done in many frameworks. This is a single responsibility, and a lot happens inside it. Or, for example, let's take some kind of report. For example, a function that generates a report. Single responsibility. And if inside it's a giant complex system, when you need to, for example, generate cells in a tricky way, something else like that, different formats, is that one responsibility or not? And whatever you say, however you think, it seems like: "Oh, I understand everything here," the system unfolds like this, we take a second programmer, give him the same task, and he will have a completely different breakdown, except for cases where they had experience creating such systems. Because, as a rule, people who have mastered a certain topic, something starts to work out for them. I'll say a bit more about this later. So, let's go on. Indentation blocks. This is not interesting. Uh-huh, the rule of one operation. Yes, it's quite obvious that a function performs multiple operations. It provides buffers, retrieves data, searches for page inheritance. Well, he's showing an example of a function that does everything. A function should perform only one operation. It should perform it well. It should do nothing else. For me, this sounds beautiful, but it means nothing in practice, because in any case, for example, the same report, I say, you will still have a function that, in the end, generates the report. And inside, even if you've broken it all down into small functions, there will be a lot of completely different actions. And then it turns out again, we can extract this into different functions, or we can not extract it. From the perspective of the function that generates the report, it's a single entity. And the fact that along the way you also need to go to the database, do a lot of different things, well, that's secondary. Then, of course, the question may arise: "Okay, and if you need to go to the database for data? Well, let's extract this logic." We can. But excessive enthusiasm for isolating side effects and abstractions also brings problems, because in such a case, you will get code with too many abstractions. That is, this is a typical story in Java, when you look and you have it just like a matryoshka doll stacked on top of each other before something happens. That is, instead of performing a simple operation, you have a pile of these factories, abstract factories, and so on. By the way, this will also be given as an example, and we will talk about it a bit later. So, um, here's an important point I wanted to make. If you look at the real practice of creating applications, and when we talk not about applied applications, but about library code, can we say that after reading this book, other books, people who go to create their own libraries, they are like: "Oh, now I'm doing it right, look, I've broken it down correctly by functions, I've understood everything correctly." The answer is, it doesn't work like that at all. If you look at the development of libraries, especially major versions, of any language, then everything, as a rule, depends on the experience that the developers gained in the process and understood what mistakes they made and how people use it, and what limitations, difficulties arise with the interface they created or the possibilities that exist in this little library. And only after that they understand: "Ah, we made a mistake in the design of this system, this library, let's change it." And they release, for example, a new version, in which they, as a rule, it's backward incompatible, in which they make significant changes, extensions, etc., and rewrite everything inside. And this often applies to frameworks themselves, it applies to libraries, well, and in general, to these kinds of things. And did Martin's advice help them, for example, that they said: "Well, we did everything right with the first version." No, not at all, because even the redistribution of modules that will be there is different, because usually there is a complete re-awareness of all this good. Therefore, on the one hand, we cannot do it right the first time in any case, until we have the appropriate experience. And therefore, by the way, those who do something for the first time are weaker. That is, if I, for example, specialize in creating, I don't know, frameworks, or if we're talking about applied code or libraries, in creating, I don't know, frontend frameworks, then at some point it will turn out quite well, yes, even the first time. Otherwise, it will be [ __ ] no matter what book I read. There are, of course, some principles that allow you to do something better, but on the whole, you need to calm down and go with the flow, because no one has ever done it right the first time, even with the best experience. It doesn't work like that. Fortunately or unfortunately, it's more likely to help you calm down a bit and just reflect more on the fact that, okay, we launched, looked, changed, improved, and so on. But not all changes make sense to make. For example, the transition from Python 2 to Python 3. Okay. Let's go on. Well, further, in principle, you can skip all this. He's rambling about the idea of one abstraction level. And yes, one abstraction level is worth a little more detail. Indeed, there is a concept that when we work, for example, we open a function, a piece of code, and there are certain lines, right, that we work with, then, of course, ideally, it's desirable that it's indeed one abstraction level. But it's not always obvious what it's all about, because, for example, in one place we read from the database, here we work with a file, and here we work with models. And so, for example, simultaneously working with a file and working with models of my domain, is this one abstraction level or not? Someone will say yes, definitely, it's not one abstraction level, and so on, but even if you hide it behind some function, working with the file will not disappear. And considering that this is still a side effect, this is, frankly, a kind of illusion of isolation, in reality it's not complete isolation. Perhaps one of the most striking examples of violating the abstraction level is this. Those people who, I suspect that most, one way or another, who are watching, have encountered this. This is if we take a classic backend, some framework, and, for example, you have a service and you work with models, right? Here you have models, and you, accordingly, um, I don't know, make some selections, well, not even selections, but, say, you've selected a model using the standard mechanisms of your framework, and then you perform some logic with them. And if at this level, at which you are doing this, and this is most likely, by the way, a service, such things are also done in a service. If you, at this level, have been working with models, working, working, and then suddenly, you make a raw query to the database, well, this is clearly not the abstraction level you should be working with. And this definitely needs to be extracted. And here it's not about the size of the function, not about the size of the code, the query, and everything else. The problem here is this. If you start writing such code, then in one place, well, if it appeared, you can definitely live with it and somehow change it further. But it creates an unpleasant situation. That is, it shows other developers, especially younger, immature, new ones, that it's normal. They look at this code, and they think: "Well, that's how it's written." They might not even reflect that there's any violation of the abstraction level here. They just see that this is the code that we have. What does this lead to? It leads to them copying this code later. They will continue to do so in other places, because, well, we've done it this way before, and no one explained to us that this is a different abstraction level. That is, you also have to guess this. That's why such things are better to write correctly right from the first time and think about it, if you understand that you are not the only one who owns this code, and you control everything you need. Because if I write alone, I can say: "Okay, I did this because I can do it quickly, and the second time I need it or I get to this piece of code, I'll refactor it." This only works if you are the only one working on the code. And then you can think through any step in advance and not be afraid to make a mistake that you will then fix yourself, because you know how you will work. If you work in a team, this never works. Copy-paste always works, and only debt grows. So, reading code from top to bottom, the rule of descent. I read, read, read, and I didn't quite understand what he wanted to say. That's why I'll probably skip it. Frankly, I don't see anything in it. That is, it's clear that he's trying to say how functions should be arranged sequentially by abstraction level, but, in general, your functions are usually scattered all over the place and generally

Here is the translation of the provided Russian text into English, following your rules:

That, with which you are working. Therefore, it is not located in one place, therefore, frankly speaking, it looks a bit strange. Well, if we are talking about doing it within a class, well, that's nothing fundamental. Okay, next. Writing a compact Switch command is quite difficult. commands together, yes, more space. This is a bit of manipulation, because he invented a problem for himself and then answered it himself. That is, he said: "Functions should be short." And then he says: "So, Switch doesn't fit this concept." Therefore, one must approach this with a certain degree of skepticism. Right. And when he writes: "Unfortunately, it's not always possible to do without a switch." No "unfortunately." Simply, if you have a task to switch, it's an absolutely normal task. With about the same success, one could say that, unfortunately, it's not always possible to do without an If. Come on, it's just a normal part of programming that everything is set up like that. And then he shows a function that has nothing but a switch. And he explains some shortcomings, like, first, it's large. Well, hello. You showed a function with only one switch. I've already said that the attitude towards this is, thank God, now more or less reasonable in the developer community, that you shouldn't get so hung up on the size of a function, and it doesn't lead to anything good. Right. And when new types of workers are added, it will grow. In short, and here he actually leads to the idea that, guys, we have, like, um, look, strategy, ah, let's make a common interface. Ah, let's, you know, replace this with essentially polymorphic behavior, and then none of this will be there at all. This is a normal story, and such things happen, it happens when you need it. But, actually, you don't always need to try to solve absolutely everything with polymorphic behavior. Well, that is, when the type is simply substituted and we don't do it with cases, but you actually have the logic embedded in the implementation, in some, say, class, if you're writing in a class-based language, but it's not necessarily so, it can also be done without classes. And in that case, yes, you will have something like a switch, but this is a very special case of using a switch. And when specifically we call logic depending on the type, it's closer to the type, not always, but closer to the type. It even has the type in the example. In fact, it's a bit different. This is what should have been shown in the section called not "Switch is bad," but "specifically, type dispatching with a switch of this type, you implement OOP yourself, right?" Because OOP implies the use of polymorphism. Although, in reality, even those who write in OOP in such languages understand that you shouldn't shove polymorphism into every place, a switch is sometimes good and normal. But otherwise, you'll get tired, and the number of interfaces and all sorts of such things will be infinitely large. Not everything should be 100% configurable and provided through some container. Simply because any abstraction, any attempt to generalize code, frankly, makes it more complex. That is, at first it all seems great, fun, wonderful, but at some point, if all your code is like that, you will simply get lost in the amount of abstraction and complexity. Here he writes further. Secondly, it clearly performs more than one operation. Thirdly, it violates the principle of responsibility. Oh, just a function that just does dispatch. Right. And it doesn't violate anything. Oh, come on, I won't argue with him here. In short, he has pulled in all the sins he could. Well, mostly his own, which he defines somehow. Right. Ah, well, no, I'll say one thing after all. Fourthly, oh, it violates the open/closed principle, because the function's code must change with each addition of new types. Yes, it's absolutely true that if you don't use polymorphic behavior, where you can set a configuration somewhere, and it will be used throughout the code, then indeed it is so. But again, he proceeds from the fact that he himself described it, he himself said that it is important, he himself said that life is impossible without it. And like, let's do this, it's a fundamental principle of programming. No, the open/closed principle is not a fundamental principle of programming. If your entire program is built in such a way that it only changes through configuration, it means that it is incredibly complex and has an incredible amount of abstract code inside. This should only be done when you truly have such a need. Moreover, by the way, it is most often done in tools, that is, in libraries, in some things that can be configured, but not in application code. That is, a framework, yes, it is often like that inside, everything can be replaced. There are rarely such cases, in fact, everything is based on abstractions. And when you write specific application code, for example, you have a user model, you simply instantiate this model and work with it directly. You don't even go through a container. This applies not only to models, it can also apply to any infrastructure or additional things you have in your code. So the principle is understandable, it's all clearly important to understand. So I have no questions here at all. It's great when you realize the connection between switches and polymorphism, ifs, and substituting implementations, with dynamic extension of behavior by not having to rewrite code, but without dogmatism, that you understand where you use it, where you don't use it. But if you don't understand this completely, then, of course, dogmatism arises. Look: "Ah, he wrote this, so it must be right." Yes. But everything ultimately comes down to understanding polymorphism and how it works. Specifically, we are talking about subtyping in this case, i.e., subtype polymorphism. Not to be confused with parametric polymorphism. Well, then he says that, like, let's build an abstract factory, don't build an abstract factory for every little thing. Well, yes. And then he talks a bit about polymorphic objects and something else along those lines. By the way, he mentions one more thing here. I won't dwell on it deeply and broadly. It's called, by the way, it's often funny. He doesn't use a conceptual apparatus that is known to people, that is described, that they know. He starts with his own words or gives something slightly incorrect. That is, when he describes that in one case, that is, you have a switch or types, in one case it's easier to add behavior, in another case it's easier to add new types, and actually this thing has a name, it's called the expression problem. If you've never encountered this, read it once. It's very useful. It's precisely on the topic of replacing switches with polymorphism. There is indeed such a trick that if you distribute logic across types, it's easier to add a new type, dynamically, all that, but it's harder to add an operation, because you have to add it, like, everywhere, to all types, right? So it's easier for you to add a new type, and new behavior, which is formed by a switch in one place, it's easier for you to add to one switch. Well, this is assuming, of course, it's not used in fifty different places. I just thought about it, maybe, uh, I sound too abstract, because when I talk about it, it all seems very logical in my head, I can see the code. But it's quite possible that not everyone has these pictures forming in their minds while listening. You should write about it, it's also interesting, because it's quite possible that when listening to such videos you'll say: "Damn, Kirill, sorry, we can't keep up with you at all, because I'm doing the dishes or something else." And it all sounds too unclear, simply because I don't see the code in front of me. Uh, just for the future, so that I understand, is it worth digging so deep or can it be presented differently. Let's go. Use meaningful names. Well, okay, agreed. Function arguments. Ideally, the number of function arguments is zero. Absolute nonsense, not at all. What he says is a nightmare. This is essentially either the use of global state or implicit state, where everything is hidden, everything is, again, from the fact that he writes in Java, everything is in objects, and everything, you know, everything is hidden in the object and all operations are performed. Any functional programmer will tell you that this contradicts normal common sense, because, on the contrary, all state should be explicit, passed through arguments, through function parameters. And it's not about them being terrible, having to be many, and so on. It's about having explicit state. Of course, there's a balance somewhere in the middle, because, well, still, if we write in Java, no matter how you twist it, we have objects, and we live with that. But at the same time, trying to push all the state completely inside somewhere and only work through functions that don't take any parameters at all, well, nothing good will come of it. I can say that right away, so it's always a matter of meaning. It's not about the number of arguments, parameters that a function takes, but about what problem you are solving. and how it fits into your architecture. By the way, I, for example, don't write mobile apps, but when I saw Android code and tried to do something, I remember how surprised I was that many functions there take really 10 arguments. Moreover, when you pass them, you first need to pass, say, five, in some order, to get to the meanings you need. This looks like overkill. And again, by the way, this is designed by people who make Android and so on. That is, serious guys who read serious books, work in a serious company, Google. And in this sense, at least named arguments would help them, yes, and at least that way you wouldn't have to pass them. But perhaps this solves a consequence, not a cause, because the cause itself is that there are too many arguments. I can't say here. In real application code, I've rarely seen such abuse of parameters. Only mobile apps, perhaps, I've seen that, and even then at the level of standard libraries and frameworks, not application code. I believe someone somewhere writes like that, but I don't think, frankly, that it's such a big problem, because function arguments, frankly, are a consequence of how people design. That is, in this way, you can invent anything, derive another 500 principles, but in reality, we are talking about how a person designs their system and identifies abstractions and works with what's inside. Right. Okay. Let's move on. Vo. Flag arguments are ugly. Passing a boolean value to a function is a terrible habit, no less so for this method, blah blah blah blah blah, proclaiming that the function performs more than one operation, this is not entirely true. On the one hand, I agree with him, and flags are usually a bad idea, but not always. It's a different function. I can give the simplest example, like Markdown. That is, when you render Markdown, you usually have a huge number of options for how to render it: display links, don't display, do 1, 2, 3, 4, and you can give many more such examples. Is this a problem? And, unfortunately, we pass flags there and do something. Yes, no, this is a normal task, absolutely logical and standard. That's how it's set up everywhere. The difference is simply that flags are passed in such cases not as simple arguments, but as a special configuration object. But here, of course, it depends on the language, that is, somewhere in TypeScript or some dynamic language it's easier, because you can just pass an object. And in some Java, it's much more complicated right away, because, for example, if you have a set of configuration parameters that need to be passed, you immediately get another class, like Configuration, something like MarkdownConfig, which you need to create separately, fill in, and, accordingly, pass it on. Or, yes, it turns out that when people look at all this, they think: "Why bother?" And they just shove 100 flags in there. Well, yes. So the problem, frankly, more often, I think, arises in such languages where the simplest story with passing parameters, configuration parameters, requires the use of classes, and not just a simple literal object syntax, as, for example, it works in JS TypeScript. By the way, therefore, yes, verbosity, of course, is much greater in such languages. So, a function with two arguments is harder to understand than a unary one. Well, that's nonsense, damn it. If you have a sum function that takes two arguments, there's nothing complicated about it. This is already stretching it. You need to look at the meaning. Okay. Let's move on. Objects as arguments. If a function needs to receive more than three arguments, it is highly likely that some of these arguments should be packaged into a separate class. This is where the substitution of concepts occurs. A person who hasn't programmed in Java and reads this book might misunderstand what it's all about. The problem is precisely that in Java, you can't just take and pass a set of parameters. They need to be packaged into something, and you can only package them into a class. That's why he talks about it. But in fact, what does he mean? That they can be passed as a set of options. And in this regard, he is absolutely right. Very often, there are very important parameters in functions, without which it's impossible. These are exactly what should be passed as arguments. And there is, for example, a set of, say, the last parameter, which is called options. And there you can pass various configuration things. And yes, in Java, this needs to be done with a class. Well, not only in Java, but also in C#, in Kotlin, I think it's not much different. Well, at least at the time when I dealt with these languages, that's how it was. Who knows what else they've invented. And then verbs, keywords. Well, it's clear that these are verbs. Oh, let's talk about side effects. A very important topic, in which Martin slightly confuses concepts. Now we'll talk about what is meant. So, side effects are essentially lies. Your function promises to do one thing, but does something else, hidden from the user. Sometimes it makes unexpected changes to its class variables, say, assigns values to variables or system global variables. In any case, such a function is a cunning, malicious lie that often leads to the creation of unnatural temporary dependencies. For example, let's take a seemingly harmless function from the listing. The function uses a standard algorithm to check a username-password pair. It returns true, false, there, blah blah blah. Well, and the function has a side effect. Can you find it? He gives an example of a checkpassword function, which has session and, well, session initialization inside. Why? I believe that what he is doing is not very good. It creates a slightly distorted perception of reality. The fact is that side effects are important. He has examples of constant side effects everywhere here. And further, before this, he showed various write functions and so on. There are side effects everywhere. He never said anything about it, because he talks about side effects, but in reality, he shows a different problem. That is, it is related to side effects, but he incorrectly defines it as a problem of side effects. What he shows here is a very well-known, common principle that I often use as an example and say that it is one of the most fundamental. And we encounter it literally in code every day. Everyone who writes code. It's called Command Query Separation. That is, roughly speaking, he has another way to describe himself. It sounds something like this: when asking a question, do not change the answer. That is, if you are writing some predicate or simply retrieving data, then, of course, you should not change the system state in any obvious way, right? That is, if you ask if the user is an admin, if the password is correct, then, of course, nothing should be changed at this moment. If you have a command, write something, change, create, then, naturally, there will be a side effect. And this is not just normal, it is correct and should be so. Therefore, when he writes that side effects are essentially lies, well, frankly, this is a phrase, well, I don't know, you can't write such a phrase here. It's not about side effects, it's just that in places where you need to read, don't create side effects. At the same time, of course, if we start digging deeper, everything is a bit trickier, because we'll say: "Okay, but what about lazy initialization, for example?" For example, there are such things, the same Stack Overflow, or articles in general, right? You open an article in a browser, and the article itself is a read. In principle, everything in the code should be only read. And by the way, that's why you might think: "Oh, I'll only read data from the replica for reading, right, to efficiently distribute the load in the backend." And then it turns out that you have a view counter, and you think: "Oops, it seems like we're reading, but in fact, we're also changing this counter." And there might be conditions and rules. So on the one hand, it's not always possible to avoid, and sometimes it's part of the business, and then no programming will help you. It's simply against business in this case. Second. Sometimes it can be done imperceptibly. That is, there are languages or systems that allow you to do this, because not everywhere can you do this, when reading turns into a side effect related to lazy loading. And this is also a normal approach. Why? Because from the outside, with proper implementation, a person will never know that there was a side effect there at all. That is, everything will look as if it wasn't there. Well, and if there is actually some operation that needs to be done, well, it should simply be a command. It shouldn't be checkpassword, it should be setpassword or something along those lines. That's all. At the same time, this does not mean that side effects should not be handled in any special way. Not just should, but must. It's just that they need to be tied to something else. And usually this principle is called the functional core, imperative shell. That is, the idea is what? That the code that deals with logic is maximally separated from the actual side effects. At the same time, side effects are moved to a separate level, right? That is, we save, read, do something separately from the program. I will give a very good example now that shows this well. For example, we have a simple console utility that takes a file, for example, CSV, as input and creates a report from it in another format, in XML or, ah, Word, and outputs it. So, a very simple program. At the same time, inside, well, how much there, well, let's say, there are 1,000 lines of code. And I periodically conduct such an experiment. I ask different guys, I say: "How much code with side effects do you think is in this function?" side effects, right, we're not talking about side effects in terms of changing some variables. That is, it's clear that you can write the code directly in very different ways, but I'm more interested in interacting with external systems in general. And always when people talk about it, they say 50%, 7%, 10, some percentage. I say: "Well, look, by and large, there are so few side effects here, even in percentage terms, that they can be counted on your fingers. There are simply two. The first is to read the file. The second is to write the file." And the main logic of your program in this case is, in fact, the formation of this report. And it turns out that, in fact, with the correct construction, you have reading the source file and writing it there at the top of the program, and in the middle, a function that directly builds this report. And, accordingly, you thus have separated responsibilities. And with this approach, with this view, the imperative core, meaning the functional imperative core, you can draw many, many different conclusions from this about how to write code in general and what to pay attention to. An attentive listener and viewer of my podcast will say: "Kirill, it's not quite like that." Because if you write the program like that, it's great, of course, but if you have a file of a couple of gigabytes, you'll get very inefficient code. Because it loads everything into memory, consumes a lot of resources, and, accordingly, causes problems for everyone. And to implement this correctly, you need to do streaming. True, it's all like that. You need to read data in chunks and spit it out, but this doesn't mean that the logic itself is intertwined with this streaming. That is, it can be very competently separated. Although, of course, the side effect itself will be extended, but it can still be quite well isolated. Let's move on. Output arguments. Arguments are interpreted as input data for the function. Anyone who has surely encountered the need for additional checks of arguments that actually turned out to be output, not input. Example appendfooter. Does this function append S as a footer to something else, or does it append some footer? The problem seems to exist, but the problem, frankly, is related to the fact that we are dealing with implicit state. By the way, he also talks about something that has fundamentally changed over the last few years, I think in the last 5 years very significantly and has ceased to be a problem. So, he writes here about looking at the signature. He often, by the way, gives this example, that you need to look at the signature, you need to look at the signature. Without the signature, we won't understand, and everything is very bad if we don't see the signature. The fact is that in all modern editors, whether it's Vim, whether it's, whether it's, all types are shown. They are not in the code, but they are shown in the file, right? That is, we, for example, define a variable through type inference simply as const or something else depending on the language, but in fact, it immediately shows what type it is. The same applies to lambda functions, when you describe them and there is some data inside, they highlight everything for you. Definitions of calls, and so on. In short, this is now built into everything. And the problem of looking at the signature, well, it doesn't exist, we just see it, and in the code by default in all modern editors. This does not replace any stories about the code needing to be understandable, but the problem itself is, as it were, absent. Right. And what he is trying to describe here, well, this needs to be discussed separately in more depth about design, because he starts to talk about it, giving a bad example. And I believe that the principle of code organization itself. And when he shows here, in fact, the reporter appendfooter, I completely disagree with such code. That is, he initially wrote code that, in principle, should not have been written, and then says: "Oh, this is incorrect code." At the same time, he shows something else as a solution. Well, as I already said, he removes arguments, completely transferring them to state inside. This means that this is how code should be written. Right. Well, let's not go into detail, I'm saying, here you need to take that reporter that he invented and see why he wrote it that way at all, because I would have written it differently. He is, as it were, writing about separating commands and queries, which I just explained. But the trick is that he does it separately. That is, what he put about side effects above, he didn't connect with it. And therefore, people who read this, I am sure, will not even understand what it is about, because nothing is said about side effects here at all. Well, on the one hand, he says to return information about this object, but here, first of all, the binding. What does the object have to do with it? This is too much of a Java thing. Secondly, he again misses this story related to side effects. That is, he talks as if it's just about changing object fields. Well, no, it's not about that principle. Although the specific case, yes, this is another problem in this book. He takes something that needs to be discussed more seriously from all sides, so that a person understands the concept. He gives it, you know, in what style? In the style of shorts or reels, when it's very short, very catchy, so that something gets into your head, but in the end, you come out with garbage, because you don't form a social picture. That is, it's not a normal, sequential description of the code building principle. It's just like a flash, bam here, bam, here, bam, here, and not connected. And because of this, I don't believe that a person who has read this book can come out and, for example, in detail, as I am explaining now with examples, talk about separating commands and queries, the connection, you know, with side effects, these cases, and so on. I need to write my own book on this topic. Although I'm sure I've read this somewhere myself, many times, in different places. Well, it's interesting, is there such a book that describes all this in detail in this way? That's a good question. Use exceptions instead of returning error codes. Yesterday, when I was preparing for the recording and reread his book a bit to see what he writes here, I laughed at this point, because when he wrote this, few people were writing in Go yet, and now it's very funny that everything he writes here completely contradicts. That is, he shows it as an absolute evil. He says that functions returning error codes is a clear violation of the command-query separation principle. Although, frankly, it has nothing to do with it. And he shows code that is quite typical for Go and says: "Look at this nightmare." At the same time, he shows too deep nesting, again, where it could have been written differently. It's clear why he does this, it's clear why all this is happening. Rather, it would be correct to write that, guys, we are working in Java. Java is a language with exceptions, they have certain meanings, and we must work with this in a certain way. This does not mean that we throw an exception in every situation, but we work with exceptions. It turns out that working with error codes is just swimming against the current if we are working in Java. But simply, no one else will interact with this system, and it will not integrate normally with anyone. Therefore, in this case, it's a Java specificity, because a person who, for example, from Go or Haskell, where there are also no exceptions, it works differently, and reads this, he will generally not understand what the point is. That is, it seems like clean code, but some kind of super-large specificity. Therefore, yes, there are languages in which it is set up like this. It's absolutely normal. Ah, you can, of course, implement it more beautifully than in Go, but in any case, most people are happy. That is, notice how he presents it as an absolute evil. Lots of people switched to such a system and say: "Thank God, we don't write exceptions anymore. We don't have all this trash." So, the world changes very quickly and significantly, as you can see. Let's move on. Isolate try-catch blocks. Try-catch blocks look quite ugly. He likes to throw things in so that then everyone, possibly, by the way, the entire success of his books lies in the fact that he throws things at the fan and makes everyone argue and constantly discuss him. So I assume that this is the main criterion for the success of his books. They confuse the code structure, mix error handling with normal processing, ah, blah blah blah blah blah blah blah. Ah, well, it's very simple here. Ideally, an exception is some kind of handling at the very top level, and inside the code, it's normal checking that can be done, what cannot be done. This is not always possible, there are various race conditions and so on. But, um, probably what he is primarily against here is, of course, against rigidly tying logic to this behavior, when we essentially start using exceptions like goto, to catch an exception practically in the same place where we throw it. This is indeed a very bad pattern. It's immediately clear that exceptions in this case will be used as logic. But in general, again, let's not dwell on this too much, because with exceptions, it's a separate story, how they work, how to work with them correctly. And moreover, they don't exist in all languages, especially now, when there are many without them. Again, Java specificity. Let's move on. Yes, we partially touched on structured programming. He started talking about it here, probably shouldn't have talked about it earlier, ah, about exiting a function, but in principle, it's about this, that structured programming is an approach where we have exactly one exit. And this was related, of course, to the extensive use of Go. Frankly, all this is quite outdated, so you shouldn't look in this direction too much, because now our programs are linear, unless, of course, we use try-catch to change behavior. This concludes the first part. Today we covered functions, then I will cover the remaining chapters until we go through the entire book. If you liked this video, give it a like, and if not, a dislike. Be sure to write comments if you disagree with me, and really blast me. If you agree, write something too. Most importantly, I really value any of your, uh, feedback to understand how best to say things, what not to say. And maybe I'm wrong somewhere, maybe you've discovered something new for yourself. And in the end, we will go through this book completely, finish it, and then, ah, we will start analyzing not how not to do things and where the problems are, but what cool, what cool things exist. Thank you all. Bye. [music]