Области сокрытия кода и рефакторинг

    Одним из главных аспектов при разработке программного обеспечения вообще и web-приложений в частности я считаю способность программного обеспечения быть изменяемым — адаптируемым к изменениям окружающего мира. Это не значит, что разработчик должен заранее предусмотреть будущие изменения среды обитания своего кода, это значит, что код должен переносить множество циклов рефакторинга, оставаясь при этом работоспособным как можно дольше. А для этого нужно, чтобы последствия изменений, вносимых в код, были либо обозримы, либо предсказуемы. Под катом я суммировал свое понимание областей сокрытия кода, сформировавшееся в результате тесных, практически интимных, отношений с Magento 2 (платформой для построения интернет-магазинов). Изложенное ниже относится во-первых, к языку PHP, во-вторых — к web-приложениям, в-третьих — ко всему остальному.


    Локальное и глобальное влияние изменений


    Самый простой случай — когда у нас изменения не выходят за рамки локальной области видимости. Под локальной областью я подразумеваю тело функции/метода:


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

    Код внутри функции можно менять как угодно, т.к. влияние изменений обозримо (ограничено телом функции/метода). Однако, мы не можем безболезненно изменить имя функции — в общем случае мы понятия не имеем, кто использует нашу функцию foo(), и какой код перестанет выполняться, если мы переименуем функцию в boo(). То есть, изменения в теле функции — локальны (обозримы), а изменения в имени функции (сигнатуре — с учетом входных и выходных параметров) — глобальны (непредсказуемы).


    В контексте данной публикации я рассматриваю изменения именно в коде, а не в функционале, реализуемом кодом. Очевидно, что если функция foo() должна добавлять запись в базу, а после рефакторинга она удаляет запись из базы, то такое резкое изменение поведения приведет к непредсказуемым последствиям в работе всех фрагментов внешнего кода, использующих функцию foo(). Тем не менее, с точки зрения вызова (сопряжения функции foo() с внешним кодом) она осталась неизменной.


    Класс


    Инкапсуляция (а точнее — сокрытие) дает возможность переместить часть функций из глобальной области влияния изменений в менее глобальную — на уровень класса:


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

    Часть кода, которая находится в приватных функциях (и сами приватные функции), может быть переработана достаточно безболезненно для всего окружающего мира. Область влияния изменений ограничена телом класса.


    К сожалению про protected-функции того же самого сказать нельзя — область влияния изменений для них ничем не отличается от public-функций. Она также является глобальной.


    Иерархия классов


    Правила хорошего тона (и человеческие способности) не рекомендуют нам создавать "простыни" кода размером в несколько тысяч строк кода в рамках одного класса. Допустим, что у нас есть некоторый функционал, для реализации которого нам объективно нужно написать несколько тысяч строк кода, но вызываться он будет из одной точки (публичного метода некоторого класса). Очевидно, что область влияния изменений будет глобальной только для одного метода, а остальной функционал можно распределить по приватным методам:


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

    Принцип декомпозиции заставляет нас разбивать "простыню" в несколько тысяч строк кода на составные части (классы), скрывая их внутреннюю реализацию в приватных методах, и комбинировать их друг с другом с использованием публичных методов:


    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;
        }
    }

    Область влияния изменений для приватных методов создаваемых классов будет ограничена телами самих классов. А вот область влияния изменений публичных методов processInput($in) и prepareOutput($data) для классов \Vendor\Module\MegaFoo\Boo и \Vendor\Module\MegaFoo\Goo будут ограничены иерархией классов:


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

    Можно ли из самого кода классов \Vendor\Module\MegaFoo\Boo и \Vendor\Module\MegaFoo\Goo сделать вывод об ограниченности их области влияния изменений? К сожалению, нет. Ничто не запрещает какому-нибудь стороннему разработчику использовать метод \Vendor\Module\MegaFoo\Boo::processInput в своем коде напрямую, т.к. нигде в коде нет маркеров, ограничивающих подобное действие. То есть, по факту мы имеем ограниченную область влияния изменений, но отсутствие инструментов для ее описания не дает нам воспользоваться этим преимуществом. Конечно, на уровне отдельного проекта можно обговорить подобные варианты на уровне соглашений, действующих в группе разработчиков.


    Модуль


    Для создания сложных приложений разработчики вынуждены использовать результаты работы друг друга. Эти результаты оформлены в виде библиотек, фреймворков, модулей для этих фреймворков. IMHO, Magento 2 находится на переднем крае подобной кооперации. По сути эта платформа представляет собой набор модулей (magento modules), созданный на базе некоторого фреймворка (Magento 2), использующего сторонние библиотеки (Zend, Symfony, Monolog, ...). Magento-модуль является вполне себе отдельным блоком, из которого создаются приложения и функционал которого могут использовать другие magento-модули. Вполне очевидно, что код внутри модуля также, как и в классе, можно разделить на 2 части — публичную и приватную. Публичная — это тот код, который предполагается к использованию другими модулями конечного приложения (при этом я не уверен, что код, вызываемый самим фреймворком относится к публичной части), приватная — это код, который разработчик модуля не предполагает к использованию вне своего модуля. На примере эволюции собственных модулей Magento 2 видно, как формируется набор публичных интерфейсов в папке ./Api/ в корне модуля.


    image


    Если развивать эту идею, то в пределе можно прийти к созданию соглашения, что разработчик модуля в явном виде, посредством интерфейсов, указывает функционал, который он предполагает сделать публичным в своем модуле, декларируя, что весь оставшийся код относится к закрытой части модуля и может быть им переработан без оглядки на его использование сторонним кодом. Т.о., влияние изменений для закрытой части модуля ограничивается файлами самого модуля, т.е. — становится обозримым.


    Приложение


    Развитие идеи о декларации в явном виде публичных интерфейсов модуля до уровня приложения можно увидеть на примере той же самой Magneto - Swagger API. Пусть даже к областям влияния изменений этот уровень уже слабо относится, т.к. с точки зрения разработчиков web-приложения область влияния изменений для всего приложения совпадает с глобальной областью.


    Резюме


    Качество рефакторинга можно значительно улучшить, если принцип сокрытия (разделения кода на приватную и публичную части) применять не только на уровне классов, но также и на уровне групп классов, связанных имплементацией единого функционала, и на уровне модулей, из которых строится приложение.


    Просьба не кидаться помидорами флудить в комментах, если изложенное в статье не применимо или слабо применимо в вашей области деятельности — это всего лишь формализация моего персонального опыта. Не нравится — минусуйте. И да, я в курсе, что все это уже придумано до меня. Спасибо тем, кто дочитал.

    Поделиться публикацией

    Комментарии 11

      +1

      весьма экзотический способ объяснения такой вещи как "сокрытие информации" (а так же принципов open/close и protected variations).


      не хватает комментариев на тему temporal coupling и о том как это влияет на возможность безболезненного внесения изменений (а так же как с этим temporal coupling бороться), на тему ISP, о том что интерфейсы объектов надо проектировать с точки зрения клиетского кода (тот который ваш объект будет потреблять) и т.д.

        0

        Да, вот были бы в похапешечке модификаторы видимости классов

          0

          Это не сильно поможет изолировать изменения. Основная проблема то не с областью видимости, это было бы слишком просто. Ограничить область использования можно по всякому. хоть за счет @internal в докблоках + статический анализатор. Да и "магенту" очень плохой пример расширяемой системы. Ну то есть она то расширяема, но я не могу назвать это "удобным".

            0

            А "основная проблема", по-вашему — она в чем?

              +1

              Это комплексная проблема. В основном из моих наблюдений:


              • у людей проблемы с декомпозицией
              • люди чаще проектируют снизу вверх, то есть не от клиентского кода отталкиваются когда проектируют интерфейс своего объекта (интерфейс в более общем смысле), а именно когда начинают писать классы и уже потом придумывать как их встроить в систему. Очень хорошо это описывает процесс проектирования сущностей — большинство сначала создает класс, потом поля, а потом только берется делать методы.

              Потому например для некоторых людей так хорошо работает TDD. Оно просто заставляет всегда проектировать код с точки зрения клиентского кода, где намного более четко прослеживаются и границы, и роли объектов.


              • последнее чаще всего приводит к такой вещи как temporal coupling (когда нам надо дергать методы строго в определенном порядке). К этому же приводят и глобальные переменные, и анемичные модели и т.д. И вот это один из самых страшных врагов рефакторинга (на ряду с content coupling)

              А "модификаторы видимости классов" — ну это как кастыль за неимением нормальной системы модулей. В целом это будет работать только если вы уже умеете грамотно выбирать границы между объектами и выделять их поведение. Если так, то не думаю что для вас это будет вообще такой уж большой проблемой.

                0
                у людей проблемы с декомпозицией

                Мир несовершенен. Возможно, когда-нибудь появится "универсальный алгоритм правильной декомпозиции" и тогда влияние этого аспекта нивелируется, но пока так.


                Очень хорошо это описывает процесс проектирования сущностей — большинство сначала создает класс, потом поля, а потом только берется делать методы.

                Может быть это потому, что мы обрабатываем данные. Причем, вначале "данные", а уже потом — "обрабатываем"?


                "Bad programmers worry about the code. Good programmers worry about data structures and their relationships." © L. Torvalds


                ну это как кастыль за неимением нормальной системы модулей.

                Какая, по-вашему, система модулей является нормальной?

                  0
                  Может быть это потому, что мы обрабатываем данные.

                  в том то и проблема, что не совсем так. Нас в первую очередь интересует "что" мы делаем а не "как". Бизнес не особо интересует какие данные мы храним, их интересует что мы с ними можем делать.


                  Good programmers worry about data structures and their relationships." © L. Torvalds

                  В моем случае о объектах и их взаимоотношениях. Напомню что то что внутри объектов в этом случае — второстепенно. Если у вас отношения объектов есть и вы определились что они делают — проперти добавить это не проблема. А если мы сначала выбрали что в объектах хранится а потом уже выбираем что мы будем с этим делать, то тут уже и проблемы с декомпозицией и общие проблемы с рефакторингами.


                  Какая, по-вашему, система модулей является нормальной?

                  • декларативная, что бы можно было на этапе компиляции/парсинга знать что откуда грузить.
                  • возможность переопределять откуда именно грузить модуль, для того что бы иметь возможность подменять на уровне конфигов (опять же что-то декларативное что бы все было известно в компайл тайме)
                  • возможность объявлять сущности языка вне глобальной области видимости

                  Вот тогда вопросы с изоляцией кусков кода отпадут. Но я таких языков не знаю. По сути, из языков с которыми я работал наиболее комфортно работать с модулями в ecmascript последних редакций. Там есть небольшие проблемы со вторым пунктом но можно просто вводить промежуточные модули которые будут являться мостом для предоставления уже нужной реализации.

                    0
                    Бизнес не особо интересует какие данные мы храним, их интересует что мы с ними можем делать.

                    Обрабатывать != Хранить. Тем не менее, для того чтобы делать "что-то" или "как-то" нужны сами данные. Так что данные тут первичны. Не будет данных — бизнесу будет неинтересно ни "что" мы можем сделать с "ничем", ни "как".


                    Если у вас отношения объектов есть и вы определились что они делают — проперти добавить это не проблема.

                    Зачем добавлять что-то, если вы уже опеределились? Мне сложно вас понять, я обычно танцую от данных — есть структура (объект, класс), есть поля (атрибуты, свойства). Есть связи между ними. Наилучшая модель для описания всего этого, IMHO — реляционная. Угадал с отражением предметной области в реляционной модели — получил профит, не угадал — рефакторь, пока не угадаешь. Изменилась предметная область (а она всегда меняется) — рефакторь, даже если угадал на прошлом шаге. Рефакторинг неизбежен. А все эти "что" и "как" — всего лишь надстройка над самими данными.


                    По системе модулей понял.

                      0
                      Тем не менее, для того чтобы делать "что-то" или "как-то" нужны сами данные.

                      Я возможно вас не верно понял, но если мыслить "данными" мы придем к такому код смелу как primitive obsession. Если же мы будем пытаться выделять VO то опять же нам не особо важно какие поля у этих VO будут, нам будет важно только поведение которое от них требуется.


                      Так что данные тут первичны.

                      Когда вы мне дадите определение этому очень расплывчатому термину, что бы мы точно знали что именно мы обсуждаем. Данные как VO, как стэйт разрезанный на объекты? данные как модель данных? Данные как деталь реализации объектов?


                      Мне сложно вас понять

                      попробуйте ради эксперимента представить что у вас нет базы данных, приложение живет вечно и все данные хранятся в памяти в том виде, с которым вам удобно работать. Удобно не в плане "привыкли" а именно удобно. И да, это может порадить определенное дублирование данных так как одна структура данных может быть удобной для одной задачи, а для другой — уже лучше другую, хотя данные одинаковые.


                      Наилучшая модель для описания всего этого, IMHO — реляционная.

                      У нас есть пользователь, его мог пригласить другой пользователь и т.д. до бесконечности. Допустим вы выбрали реляционную модель. И нам пришло требование — надо иметь возможность отобразить полное дерево всех рефералов. Вам все еще удобно использовать чисто реляционную алгебру вашей СУБД для этого? Или нам проще будет строить проекции в какую-то графовую структуру данных чисто под эту задачу?


                      Рефакторинг неизбежен.

                      полностью поддерживаю. Я считаю что рефакторинг должен быть такой же неотъемлемой частью ежедневной разработки как и написание кода который в будете рефакторить. Нельзя знать всего наперед.

                        0

                        "primitive obsession" & "value objects" — это же все, связанное с ООП. Вы привязываете поведение к данным, создавая объекты. Перефразируя Фрейда можно сказать: "иногда данные — это просто данные". Без поведения.


                        Когда вы мне дадите определение этому очень расплывчатому термину

                        Под данными я подразумеваю то, что в очень известной формуле "y = f(x)" находится за символами "x" и "y".


                        у вас нет базы данных, приложение живет вечно и все данные хранятся в памяти в том виде, с которым вам удобно работать.

                        Я надеюсь, вы не привязываете термин "база данных" к способу или формату хранения данных? In-memory DB для меня такое же хранилище, как и файловая система. А глиняные таблички по своим запоминающим свойствам ничуть не хуже "облака" (а с точки зрения энергопотребления — так еще и лучше). Дублирование данных в приложении — сплошь и рядом. Вызов функции — уже дублирование параметров, переданных по значению. ORM (отражение персистентных данных в программные объекты и обратно) и кэш любого уровня — первое, что приходит в голову, если говорить про "определенное дублирование". Я, наверное, еще раз упомяну здесь "хранение != обработка". Обрабатываю я в том виде, в котором мне удобно обрабатывать, а храню — в том виде, в котором удобно хранить.


                        Допустим вы выбрали реляционную модель. И нам пришло требование — надо иметь возможность отобразить полное дерево всех рефералов.

                        Это из той же оперы: "хранение != обработка"


                        Ну, по рефакторингу у нас консенсус :)

            0

            Неожиданный вывод, хотя и вполне логичный.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое