Areas of code hiding and refactoring

    One of the main aspects in the development of software in general and web applications in particular, I consider the ability of software to be changeable - adaptable to changes in the surrounding world. This does not mean that the developer must anticipate future changes in the environment of his code, it means that the code must endure many refactoring cycles, while remaining operational for as long as possible. And for this it is necessary that the consequences of changes made to the code are either foreseeable or predictable. Under the cut, I summarized my understanding of the areas of code hiding, formed as a result of a close, almost intimate relationship with Magento 2 (a platform for building online stores). The foregoing refers firstly to the PHP language, secondly to web applications, and thirdly to everything else.


    Local and global impact of change


    The simplest case is when our changes do not go beyond the local scope. By local scope, I mean the body of the function / method:


    function foo(int $in): int
    {
        $out = $in * 2;
        return $out;
    }

    The code inside the function can be changed as you like, because the effect of changes is visible (limited by the body of the function / method). However, we cannot painlessly change the name of the function - in general, we have no idea who uses our foo () function , and what code will stop executing if we rename the function to boo () . That is, changes in the function body are local (visible), and changes in the function name (signature - taking into account the input and output parameters) are global (unpredictable).


    In the context of this publication, I consider changes in the code, and not in the functionality implemented by the code. Obviously, if the function foo () should add an entry to the database, and after refactoring it removes the entry from the database, then such a sharp change in behavior will lead to unpredictable consequences in the work of all fragments of external code that use the function foo () . However, from the point of view of the call (pairing the function foo () with external code), it remained unchanged.


    Class


    Encapsulation (or rather, hiding) makes it possible to move part of the functions from the global area of ​​influence of changes to a less global one - to the class level:


    class Foo
    {
        private function localChanges() {}
        public function globalChanges() {}
    }

    The part of the code that is in the private functions (and the private functions themselves) can be redesigned quite painlessly for the entire surrounding world. The scope of change is limited to the class body.


    Unfortunately, the same cannot be said about protected functions - the scope of changes for them is no different from public functions. She is also global .


    Class hierarchy


    Good manners (and human abilities) do not recommend us to create "sheets" of code that are several thousand lines of code in the same class. Suppose that we have some functionality for the implementation of which we objectively need to write several thousand lines of code, but it will be called from one point (a public method of some class). Obviously, the scope of changes will be global for only one method, and the rest of the functionality can be distributed according to private methods:


    class MegaFoo
    {
        private function validateInput($in) {}
        ...
        private function prepareOutput($in) {}
        public function exec($in) {}
    }

    The decomposition principle forces us to break the “sheet” into several thousand lines of code into component parts (classes), hiding their internal implementation in private methods, and combine them with each other using public methods:


    namespace Vendor\Module\MegaFoo;
    class Boo
    {
        public function validateInput($in)
        {
            $result = ($in > 0) ? $in : 0;
            return $result;
        }
    }

    namespace Vendor\Module\MegaFoo;
    class Goo
    {
        public function prepareOutput($in)
        {
            $result = number_format($in, 2);
            return $result;
        }
    } 

    namespace Vendor\Module;
    class MegaFoo
    {
        private $boo;
        private $goo;
        public function __construct(
            \Vendor\Module\MegaFoo\Boo $boo,
            \Vendor\Module\MegaFoo\Goo $goo
        )
        {
            $this->boo = $boo;
            $this->goo = $goo;
        }
        public function exec($in)
        {
            $data = $this->boo->processInput($in);
            $result = $this->goo->prepareOutput($data);
            return $result;
        }
    }

    The scope of changes for private methods of created classes will be limited by the bodies of the classes themselves. But the scope of changes to the public methods processInput ($ in) and prepareOutput ($ data) for classes \ Vendor \ Module \ MegaFoo \ Boo and \ Vendor \ Module \ MegaFoo \ Goo will be limited by the class hierarchy:


    • \ Vendor \ Module \ MegaFoo
    • \ Vendor \ Module \ MegaFoo \ Boo
    • \ Vendor \ Module \ MegaFoo \ Goo

    Is it possible from the code of the classes \ Vendor \ Module \ MegaFoo \ Boo and \ Vendor \ Module \ MegaFoo \ Goo to conclude that their scope of change is limited? Unfortunately not. Nothing prohibits any third-party developer from using the \ Vendor \ Module \ MegaFoo \ Boo :: processInput method in their code directly, because Nowhere in the code are there markers restricting such an action. That is, in fact, we have a limited area of ​​influence of changes, but the lack of tools to describe it does not allow us to take advantage of this. Of course, at the level of an individual project, you can discuss such options at the level of agreements in force in the development group.


    Module


    To create complex applications, developers are forced to use each other's results. These results are presented in the form of libraries, frameworks, modules for these frameworks. IMHO, Magento 2 is at the forefront of such cooperation. In fact, this platform is a set of modules (magento modules), created on the basis of some framework (Magento 2), using third-party libraries (Zend, Symfony, Monolog, ...). Magento-module is a completely separate unit from which applications are created and the functionality of which other magento-modules can use. It is quite obvious that the code inside the module as well as in the class can be divided into 2 parts - public and private. Public - this is the code that is supposed to be used by other modules of the final application (in this case I'm not sure that the code, called by the framework itself refers to the public part), private is code that the module developer does not intend to use outside his module. An example of the evolution of Magento 2 proprietary modules shows how a set of public interfaces is formed in a folder./Api/ at the root of the module.


    image


    If we develop this idea, then in the limit we can come to an agreement that the developer of the module explicitly, through interfaces, indicates the functionality that he intends to make public in his module, declaring that all the remaining code refers to the closed part of the module and can be it was redesigned without regard to its use by third-party code. Thus, the effect of changes for the closed part of the module is limited to the files of the module itself, i.e. - becomes visible.


    application


    The development of the idea of ​​declaring explicitly public module interfaces to the application level can be seen on the example of the same Magneto - Swagger API . Even if this level is already weakly related to the areas of influence of changes, because From the point of view of web application developers, the scope of change for the entire application is the same as the global scope.


    Summary


    The quality of refactoring can be significantly improved if the principle of concealment (separation of code into private and public parts) is applied not only at the class level, but also at the level of class groups related to the implementation of a single functional, and at the level of the modules from which the application is built.


    Request not throw tomatoesflood in the comments, if what is stated in the article is not applicable or weakly applicable in your area of ​​activity - this is just a formalization of my personal experience. Do not like it - minus it. And yes, I am aware that all this has already been invented before me. Thanks to those who read it.


    Also popular now: