Information technologies are developing in leaps and bounds, new devices, platforms, operating systems are emerging, and with it the range of tasks that developers have to solve grows. But, not everything is so bad - new development tools, ide'shiki, new programming languages, methodologies, etc. are rushing to help programmers. The
list of programming paradigms alone is impressive, and taking into account modern multi-paradigm PL (for example, C #), the question is reasonable: “How to deal with all this? What to choose?".
Let's try to understand a little.
Where did so many paradigms come from?
In fact, the answer has already sounded in this post - different types of tasks are easier and faster to solve using the appropriate paradigms. Accordingly, with the development of IT, new types of tasks appeared (or old ones became relevant), and solving them using the old approaches was inconvenient, which entailed rethinking and the emergence of new techniques.
What to choose?
It all depends on what needs to be done. It is worth noting that all development tools are different, some support one, others support another. For example, PHP with a “standard” set of modules does not support
aspect-oriented programming . Therefore, the choice of methodology is quite closely related to the development platform. Well, and do not forget that you can combine different approaches, which leads us to the choice of a stack of paradigms.
')
To categorize paradigms, I use 4 dimensions that are inherent in almost any task:
- Data
Any program, anyway, works with data: stores them, processes, analyzes, transfers
- Actions
Any program should do something, usually actions are data related.
- Logic .
Logic or business logic defines the rules by which data and actions are subject. Without logic, the program is meaningless. - Interface .
How the program interacts with the outside world
You can go further and delve into this idea: come up with qualitative characteristics for these four measures, add strict rules and a bit of mathematics, but this is perhaps a topic for a separate post. I think most system architects define these characteristics for a specific task based on their knowledge and experience.
After you analyze your task on these 4 dimensions, you will most likely see that a certain dimension is more pronounced than the rest. And this, in turn, will make it possible to determine the programming paradigm, since they are usually aimed at a single dimension.
Consider examples:
- Data Driven Design ( Data Driven Design ). We are primarily concerned with data, and not with how they are interconnected.
Types of suitable applications:
- grabbers (collect data from various sources, save somewhere)
- various admins, database interfaces, all where there are a lot of simple CRUD operations
- cases when work with data is already defined, for example, a program is required to be developed, a database already exists and a data scheme is not changed. In this case, it is probably easier to focus on what is already there than to create additional wrappers over data and data access levels.
- often data orientation appears when using ORM, but it is impossible to say in advance whether this is good or bad (more on this below)
- Orientation to actions - imperative approaches to development. I think that such paradigms as Event Driven Programming , Aspect Oriented Programming can be included here.
- Logic Orientation - Domain Driven Design (DDD) and everything connected with it. Here the subject area of ​​the problem is important to us, we pay attention to the modeling of objects, the analysis of relationships and dependencies. Used primarily in business applications.
Also, this includes a declarative approach and partly functional programming (solving problems that are well described by mathematical formulas) - Interface orientation. It is used when it is primarily important how the program interacts with the outside world.
Developing an application with an interface-only focus is a rather rare situation. Although in some books I met the mention that this approach was considered seriously, and based on the user interface - they took what the user sees directly and, based on this, designed the data structures and everything else.
Orientation to the user interface in business applications often manifests itself indirectly: for example, the user needs to see certain data that is difficult to obtain, due to which the architecture acquires additional structures (for example, forced data redundancy).
Formally, this includes Event Driven Programming
And what in practice?
In our company, we are developing business applications (startups, web services, websites, application programs, etc.), so we’ll talk further about programming paradigms that are found both in our practice and in other teams working in this field.
Based on my experience, I can say that in this area two approaches prevail: data orientation (Data Driven) and logic orientation (Domain Driven). In fact, they are competing methodologies, but in practice they can be combined in symbiosis, which are often known anti-patterns.
One of the advantages of Data Driven compared to Domain Driven is its ease of use and implementation. Therefore, Data Drivens begin to be used wherever you need to apply Domain Driven (and often this happens unconsciously). The problems arise from the fact that Data Driven is poorly compatible with the concepts of object-oriented programming (of course, if you use OOP at all). On small applications, these problems are almost imperceptible. On medium-sized applications, these problems are already noticeable and are beginning to lead to anti-patterns, well, and on large projects, the problems become serious and require appropriate measures.
In turn, Domain Driven is advantageous on large projects, and on small projects it complicates the solution and requires more resources for development, which is often critical from the business point of view (bring the project to the asap market, for a small budget).
In order to understand the difference in approaches, we consider a more specific example. Suppose we need to develop an order accounting system. We have such entities as:
- Product
- Customer
- Quota (sent to the customer as an offer)
- Order (order)
- Invoice (payment)
- Warrant supplier
- Bill (in fact, the payment from the supplier)
Having decided that we have a clear context, we begin to design a database. We create the corresponding tables, run the ORM, generate the entity classes (well, or in the case of the smart orm, we prescribe the scheme somewhere separately, for example, in xml, and already generate the base and the essence classes from it). As a result, we obtain for each entity a separate, independent class. We rejoice in life, work with objects is easy and simple.
Time passes, and we need to add additional logic to the program - for example, to find the goods with the highest price from the order. There may already be problems if your orm does not support external relations (i.e., entity classes do not know anything about the data context), in this case, you will have to create a service in which there will be a method - to return the required product on an order. But, our orm is good, can work with external relations, and we simply add a method to the order class. We enjoy life again, the goal has been achieved, a method has been added to the class, we have almost the real PLO.
Time passes, and we need to add the same method for the quota, for the invoice and for other similar entities. What to do? We can simply register this method in all classes, but this will, in fact, duplicate code and backfire with support and testing. We do not want to complicate and simply copy the method to all classes. Then similar methods appear, the entity classes begin to swell with the same code.
Time passes, and logic appears, which cannot be described by external links in the database, and therefore it is not possible to place it in the essential classes. We are starting to create services that perform these functions. As a result, we find that the business logic is scattered throughout the essential classes and services; it is becoming increasingly difficult to understand where to look for the desired method. We decide to refactor and render, for example, repetitive code to services — select the general functionality of the interface (for example, we make the interface IProductable, that is, something that contains products), services can work with these interfaces, thereby winning a little in abstraction. But fundamentally this does not solve the problem, we get more methods in services and decide for the unity of the picture to transfer all the methods from the essential classes to the services. Now we know where to look for methods, but our essential classes are deprived of all logic, and we got the so-called “Anemic Model” (Anemic Model).
At this stage, we completely left the concept of OOP - objects store only data, all logic is in separate classes, neither encapsulation nor inheritance.
It is worth noting that this is not as bad as it may seem - nothing prevents to introduce unit testing, and indeed development through testing, implement dependency management patterns, etc., in general, you can live with it. Problems will arise when the application grows into a large one, when there are so many entities that it is impossible to keep them in mind, and there is no need to talk about interaction. In this case, the support and development of such an application will become a problem.
As you may have guessed, this scenario describes the use of the Data Driven approach and its problems.
In the case of Domain Driven, we would do the following. First, about any design of the database at the first stage of speech would not go. We would need to carefully analyze the contextual area of ​​the problem, model it and transfer it to the PLO language.
For example, we can create an abstract document model that has a set of basic properties. Inherit from it a document that has products, inherit a “payment” document from it, with a price and billing address, and so on. With this approach, add a method that gets the most expensive product, it is easy - we just add it to the appropriate base class.
As a result, the contextual area of ​​the task will be described using OOP to its fullest.
But there are obvious problems: how to save data in the database? Actually, for this, you will need to create functionality for mepping data from our models onto fields in the database. Such meppers can be quite complex, and as models change, meppers will have to be changed.
Moreover, you are not insured against modeling errors, which can lead to complex refactoring.
So, let's summarize the Data Driven vs Domain Driven:
Data Driven :
- pros
- Allows you to quickly develop an application or prototype
- It is convenient to design (code generation according to the scheme, etc.)
- On small or medium-sized projects, it may be quite a solution.
- Minuses
- May lead to anti-patterns and avoiding OOP
- On large projects leads to chaos, complex support, etc.
Domain Driven:- pros
- Uses the power of OOP
- Allows you to control the complexity of the context area (domain)
- There are a number of advantages not described in the article, for example, the creation of a domain language and the introduction of BDD.
- Gives a powerful tool for developing complex and large solutions.
- Minuses
- It requires significantly more resources in the development, which increases the cost of solutions
- Certain parts become more difficult to maintain (data mappers, etc.)
So, what the hell should I choose?
Unfortunately, there is no definite answer. Analyze your problem, your resources, development prospects, goals and objectives. The right choice is always a compromise.