📜 ⬆️ ⬇️

Cohesion in Enterprise Applications

Introduction


Code structure, project structure, project design, project architecture — these concepts can have different meanings, complexity, or depth for an architect, developer, project manager, or consultant. Then there should be a long digging in terminology, but let me be lazy and assume that in this article all these concepts express approximately the same thing, namely a set of patterns, rules that say how to write code, reacting correctly to incoming requirements. For example, if we use DAO (Data Access Object) to access the database, then, together with creating a new structure in the database, we will need to create a new DAO or expand the existing one, but not write SQL at all, say, at the presentation level.

To make it even clearer, I will add that it will be about the same thing that the “classic” wrote about - Patterns of enterprise application architecture by M. Fowler. Speech in the book goes in general about how to divide the functional, i.e. which method should belong to which class.

Problem


Let's start with an example. Our task will be to write a system for processing requests for a visa to the country of Oc. The system should allow you to create a request consisting of contact data and data on the place of work; send a processing request to the visa application center; make a decision on the issuance or denial of a visa; generate a report on the person who submits the documents; deliver a passport to flying monkeys. In addition, the request may be issued to several people.
')
In simplified form, the structure of the request may be as follows:

class VisaRequest{ Collection<Applicant> applicants } class Applicant{ Contact contact WokrInfo workInfo } … 

Do not forget that the programming language is magical (that is, it cannot be compiled without magic), although it is similar to groovy.

The functionality itself in this case may look something like this:
 class VisaRequestService { def create(def visaRequest){...} def update(def visaRequest){...} def get(def requestId){} def submit(def requestId){...} def getDecision(def requestId, def applicantName){...} def generateReport(def requestId, def applicantName){...} } 


A class diagram of such a system, reflecting the level of services and data model, is shown below.



What does Martin Fowler write about this? I will not retell the book, I will only say that, along with everything else, he drew attention to the fact that with the addition of functionality, the class of type VisaRequestService will grow and re-use written methods will be more and more difficult, since each method is actually a script specific to a specific scenario. We will not argue with him, but pay attention to the concept from the title of the article - cohesion. Cohesion is a property of an object / class that determines how busy an object / class is. If the cohesion is low, then the class has too much responsibility, the class does too many different operations, which makes it large, and the large class is hard to read, hard to expand, etc.

In our trivial case, we actually created God Class, which manages everything. Of course, this is not the only solution and we could create a separate DecisionService:

 class DecisionService{ def getDecision(def requestId, def applicantName){...} } 

or a separate SubmitService:
 class SubmitService{ def submit(def requestId){...} } 

or a separate ApplicantService:
 class ApplicantService{ def getDecision(def requestId, def applicantName){...} def generateReport(def requestId, def applicantName){...} } 


or something else (after all, Os country).
In all these examples, the cohesion of new classes seems pretty good, but not all of them improve the cohesion of existing ones. In the first two cases, if we limit ourselves to creating only one of these classes, the VisaRequestService still continues to do too much. In addition, the reasons why we have broken the class in this way remain unclear.

Decision


Classic


Fowler solves this problem by shifting it entirely on the shoulders of the PLO. He introduces the concept of Rich Data Model, which is nothing but an honest combination of data and methods in the same class, which is one of the foundations of OOP. Rich Data Model for our system will look like this:

 class VisaRequest{ Collection<Applicant> applicants def create(){...} def update(){...} def get(){} def submit(){...} } class Applicant{ Contact contact WokrInfo workInfo def getDecision(){...} def generateReport(){...} def create(){...} def update(){...} def get(){} } 


The division of functionality occurs according to a very simple rule: a class contains only methods related to the entity that this class reflects. Of course, this definition is not strict, as, indeed, the definition of cohesion itself.

Rich Data Model is convenient for a number of systems, for example in the case of complex calculations, but a small number of operations on the entity and weak interaction with the external environment, but not always. There are several reasons for this, but perhaps the most important ones are two.

The first of them is such a statefull model, i.e. the object has a certain state. As long as the state had only data structures, this was not a problem, but now the objects that carry the functionality of the system also have a state. In practice, this leads to the complication of the process of creating such objects. In fact, this makes it very problematic to use popular DI (Dependency Injection) containers, such as spring. In addition, if we talk about J2EE or WEB, nobody canceled the need to create facades (actions in spring), which leads to a new layer of functionality, which is not clear how to structure.

The second reason is that in large systems we can actually have many operations applicable to one entity, which can generate a large class and we will have to come up with a new partitioning technique that complicates the overall design.

Not classic


Let's try to solve the problem in a slightly different way, using all the same services. Why them? At least because services are very popular among programmers. The question arises, how to properly use them, bypassing the acute angles and obstacles described above. In my opinion, the most convenient way is splitting into two coordinates - the structure (element) of data and functionality, which should be reflected in the name of the service.



Following this rule, we will get the following services for our system:
 class VisaRequestCRUDService { def create(def visaRequest){...} def update(def visaRequest){...} def get(def requestId){...} def getDecision(def requestId, def applicantName){...} } class VisaRequestActionService { def submit(def requestId){...} } class ApplicantCRUDService{ def create(def Applicant){...} def update(def Applicant){...} def get(def applicantId){...} def create(def Applicant){...} } class ApplicantInfoService{ def getDecision(def requestId, def applicantName){...} def generateReport(def requestId, def applicantName){...} } 


Something like this would look like a class diagram:



The advantages of this approach are quite obvious. Firstly, we have almost all the advantages, as when using Rich Data Model, only instead of one class we will have two, which reduces connectivity, which is good. Secondly, we clearly indicate the mechanism (rule) of further improving cohesion, which minimizes system contamination.
There is another, not quite obvious plus. The data model does not always coincide with the domain model, and the operations always belong to the domain model. The above solution allows to reflect the differences between these two models on the coordinate of the functional. For example, let's remember about flying monkeys. Based on the requirements, they have no characteristics, i.e. we do not need to store any information about them and, accordingly, they have nothing to do in the data model. On the other hand, they are in the domain model, which we can reflect in the service, depending on the VisaRequest class:

 class FlyApesRequestDeliveryService{ def deliver(def Request){...} } 


Of course, the described approach has its drawbacks. The same flexibility of partitioning along the coordinate of the functional gives rise to the non-obviousness of this partitioning. The developer must have a clear idea of ​​the type of system and the set of operations performed (at least at the level of business requirements) in order to use this design correctly.

Instead of conclusion


I think cohesion is more or less clear, but what about an enterprise? Corporate applications are interesting because, as a rule, several commands work on them, possibly broken geographically. Many teams - many people, many people - many questions. Where and how to implement new functionality? Is there a similar system? New developer - new approach.

All this creates a lot of problems, however, the most difficult from a technical point of view is a dirty code (and the bad structure of classes is one of the clearest examples of dirty code). A system with dirty code is often easier to rewrite than fix. I am sure that not everyone will agree with me, but I would say that cohesion is thus one of the main indicators of the health of the system.

Perhaps this should be finished. I hope the article will be useful to someone, but if the problems described above are not familiar to you, you know what an evolutionary design is and it works in your team - forget everything that is written here, you are already in the country of Os.

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


All Articles