Today, an approach to developing an application whose name is
Git Workflow is popular. Sometimes it comes to the fact that when asked if you use this approach, they answer with surprise: “and who does not use it?”. At first glance, this is a really convenient model, simple and clear, but the one who develops with a large number of participants knows how complicated and tedious sometimes merdzhi are. What severe conflicts can arise and what routine work to solve them. Already in the team of two developers, you can hear sighs about the upcoming merger, what about 10, 20 developers? Plus, there are often three main branches - (conditionally) dev, staging, prod - which also someone needs to keep up to date, test and resolve merge conflicts. And not only in one direction, but in the opposite direction, because if a production turns out to be a bug and there is an urgent need to do something, then often the hotfix goes into production, and then the merzhitsya to other branches. Of course, if the team lead or other lucky person responsible for the display is a semi-robot, then the problem is exaggerated. But if there is a desire to try another version of development, then under the cut there is an offer of super potion.

Components
So, two patterns -
Service Locator and
Branch By Abstraction - these are the ingredients for cooking our super potion. I will be guided by the fact that the reader is familiar with the Service Locator, but if not, that’s Martin Fowler’s
article . There is also a lot of literature on the Internet about this pattern, with both positive and negative shades. Someone generally calls him
anti-pattern . You can also read the article
" pros
and cons" in the Habré. My opinion is a very good and convenient pattern that you need to know how to use, so as not to overdo it. Actually, like everything else in our world - find a middle ground.
So the second component is
Branch By Abstraction . Under the link again I send interested in to Fowler, and to whom to laziness - I will briefly describe the essence here.
')
When it comes time to add a new functionality, or refactoring, instead of creating a new branch or editing of 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 is replaced with a new one, tested and laid out on production. That the class does not come into conflict with other components of the system - the class implements the interface.
MoraleIn general, the development of interfaces, is a very good approach and in vain they are neglected. "Program on the basis of the interface, not its implementation"
Often in the tutorials on Branch By Abstracion there is such a text: “First of all, the developer commits the switch for the feature, which is turned off by default, and when the class is ready - turn it on”. But what kind of "switch features", how it is implemented and how the new class will replace the old one - is missed from the description.
Magic
Well, let's mix the ingredients now and get the potion. From the description we go directly to the recipe itself.
interface IDo { public function doBaz(); public function doBar(); } class Foo implements IDo { public function doBaz() { } public function doBar() { } } class Baz implements IBaz { public function __construct(IDo $class) {} }
The task appears to change the work of
doBaz()
and add a new method
doGood()
. Add a new method to the interface, also make a stub in the
Foo
class and create a new class next to the old one:
class FooFeature implements IDo { public function doBaz() { } public function doBar() { } public function doGood() { } }
Great, but now how do we make the “feature switch” and introduce a new class into the client code? This will help the
Service Locator .
File service.php
if ($config->enableFooFeature) {
The Baz class has a dependency on Foo.
Service Locator itself injects the required dependency, the developer only needs to get the class from the
$serviceLocator->get('baz');
locator
$serviceLocator->get('baz');
And what is the super power?
Replacing the old class with a new one occurs in one place and throughout the application where the locator is used. Mentally, you can imagine that you no longer need to search the entire project
new Foo
,
Foo::doSmth()
to replace one class with another.
The condition by which one or another class will get to the locator by key may be anything - setting in the config, depending on the environment (dev, production), GET parameter, rand (), time, and so on.
This flexibility allows you to develop 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 the new feature is disabled in the production configuration file. The functionality that is being developed is visible to other developers. It is possible to test on production, how a new code behaves on a certain percentage of users, or to enable it only for users with certain cookies. The condition of switching on / off the new functionality is limited only by fantasy. You can check the ingenious optimization and quickly see whether it should be used, 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 and forget about it.
And if it suddenly turns out that a new feature on the production has bugs, then you don’t have to convulsively roll back or write headlining hotfix - just disable the condition of adding it to the locator and return the inclusion of stable code to users, and for developers enable the profiler, fix the problem and commit the fix without any
cherry-pick . Shaking before the release with such a potion will be less:

When the new class is finally tested, the old one can be completely removed in order not to produce entities. Also, this concept of development is better placed in working with Continious Integration, if the builds are built from one branch. The green build - production is not broken, you can upload and do not need to merge anything or run the build on the prod branch. The speed of implementation of the new functionality also grows, there are no problems, that master lags 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 Abstraction for assemblies for each client, however with the growth of clients, the number of similar classes increases. At some point they may become too much, and the configuration of the locator is too complex. In this case, it may be more convenient to use branches for customers, but no one bothers to use a super potion inside each branch.
Negative consequences
Rasplode classes can be attributed to the minuses of this approach - if you constantly add new features, refactor and do not finish things, it is easy to clog the project. Also the following situations will not add elegance to the code:
- after refactoring the classes, it turned out that it is possible to refuse from two classes, 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 so changed the task execution that it needed to be renamed. For backward compatibility, the object will need to be stored in a locator under two keys (old and new);
These problems are solved by refactoring the client code for new circumstances, however, the preservation of switching to a new / stable code is lost.
There may also be a situation when a bug is detected in the class from which a copy was made to introduce a new functionality. We'll have to correct the error in two places.
Does anyone use this?
Yes, and if you believe Paul Hamantu, then this approach is practiced in Facebook and Google and is called
Trunk Based Development . In his blog, he has many articles on this topic, if you're interested in reading - that's about
facebook and
google .
Also when developing Chromium, the team works with one trunk branch and feature on / off flags. Since there are a huge number of various tests (12k units, 2k integration, etc.), it does not allow the trunk to turn into a fiend, and the release process helps to keep it at a very high frequency. Read more about this in a good article
here .
In conclusion, I will say that I applied this approach in my practice and was pleased. Try it, maybe it will help you to reduce the amount of work on managing the code, increase productivity and make you happy!