⬆️ ⬇️

CORE Design Technique

I have been working as a programmer for more than 5 years (web), and I would like to share a technique that saves energy, time and helps automate the design process.



The technique is based on object-oriented design, but somewhat unusual. But it has obvious advantages:

- ideally, CORE programming is reduced to the description of the task (the code is close to the business logic)

- clearly divides the system into loosely coupled components

- easily automated, allows you to generate meaningful code



Why is the technique called CORE and how does it stand for? Partly because I have a craving for beautiful names. Spell:

Context - the context of the calculation (which initiated the calculation)

Object - an object that performs calculations

Request - the action to be performed so that the object can continue the calculations.

Event - an event that occurs with an object.

')

Pluses in comparison with standard ways of development:

- acceleration of the design stage due to the formalized design scheme

- acceleration of the development stage due to clever code generation

- automation of the creation of unit tests

- non-buggy implementation of business logic of almost any complexity

- simple code support

- ease of sharing code



Disadvantages of standard methods compared to CORE:

- it is often impossible to take one look at the whole part of the system

- you need to think over yourself when and in what place handlers of certain actions will be called. CORE resolves calls automatically.

- often additional levels of abstraction are introduced that are not related to business logic in order to implement its features. in CORE it is not necessary.

- the programmer often performs a bunch of monotonous actions that can be easily automated

- unit testing is harder to implement



A bit of theory.



Code connectivity, modularity and execution flow



In programming, everything is trivial and simple: if we are at the point of execution, then someone has called us.



 o WebDispatcher
  \
    o ArticleController
     \
      o ArticleMapper
       \
         o DB
        /
      o
     /
    o
     \
      o WidgetManager
       \
        o WidgetForNovice
       /
      o
       \
        o WidgetForExpirienced
       /
      o
     /
    o
     \
      o Stat
     /
    o
   /
 o
  \
   o ViewRenderer
  /
 o


This is how code coherence is organized. However, when we want to build a modular system, we come into conflict: suddenly it becomes necessary for low-level classes to call methods of high-level classes, places are created where a bunch of logically unrelated code accumulates ... And so, modularity is lost: the code becomes scattered different places of the project. If we want to add widgets to the pages without touching the controller (for example, as an experiment for 10% of users), nothing will work out for us - we will definitely have to register ourselves somewhere.



But what if we want to leave the modular structure, and we want the code relating to one task to be in the same place? But, after all, the module, for example, statistics, needs someone to call it?



Let's try to classify the possible types of calls:



- I am the caller who logically “knows” the called class and calls it because it is part of its action logic (for example, the mapper knows how to call the database and how to work with it - in fact, he knows everything about the database)

- the caller logically “does not know” the called class, but the called class necessarily needs to be called (example: the controller does not need statistics for successful work, but the statistics need to be called from the controller, because otherwise it will not work)

- the caller does not know who to call, but she needs something to be done (example: WidgetManager does not know which WidgetForNovice and WidgetForExpirienced will be shown, but he needs to see the appropriate widget for the conditions)



How to look at it from the practical side

- the DB class doesn’t care who uses it - the mapper or someone else - it’s not tied to logic

- Mapper, on the contrary, is very dependent on the database, it is tied to it, and if instead of MySQL it is slipped to Redis, it will break

- by and large, the ArticleController class doesn’t care that it needs to pull statistics along the way - it doesn’t affect the data display assigned to it by its duties

- the Stat class, on the contrary, wants to know from which sources it collects data - it must know everything about its sources in order to work correctly

- the WidgetManager class does not matter how many widgets are registered in the system, but it is important that one of them is displayed

- it is important for widget classes to check that they can be displayed and displayed the same - they were created to be auxiliary to WidgetManager



According to the type of dependency, components (modules) can also be divided into three types :

- components dependent directly (mapper depends on the database, he uses it for his work)

- components that require incoming calls (statistics depends on whether its controller is twitching)

- components that require the resolution of outgoing calls (WidgetManager will not be able to work if there is no one to delegate)



To resolve dependencies while maintaining modularity, there is nothing left except the use of proxy objects that will create connectivity. And external challenges (albeit more abstract) will still have to be made. Thus, modules can provide a call resource.



By the type of resources provided, the components can be divided into two types :

- components that make external calls (ArticleController must invoke a bunch of classes)

- components that do not make external calls (statistics, for example, simply have nothing to call)



In the CORE system, I propose to provide Events Events (by subscribing to which, you can get into the execution flow) and Requests (for cases when the caller is waiting for the action to take place, but does not know who will perform it).



Why Events and Requests? These abstractions are well combined with business logic - almost any business logic can be decomposed into events (“when something happened, do something else”) and queries (“the module needs this to happen” without specifying what and when will implement it).



If we describe business logic in terms of Events and Requests, we automatically get the implementation of the logic we need. And it is easy to control what has already been implemented and what has not.



Principles of well-structured software code



It is good if we follow the following principles when programming:



- weak connectivity

Often when working with conventional techniques, there are garbage classes that are aware of dozens of other classes in the system. Over time, it is very and very difficult to understand such a code, and the most guilty / most responsible team members are forced to refactor.



- avoid reverse dependencies

This happens when objects are low level, call objects high level. For example, some WebDispatcher in which calls to Page or classes of a business logic level are directly registered. Because once it was so convenient, but they forgot about refactoring. Well, do not care, it works.



- possibility of code reuse

Correct dependencies are very important - try moving the WebDispatcher class with high-level calls from one project to another. I feel it will not be easy ...

The idea of ​​reuse is often misunderstood, breaking the class into very small subclasses with one or two methods inside, and then creating a dozen or two small objects and feeding it to the target class. In fact, this is a real torture - about increasing the number of levels of abstraction, I will say a word.



- implementation is closer to business logic

The problem of “flowing abstractions” forces us to redesign everything anew when additional conditions are added to the task. And more often just insert crutches into the most unexpected places of the program. Quickly, “so that it works.” If we initially design in terms of the subject area, we are spared these problems, and the code with any change remains clean and transparent.



- too many levels of abstraction are not good

When we have dozens or even hundreds of small classes in the system, about which it is intuitively incomprehensible what they do (and some also look alike), the matter is much more serious than preparation for the higher mathematics exam.

It is necessary to remember that they program with the head, and when the head is overloaded with a bunch of trifles, the programmer's time is wasted. It would be better if the programmer used the time to write the tests.



- well break the code into components

Component easier to understand. Component easier to test. With a component structure, it is easier to break a project into pieces and organize work in small groups. I will not comment further, here and so everything is clear.



- the code must be ready for unit testing

Overlaps with the previous item, with one difference. Inside the components (and it can be complicated) we should be able to implement unit tests.



Also, add a couple of myself:



- automation is cool - the programmer should work with the head, not hands

It hurts to realize that we often do by hand something that we could easily automate. It swings everything - designing, programming, thinking through the structure of classes, searching for bottlenecks. All this can be charged to the computer.



- code generation is cool - eliminates stupid mistakes and helps to formalize the process

Many stupid mistakes are made when you incorrectly call a variable or method. The error can be in one letter, and it is very annoying. Code generation eliminates a fairly large percentage of such errors.

And if we consider that we can also include the generation of tests in the code generation ... mmm ... such programming seems to me much more honest than hand-made articles on deadline.

It should be noted that I’m not talking about the generation of an empty class with empty methods, but the structure of classes with code blanks and test blanks. This is done on the basis of a formal description of the solution of the problem before programming (in this case, as an xml file).

About process formalization - the process can be divided into two stages - 1. design, the result of which is a formal description of the algorithm (the same xml-file), 2. generation of semi-finished products of all classes, which are given to the final implementation to developers. Saving man-hours, I think, no need to explain. The advantage is the same xml-file, which contains the structure of classes and a description of the solution to the problem in short form.



To practice



So, let's try to solve an abstract practical problem from the life of any developing web project.



We take the task of a not too trivial, breaking beautiful code structure in the classical organization of the MVC model: “to conduct an experiment: for 50% of users from Moscow aged 27 to 35 years on the 5th open page in the session show a pop-up calling for buying in-game currency, and collect statistics on the change in the average session length (time on the site / views) and an increase in total sales per unique visitor. ”



We formalize the problem:



Show popup:

- we allocate users from 27 to 35 years, we will call the TargetGroupMsk27to35 group

- we divide users into groups A and B (test and control groups)

- when the user entered the game, for group A we count the 5th page, we call this event “the 5th viewing in group A” (GroupMsk27to35TargetView)

- when the GroupAView event has come, we show the necessary popup



Session statistics:

- when the session started at TargetGroupMsk27to35, note the time it began

- when the session ended at TargetGroupMsk27to35 measure the time of its end and put in the statistics

- when a page view of a user in the TargetGroupMsk27to35 group occurs, we increment the view counter

- when the session is over from TargetGroupMsk27to35, we take the value from the view counter and put it in the statistics



Monetization statistics:

- when a user purchases from TargetGroupMsk27to35, we put the value in our selected statistics



Separately, we note that the phrase “we show the necessary popup” is rather abstract, therefore we formalize:

- when you want to show PopupMsk27to35, take it from the files PopupMsk27to35.tpl, PopupMsk27to35.css and PopupMsk27to35.js



As you can see, our business problem is easily decomposed into CORE terms:

Contexts: web request, session end script

Objects: experiment ExperimentMsk27to35, popup PopskMsk27to35, statistics StatMsk27to35

Events: PageView, UserStartSession, UserEndSession, UserBuyMoney, GroupMsk27to35TargetView



According to the formal description, we generate the code:



//   ,   php class ExperimentMsk27to35 { function isOn() { return Config::get('ExperimentMsk27to35_enabled'); //    } function inTargetGroup(User $User) { return $User->getAge() >= 27 && $User->getAge() <= 35; } function inGroupA(User $User) { //  ,    ,  md5,    //  50%     id return self::inTargetGroup($User) && $User->getId()%2 == 0; } function inGroupB(User $User) { return self::inTargetGroup($User) && $User->getId()%2 == 1; } function onPageView(User $User, Page $Page, Session $Session) { if (self::inGroupA($User)) { //    memcached $count = Memcached::getInstance()->increment('Msk25to37GroupAPageViews_'.$Session->getId()); if($count == 5) new Event('GroupMsk27to35TargetView', $User, $Page); } } } class PopupMsk27to35 { function onGroupMsk27to35TargetView() { if(ExperimentMsk27to35::isOn()) { new Request('ShowPopupMsk27to35', $Page); } } } class PopupMsk27to35View extends ViewElement { protected $render = false; function requestShowPopupMsk27to35() { $this->render = true; } function onPageRender(Page $Page) { if($this->render) { $this->renderTo($Page, 'PopupMsk27to35.tpl', 'PopupMsk27to35.css', 'PopupMsk27to35.js'); } } } class StatMsk25to35 extends Stat { function onSessionStart(User $User, Session $Session) { if(ExperimentMsk27to35::inTargetGroup($User)) { Memcached::getInstance()->set('Msk25to37sessionStartTime_'.$Session->getId(), time()); } } function onPageView(User $User, Page $Page, Session $Session) { if (ExperimentMsk27to35::inTargetGroup($User)) { //    memcached Memcached::getInstance()->increment('Msk25to37PageViews'.$Session->getId()); } } function getSuffix(User $User) { if(ExperimentMsk27to35::inGroupA($User)) { return "a"; } if(ExperimentMsk27to35::inGroupB($User)) { return "b"; } return $stat_suffix; } function onSessionEnd(User $User, Session $Session) { if(ExperimentMsk27to35::inTargetGroup($User)) { $time0 = Memcached::getInstance()->get('Msk25to37sessionStartTime_'.$Session->getId()); $sessoin_time = time() - $time0; $page_views = Memcached::getInstance()->get('Msk25to37PageViews'.$Session->getId()); $stat_suffix = $this->getStatSuffix($User); $this->writeUserStat($User, 'session_time_'. $stat_suffix, $session_time); $this->writeUserStat($User, 'page_views_'. $stat_suffix, page_views); } } function onIncomingMoney($User, $MoneyOperation) { if(ExperimentMsk27to35::inTargetGroup($User)) { $stat_suffix = $this->getStatSuffix($User); $this->writeUserStat($User, 'money_'. $stat_suffix, $MoneyOperation->getAmount()); } } } 




In a real project, the code looks a bit different (more features are supported), I intentionally simplified the code to demonstrate the principle. Unfortunately, the real code and design can not lead - NDA.



(offtop: anticipating the discussion on the topic “what can be improved here?” - something can be done, for example, to postpone statistics recording, in a queue so as not to generate requests to the memory cache in the web request)



As you can see, the code is the simplest and fully corresponds to the formal description of the task. This code is easy to understand and maintain. The classes implement actions strictly at the level of abstraction at which the task descriptions are made (note the PopupMsk27to35 class, which describes only the behavior, and PopupMsk27to35View, which describes only the logic of the VIew level).



All files are in the same folder and when you delete this folder, refactoring is not required - just the functionality disappears from the project.



Questions and answers



Question: that is, the dependencies between the components are given implicitly? Somewhere inside there is a call to events and can’t be traced back to what event what is going on?

Answer: Nothing like that. The fact is that the code of the bundles is generated statically, and you can go inside the call, see what is being called and where. The code will even catch up with IDE and everything will work - autocomplete, syntax highlighting. Outwardly, everything looks as if the code of the bundles of Event / Request and handler was written by the programmer, but in practice the programmer does not need to support it.



Question: is it not clear what is the difference between Event and Request? They look exactly the same.

Answer: the difference is fundamental:

- Event (event) - this is what has already happened. The event can be written to the queue and process delayed. Request - this is what you need to do before continuing calculations.

- Event does not return a result, Request can return a result (and the caller expects this result)

- A Request can have several handlers, but only one of them will work. If no handler will be (or none will work), an exception will be thrown.

How to distinguish requests from events in practice? If some action does not fall into the logic of segregation of duties (in our example, the logic of the popup display conditions should not coincide with the logic of the popup view), we use Request to separate the responsibilities. A simple method call will generate connectivity. While using rekvesta we can show different pop-ups for desktop and mobile clients, without at all touching the logic of the display conditions. Each logic is at its own level of abstraction.

If we want to notify that a certain event has occurred, and we need to have several listeners receive this event, or we don’t care, even if it doesn’t receive any, we use Event s



Question: Will the left subscriber put the whole application? When everything is broken up into components, and the components are “black boxes”, the likelihood of falling due to shit-code is great.

Answer: theoretically, yes, you can cause a fatal error (if we talk about php). In practice, each call turns around in try / catch, information about the speed of execution is automatically collected for each subscriber, and in general everything is under control, accidentally putting a project is not so easy. Plus, unit tests. By the way, I can recommend trying to write unit tests for the code above. It is really very simple.

Plus, the processing of statistics for statistics, for example, can be pushed into the queue with a single line in the config file. And that's it - the execution environment is isolated. This is also a plus for scalability (we automatically get queues from events).



Question: And if the order of execution of handlers is important? This method is no longer suitable?

The answer: of course, important! In the implementation there is a possibility to manage the order on the basis of priorities (weights) or by directly specifying after / before, both for events and requests.



The question is: where to touch in person?

Answer: the existing code is now closed, I am working on the OpenSource implementation of the php and js framework according to these ideas. Post in the comments if there is interest, and I will plan the time more clearly when I can open the framework code for public access.

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



All Articles