Do you often think about why something is done anyway? Why do you have microservices or a monolith, two-star or three-star? Why do you need multi-layered architecture and how many layers do you have? What is business logic, application logic, presentation logic and why is everything so divided? Look at your application - how is it designed at all? What is in it and where is it, why is it done that way?
Because it is written in books or so authoritative people say? What are your problems solves this or that approach / pattern?
Even the fact that at first glance seems obvious, it is sometimes very difficult to explain. And sometimes, in an attempt to explain, comes the understanding that the obvious thoughts were completely wrong.
Let's try to take an example and study these questions on it from all sides.
Toy CityVirtual citySOLID how much is in this word ...Subject areaPresentation logicState savingLayering2-tierN-tier2 as 3ServicesInstrumentsTheory and practiceTotalToy City
Let's imagine a small toy town. It consists of a number of buildings, several roads pass through it. Cars are moving along the roads and people are walking. Traffic control traffic lights. Everything that happens in the city is subject to certain rules and all this diversity can be controlled.
People and cars can be moved, traffic lights switch, change the time of day or night, etc ... Several people can interact with this city at the same time. They can either just watch or do something, forcing the city to change. All this perfectly exists, but there comes a time when it becomes necessary to transfer the toy city to the virtual world.
')
Those people who interacted with the city directly now sat comfortably in their chairs and stared at the dark monitors, squeezing the mouse in one hand and putting the other on the keyboard, waiting for the moment when everything comes to life and the virtual city will shine with its colors before their eyes . But for this to happen, there is a long way to go.
Virtual city
First you need to do the most important thing - to create a model of our virtual city. Although it may seem something simple, in fact, in this lies the majority of the problems and difficulties. But you still need to start, so let's get started.
Our goal is to describe the model of the city in a virtual form. To do this, we take any popular high-level
object-oriented language. The use of such a language involves the use of objects as basic building blocks for creating a virtual model of the city.
Of course, you can simply describe the entire model in
one object , but this is fraught with unnecessary complexity and confusion. When everything is “dumped” in one place and it is not clear how it is mixed up, it becomes difficult to understand what is happening and, all the more, to make any changes. Therefore, in order to make it easier and not to get lost in the resulting program, we will break the description of our city into small separate parts.
As such parts, we take what is easily separated from each other when looking at our real city - separate objects of this city (culture house, red BMW at the crossroads, Petrovich, running about their business). The description of each object in the virtual world is a description of its properties (color, model, name, location, etc.). In order not to repeat and not describe the same properties for similar objects each time, we will select a group of such properties and call them the
type of object . Good candidates are such common types as car, house, man, etc. They will allow to concentrate in itself the description of the main properties. And various types of machines, for example, will complement the basic type of “machine” with their unique set of properties, creating a whole set of new types. Such new types with respect to the original types are called
heirs . And the process of creating a new type, on the basis of the existing one, is
inheritance .
All types of objects created by us will represent the
model of our city.
After that, we simply create instances of these types for each object existing in the city and fill them with unique values.
And everything seems to be in its place, a group of cars is standing at the intersection waiting for the green signal of the traffic light, the girl Yulia is waiting for her elevator, and even the water is frozen in the pipes of a huge skyscraper. We filled our model with a
state , repeating the state of our real city at a certain point in time.
But, if we carefully look at our real city, we will see that it is constantly changing. All changes in its state are changes in the value of properties of various objects, the emergence of new objects or the disappearance of old ones. Here the traffic light switched and changed the value of the property “Current traffic signal” from
red to
green . Here, the elevator changed the value of the “Floor” property from the
second to the
first and the value of the “Doors open” property from
yes to
no .
So in order for our city to come alive, our program must be able to change the state of the model, which means - be able to change the properties of various objects, add new objects or delete old ones, i.e. have a
behavior .
To do this, add to the program
all possible actions in our city. Each such action can be described as a procedure for changing the properties of an object or group of objects. After describing all these procedures, it becomes clear that their number is quite large. To simplify support and change all available procedures, they should be divided into groups. Without thinking twice, you can group such procedures according to the uniformity of their actions, thus obtaining a set of classes, each of which will be responsible for a set of similar actions.
It seems that now everything is divided and looks very good, but there is one “but”. The description of the properties of our objects is completely separated from the procedures that these properties change and turn our model into an
anemic model . With this separation, it is completely incomprehensible how the properties of a particular object can change. And if a change in one property should be associated with a change in other properties or depends on the value of other properties, then this knowledge about the internal structure of the object will have to be duplicated in all procedures that change this property. To avoid these problems, we will not group procedures by actions, but decompose these procedures into those types whose properties they change.
Due to this, for public access it will be possible to leave only the properties and methods used by others, hiding knowledge about the internal structure of the object. This process of hiding the internal principles of work is called
encapsulation . For example, we want to move the elevator to several floors. To do this, check the condition of the doors - open or closed, start and stop the engine, etc. All this logic will simply be hidden behind the action "move to the floor." The result is that the type of the object is a set of properties and procedures that change these properties.
Some procedures may have the same meaning, but be associated with different objects. For example, the procedure "to emit a beep" is in both the red BMW and the blue Zhiguli. And although inside they can be performed in completely different ways, they carry the same meaning.
Since we already have a general type of “machine”, we can put the procedure “beep” there. If the logic of such behavior is the same for everyone, then it can also be defined there. This simplifies derived types and eliminates code duplication. But if suddenly, in derived types, the logic of this behavior will be different, then it can be easily changed by redefining the behavior. This feature is called
polymorphism .
Abstraction, inheritance, encapsulation and polymorphism are those OOP charm, following which you can create a more flexible design. In addition to them, there is a certain set of principles, the purpose of which is the same - to help create a more flexible design.
SOLID how much is in this word ...
SOLID is an abbreviation of the well-known principles of object-oriented design. In this abbreviation there are five hidden, one for each letter.
Single Responsibility Principle (principle of sole responsibility) - each object should have only one reason for change.
In order to use both the Internet and electricity in every room of every house, a single outlet with connectors for internet wire and electrical wire was created.
public class Socket { private PowerWire _powerWire; private EthernetWire _ethernetWire; public Socket(PowerWire powerWire, EthernetWire ethernetWire) { _powerWire = powerWire; _ethernetWire = ethernetWire; } ... }
For the operation of such an outlet, it was necessary to connect an Internet wire and an electrical wire. And, it would seem, there is nothing terrible in the fact that not all sockets always use the Internet and electricity, but are compact and convenient. But things are not so convenient when it comes time to change.
First there were apartments and even entire buildings in which the Internet was not needed. Totally. But in order for our sockets to work in them, we had to pull the Internet wires there, which only increased the cost of work, without bringing any benefit.
Then there was a requirement that the sockets should be equipped with an additional wire for grounding, and because of this, we had to change ALL sockets, including those that were used only for the Internet. A large amount of work has been done, which could have been less if the sockets used only for the Internet were not affected.
public class Socket { private PowerWire _powerWire; private EthernetWire _ethernetWire; private GroundWire _groundWire; public Socket(PowerWire powerWire, EthernetWire ethernetWire, GroundWire groundWire) { _powerWire = powerWire; _ethernetWire = ethernetWire; _groundWire = groundWire; } ... }
But the last straw was the requirement to change the Internet wire to a new standard for all Internet outlets. And since in general all the sockets are also Internet sockets at the same time, I had to change EVERYTHING again. Although the amount of work could be much less, since the number of sockets used for the Internet is several times smaller than the number of all sockets.
public class Socket { private PowerWire _powerWire; private SuperEnthernetWire _superEnthernetWire; private GroundWire _groundWire; public Socket(PowerWire powerWire, SuperEthernetWire superEthernetWire, GroundWire groundWare) { _powerWire = powerWire; _superEnthernetWire = superEnthernetWire; _groundWare = groundWare; } ... }
In all cases, a completely extra amount of work was done due to the fact that several, completely unrelated duties were combined in one object. And each of these responsibilities had its own, separate reason for the change.
To avoid such problems - the outlet had to be divided into two, independent from each other, parts - an electrical outlet and an Internet outlet:
public PowerSocket { private PowerWire _powerWire; private GroundWare _groundWare; public PowerSocket(PowerWire powerWire, GroundWare groundWare) { _powerWire = powerWire; _groundWare = groundWare; } ... } public class EthernetSocket { private SuperEthernetWire _superEthernetWire; public EthernetSocket (SuperEthernetWire _superEthernetWire) { _superEthernetWire = superEthernetWire; } ... }
And in those cases when both an electrical outlet and an internet outlet could be used,
aggregation could be used:
public PowerEthernetSocket { private PowerSocket _powerSocket; private EthernetSocket _ethernetSocket; public PowerEthernetSocket (PowerSocket powerSocket, EthernetSocket ethernetSocket) { _powerSocket = powerWire; _ethernetSocket = ethernetSocket; } ... }
Open-closed principle - the objects should be closed for modifications, but open for extensions.
To alert people about important information in the city center, a big screen was installed on the busiest crossroads. It displays the text of messages coming from various sources.
public class Message { public string Text { get; set; } } public class BigDisplay { public void DisplayMessage(Message message) { PrintText(message.Text); } public void PrintText(string text) { ... } }
After some time, a new kind of message appeared - messages containing the date. And for such messages on the screen it was necessary to display both the date and the text. Refinement could be done in various ways.
1. Create a new type derived from the “message” type, add the “date” attribute to it and change the procedure for displaying messages.
public class Message { public string Text { get; set; } } public class MessageWithDate : Message { public DateTime Date { get; set; } } ... public void DisplayMessage(Message message) { if (message is MessageWithDate) PrintText(message.Date + Message.Text) else PrintText(message.Text); }
But this method is bad in that it is necessary to change the behavior of all types, which in some way display a message. And if in the future there will be another new, special type of messages - everything will have to be changed again.
2. Add the “date” property to the “message” type and change the way the text is received to make it like this:
public class Message { private string _text; public string Text { get { if(Date.HasValue) return Date.Value + _text; else return _text; } set { _text = value; } } public DateTime? Date { get; set; } }
But, first, this method is bad in that we have to change the basic type and add behavior to it, which is not characteristic of all messages, which causes unnecessary checks. Secondly, when a new type of message appears, we will have to create another attribute, which not all messages will have, and add extra checks to the code. Thirdly, there is now no way to get the message text without a date for messages with dates. And in case of such need - the message text will have to pick out.
3. Immediately record the date in the message text in order not to create any new type at all and not change the way the message is displayed on the screen. But this method is bad in that in those places where you only need to get the date from the message - you first have to determine whether the message contains a date at all, and then isolate it from the message text.
If you follow the principle of openness-closeness, you can avoid all these problems and go the fourth way:
public class Message { public string Text { get; set; } public virtual string GetDisplayText() { return Text; } } public class MessageWithDate : Message { public DateTime Date { get; set; } public override string GetDisplayText() { return Date + Text; } } ... public void DisplayMessage(Message message) { PrintText(message.GetDisplayText()); }
With this approach, we do not need to change the basic type of "message" - it will remain
closed . At the same time, it will be
open to expanding its capabilities. In addition, all problems peculiar to other approaches will disappear.
Liskov substitution principle (Barbara
Liskov substitution principle ) - functions that use the base type should be able to use subtypes of the base type without knowing about it.
After adding the type of "bike" to the program, it is time to add another type - "moped". A moped is like a bicycle, only better. So the bike is perfect as a base type for a moped. Said and done, and the program has another type of "moped" - derived from the type of "bicycle".
public class Bicycle { public int Location; public void Move (int distance) { ... } } public class MotorBicycle : Bicycle { public int Fuel; public override void Move(int distance) { if (fuel == 0) return; ... fuel = fuel – usedFuel; } }
But compared to a bicycle, a moped has one unpleasant feature - if gasoline ends, the moped can no longer move. And this unpleasant feature had to be considered in the code. We took into account, but did not give any values ​​- for that we also have a derived type, in order to take into account any specific features.
Since the moped is faster than the bike, when possible, it began to be used in the program where the bike was previously used. But, one fine day, the program hung tight. The search for the error was long and painful, because The problem was repeated periodically and randomly. Let us omit the description of sleepless nights and immediately move on to the culprit of all troubles - a method that moved the cyclist between two points far from each other:
public void LongJourney (int to, Biker biker, Bicycle bicycle) { while(bicycle.location < to) { int distance = to - bicycle.location; if (distance > 1000) distance = 1000; bicycle.move(distance); biker.sleep(); } }
When a moped with an insufficient amount of gasoline was transferred to the method instead of a bicycle, it hung forever due to the fact that the moped did not move even a centimeter, even if such an action was clearly caused. To fix this problem - one could do so, of course:
public void LongJourney (int to, Biker biker, Bicycle bicycle) { while(bicycle.location < to) { int distance = to - bicycle.location; if (distance > 1000) distance = 1000; if (bicycle is MotorBicycle && bicycle.Fuel == 0) FillFuel((MotorBicycle)bicycle); bicycle.move(distance); biker.sleep(); } } public void FillFuel(MotorBicycle motorBicycle) { ... }
But then the introduction of such changes would require a change in a large number of procedures, which, firstly, for a long time, and secondly, is fraught with the fact that it can be forgotten and this forgetfulness will lead to yet another elusive problems. In addition, the addition of such conditions would be an abstraction leak. And in the case of the appearance of any other factor affecting the behavior, all these difficulties would only double.
In fact, the initial problem lies in the fact that the moped is not a type of bicycle, despite all its external similarity. Therefore, attempts to bring them to a common denominator did not lead to anything good. For the moped, it was necessary to make a separate type independent of the bike, and to take this into account in all the necessary procedures.
Interface segregation principle - clients should not depend on methods that they do not use.
To make it more realistic, one interesting behavior was added. If in the wrong place, on the road, a man appeared, then the vehicle he had prevented had to stop and signal. For this method was implemented:
public void CheckIntersect(Car car, People[] people) { ... if (Intersect(car, people)) { car.Stop(); car.Beep(); } } public bool Intersect(Car car, People[] people) { ... }
The behavior was checked and everything was fine until a bicycle appeared in the system and hit a person who ran onto the highway. And this is not surprising, because the collision check and automatic stop were made only for cars.
The first crazy idea could be the desire to make a bicycle a derived type from the type “machine” so that it can be easily substituted into such methods without changing them. After all, if you take a closer look, the procedure uses only those actions of the machine that the bicycle has, and nothing terrible will happen. But it is only in this method. If such a strange derived type is transferred to some other procedure using something machine-specific, then such a procedure will break.
The second crazy idea would be to create a separate procedure for checking the collision of a bicycle with a person. But then it turns out that all the logic will be duplicated. Moreover, it is necessary to create a hotel procedure for each new vehicle. This is not flexible at all.
But, if in the collision checking procedure only two actions are used that any vehicle has, why transfer a particular type to the method?
We can define a contract that must comply with the type that can be used in this method and implement it in all vehicles.
public interface IVehicle { ... void Stop(); void Beep(); } public class Car : IVehicle { ... } public class Bycycle : IVehicle { ... } public void CheckIntersect(IVehicle vehicle, People[] peoples) { ... if (Intersect(vehicle, peoples)) { vehicle.Stop(); vehicle.Beep(); } } public bool Intersect(IVehicle vehicle, People[] people) { ... }
Then, in this method it will be possible to transfer any existing type of vehicle and any type that will appear in the future, subject to the conditions of the contract.
Dependency Inversion Principle - Abstractions should not depend on details. Details must depend on abstractions.
In our city there is a news alert system. The system receives important news and broadcasts it through the city’s speaker system.
public class NotifySystem { private SpeakerSystem _speakerSystem = new SpeakerSystem(); public void NotifyOnWarning(string message) { _speakerSystem.Play(message); } }
Everything works fine, but the requirements tend to change. Now we have to sometimes send messages not through the system of speakers, but through mobile phones, sending them in the form of SMS. Of course, we can make another object to send notification of messages via SMS, but for this you will have to duplicate most of the logic. To avoid this - we will go the other way. The alert system, in principle, doesn't care how its messages will be displayed. The most important thing is to send them. Therefore, we can do this:
public class NotifySystem { private IMessageDelivery _messageDelivery; public public NotifySystem(IMessageDelivery messageDelivery) { _messageDelivery = messageDelivery; } public void NotifyOnWarning(string message) { _messageDelivery.SendMessage(message); } }
We will simply declare the message delivery system interface that we will use inside the alert system. And the implementation of this interface will fall on the shoulders of those who want to use the alert system. In fact, we create a certain pattern of behavior, which we allow to expand in the way that is convenient for the consumer.
- Could you just not bother with all these principles and write the code as it will?
- Of course!
- And still it would still work?
- Naturally!
- Why then do you need to use all these difficulties in the form of OOP, Ltd.? If I do not think about all these difficulties, I will write the program much faster! And the faster I write, the better!
- If the program is simple enough, and its further development, improvement or correction of errors is not expected, then all this, in principle, is not necessary. But! If the program is complex, it is planned to be improved and supported, then the application of all these “extra difficulties” will directly affect the most important thing - the amount of resources spent required for improvements and support, which means the cost .All these rules, patterns, paradigms are united by one main idea - to make the code modular, flexible, easily extensible and resistant to change. Changing the behavior of such small, loosely coupled modules, as a rule, is cheaper than changing the behavior of a large module, in which everything is mixed together.
Subject area
- Remember, if a bad algorithm costs you a thousand, then a bad architecture will cost you a million.However, all that has been described above is basically a solution to technical problems. The fact that such a PLO, OOD, what advantages they have and how to use them is already written decades ago. Enough to drive a couple of keywords, for example, SOLID and get a bunch of explanatory information.
If the problem is quite general and requires some algorithm for its solution, then, most likely, it already exists too - you just need to reach out. A couple of queries in the search engine, for example, "
count cars, coupled in a circle, the solution " and hundreds of results in front of you.
But in order to properly divide logic into modules and services, technical knowledge alone is not enough, and the search engine will not help here. You need a good understanding of the subject area, and this is already a problem, since you have to put a lot of effort into learning something completely with programming that is not related. Therefore, developers are more like to solve technical problems, giving the decision of business problems at the mercy of analysts. But analysts do not know the internal structure of the program, which reflects not the structure of the business, but the developer’s perception of this structure. And the further this idea is from reality, the more difficult this structure will be to change to match the changes that have occurred in the business.
This is also due to the double interpretation of the task. First, the analyst tries to understand what needs to be added or changed in the application, communicating with business users. Then he tries to convey this knowledge in his own words to the developer. As a result, the truth can be distorted and all these distortions can result in an unsuitable program structure.
The user expressed the wish that daylight should come into the rooms of the houses so that everything could be seen. The developer, without thinking twice, hollowed out a round hole in the wall and checked - it turned out that there was still not enough light. Then the developer gouged two more holes nearby and checked again. Making sure that there is enough light, the developer decided that the task was completed. According to this principle, holes were hollowed out in all houses. Requirements have been met.
But the summer was over and winter came, and with it the cold. The angry user ran up and began to swear that it was terribly cold in the rooms. When clarifying the reasons, it became clear that because of the holes hollowed out under the sunlight, a lot of cold air from the street gets into the room and it freezes through. After clarifying the circumstances, it turned out that the user wanted a regular window, but he forgot to mention some requirements, but the analyst conveyed something in his own way, and it turned out what worked. Since everything was frozen - the problem had to be solved on the move, there was no time to develop good windows and to redo the holes under the windows in winter. Therefore, it was decided to get by with the “crutch” and cover the holes with polyethylene. This method helped get rid of the strong freezing and allowed to leave the sunlight in the afternoon. But who knows how many new problems incomplete understanding of the initial requirements will bring ...In order to solve this problem between all the participants in the development, a common, identical idea of ​​what is happening and what is needed should be developed. For this, there is a whole approach -
Domain Driven Design , whose goal is to develop a common understanding and common terminology (common language) among all the participants in the development.
Poor understanding of the model leads to the fact that the developer is trying to do what is closer and clearer to him - the solution of technical problems. Including the solution of business problems in a purely technical way. And in the case of the absence of technical problems - their invention and their own heroic solution. All this will only lead to the appearance of strange, obscure structures in the program. On the other hand, this can be a conscious choice if the developer follows Ipoteka Driven Development.
But it was time to end the short journey on the principles of the OOD and other abstract topics and return to the program. After adding business logic to our model, we did not just have a snapshot of the frozen city, but its full-fledged model. A model whose state can be easily changed in the same way as the state of a real model.
Presentation logic
Despite all our successes, users continue to wait impatiently in front of the dimmed monitor. We created a model of our city, breathed life into it, describing the logic of its change, thus doing a very important amount of work. It now remains for us to enable users to “see” what we have done. . -, , , , , - . -, — , , ..
. , , , , . – , . . – . . - - . , .
, . , , , . , — , . , . —
, (, , ). , , , – .
, , «» , . «» - . , . , , . , , . . , .
– . , . , , . . .
, . , , — , . , .
,
– , , , . - - , . , . .
– , .
-, Microsoft HoloLens, , .
, , .
— – .
— – .
. . — , – . , …, . , , . , , . , , . .
: , - , , .
.
.
. , , , , . , , , .
. , . . , , - — , .
, , -, , . .
ORM . , ,
.
, , . redo- , .
event sourcing .
— . , , , , .. -, , ?
— , .
— , , – , . …., , , .
-
, . , . .
, . , -, .
, , . , .
, , . -, , . , .
. , . , , , ? , , ? . , . - , .
2-tier
— ! – . – !
— , , . – .
— … – . — , .
— , – . – , .., , .
— ? – . – , , .
., — . , , . , , .
. , –
. . – , . , .
, . , .
.
—
- () .
. -, , . , .
-, . , ,
. , ,
.
N-tier
— , , , - , . – .
— , . – .
…- , ,
.
. - , .
, -, «»
.
, - , - . , , , .. , ,
.
2 3
, , , . , - , . , .
— - , , . , – , -.
,
, . , . .
. , . , , - , .
Services
— - . – , – , – . – - , ? – .
— – – , ? – .
, . , , . , , , . , . — , . , «
». , , : , , ..
— . But, since , . , , . , .
, . (
). – .
, . , , , , — . , .
, «» , , , - .
, ,
. -
.
— . , , . , , .
, , , , , . , , - , , ,
.
— , ?
— , -, , .
— - …, . , , , , .
, , , . , — , , , . , , .
Instruments
- , , , , ?
— , ., , , – , . , . , . , - , — !
– . , , ( , ), -, , .
, – «
Not invented here ». , , - , . .
— - , . , - — , .
Theory and practice
, . — ., , , . , , – .
. . , , . , , , , . - , , , .
,
.
Total
, . , . /// .. – .
— . , .
.
— .