Service Locator and Branch By Abstraction - Super Potion

    Today, a popular approach to developing an application called Git Workflow. It sometimes comes to such that when asked whether you use this approach, they answer in surprise: "who does not use it?" At first glance, this is a really convenient model, simple and straightforward, but those who conduct development with a large number of participants know how complicated and tedious merges are sometimes. What serious conflicts can arise and what routine work to solve them. Already in a team of two developers you can hear sighs about the upcoming merger, what to say about 10, 20 developers? Plus, there are often three main branches - (conditionally) dev, staging, prod - which also someone should keep up to date, test and resolve merge conflicts. And not only in one direction, but also in the opposite, because if there is a bug on the production and you urgently need to do something, then often the hotfix goes into production, and then hedges to other branches. Of course, if the team leader or other lucky person responsible for the layout is a semi-robot, then the problem is bloated. But if there is a desire to try another development option, then under the cat there is an offer of super potion.



    Components


    So, two patterns - Service Locator and Branch By Abstraction - are the ingredients for cooking our super potion. I will be guided by the fact that the reader is familiar with the Service Locator, if not, here is Martin Fowler 's article . There is also a lot of literature on the Internet about this pattern, both with positive and negative shades. Someone even calls him antipattern . You can read the article " pros and cons" on the hub My opinion is a very successful and convenient pattern that you need to know how to use in order not to overdo it. Actually, like everything in our world - find a middle ground.

    So the second component is Branch By Abstraction. By reference I send again those who are interested in Fowler, and to whom laziness - I will briefly describe the essence here.

    When it comes time to add new functionality, or refactoring, instead of creating a new branch or editing the directly required class, the developer creates a copy of the class in which he is developing next to the class. When the class is ready, the original one is replaced by a new one, it is tested and laid out for production. So that the class does not conflict with other components of the system, the class implements an interface.
    Moral education
    In general, the development of interfaces is a very good approach and should not be neglected. “Program based on the interface, not its implementation”
    Often in the tutorials about Branch By Abstracion there is such a text: “First of all, the developer commits a switch for a feature that is off by default, and when the class is ready, turn it on.” But what kind of “feature switch” is it, how it is implemented and how the new class will replace the old one — they are missing from the description.

    Magic


    Well, now let's mix the ingredients and get the potion. We proceed directly from the description to the recipe itself.

    interface IDo
    {
        public function doBaz();
        public function doBar();
    }
    class Foo implements IDo
    {
        public function doBaz() { /* do smth */ }
        public function doBar() { /* do smth */ }
    }
    class Baz implements IBaz
    {
        public function __construct(IDo $class) {}
    }
    

    A task appears to change the work doBaz()and add a new method doGood(). Add a new method to the interface, also make a stub in the class Fooand create a new class next to the old one:

    class FooFeature implements IDo
    {
        public function doBaz() { /* new code */ }
        public function doBar() { /* do smth */ }
        public function doGood() { /* do very good  */ }
    }
    

    Great, but how do we make the feature switch and implement the new class in the client code? This will help Service Locator .

    File service.php
    if ($config->enableFooFeature) { // здесь может быть любое условие: GET param, rand(), и т.п.
        $serviceLocator->set('foo', new FooFeature)
    } else {
        $serviceLocator->set('foo', new Foo)
    }
    $serviceLocator->set('baz', new Baz($serviceLocator->get('foo')));
    

    Класс Baz имеет зависимость от Foo. Service Locator сам инжектит требуемую зависимость, разработчику нужно только получить класс из локатора $serviceLocator->get('baz');

    И в чем супер-сила?


    Замена старого класса на новый происходит в одном месте и по всему приложению, где используется локатор. Мысленно можно представить, что больше не нужно искать по всему проекту new Foo, Foo::doSmth(), чтобы заменить один класс на другой.
    Условие, по которому будет попадать по ключу в локатор тот или иной класс, может быть каким угодно — настройка в конфиге, зависящая от окружения (dev, production), GET параметр, rand(), время и так далее.

    Such flexibility allows us to conduct development in one branch, which is dev and prod at the same time. There are no mergers and conflicts, the developers are fearlessly pushing into the repository, because in the config on production a new feature is turned off. The functionality that is being developed is visible to other developers. It is possible to test on combat production how the new code behaves on a certain percentage of users or enable it only for users with certain cookies. The condition for turning on / off the new functionality is limited only by imagination. You can check out the ingenious optimization and quickly see if it is worth using, whether it will add performance gains and by how much. If it turns out that the new class does not win the old one in anything, just delete it and forget about it.

    And if it suddenly turned out that a new feature on production has bugs, then you do not need to frantically roll back or head over to write a hotfix - just turn off the condition for adding it to the locator and return the inclusion of stable code for users, and for developers enable the profiler, fix the problem and commit without any cherry-pick . Shake before release with such a potion will be less:

    image

    When the new class is finally tested, then the old one can be completely removed so as not to produce essence. Also, such a development concept fits better with Continious Integration if builds are collected from the same branch. Green build - production is not broken, you can upload it and don’t need to merge anything or run the build on the prod branch. The speed of introducing new functionality is also increasing, there are no problems that master is too far behind the dev version.

    It is possible that you are developing a project that has different application functionality for different clients. If there are few such clients, it is also convenient to use Branch By Abstractionfor assemblies for each client, however, with the growth of customers, the number of similar classes increases. At some point, they may become too many, and the configuration of the locator is too complicated. In this case, it may be more convenient to use client branches, but no one bothers to use a super potion inside each branch.

    Negative consequences


    The brood of classes can be attributed to the minuses of this approach - if you constantly add new features, refactor and do not finish the job, it’s easy to clog the project. Also, the following situations will not add elegance to the code:
    • After refactoring the classes, it turned out that you can refuse two classes by replacing them with one, but the client code works with two and takes them from the locator under different keys. You have to put the same object with different keys;
    • after refactoring, the component changed the task so much that it needed to be renamed. For backward compatibility, the object will need to be stored in the locator under two keys (old and new);

    These problems are solved by refactoring the client code to new circumstances, however, the saving of switching the new / stable code is lost.

    A situation may also arise when a bug is detected in the class from which a copy was made to implement new functionality. We'll have to fix the error in two places.

    Does anyone use this?


    Yes, and according to Paul Hamant, then this approach is practiced on Facebook and Google and it is called Trunk Based Development . He has many articles on this topic on his blog, if you are interested in reading, then here is about facebook and google .

    Also, when developing Chromium, the team works with one trunk branch and features on / off flags. Since there are a huge number of various tests (12k unit, 2k integration, etc.), this does not allow turning trunk into a fiend of hell, and the release process helps to keep it at a very high frequency. You can read more about this in a good article here .

    In conclusion, I will say that I applied this approach in my practice and was satisfied. Try it, maybe it will help you reduce the amount of code management work, increase productivity and make you happy!

    Also popular now: