«Божественный» код — громкий термин, который может показаться желтым заголовком, но всё же именно о таком коде будет идти речь: из каких частей он состоит и как его писать. Это история о моих стараниях сделать так, чтобы задачи не возвращались с code review с пометкой: «Всё хе*ня — переделать».

У меня нет профильного образования, и учиться программированию приходилось на практике, через ошибки, ссадины и ушибы. Непрерывно работая над улучшением качества написанного кода, я вырабатывал некоторые правила, которым он должен соответствовать. Хочу ими поделиться.

GOD’S code — акроним из акронимов — код, написанный в соответствии с принципами Grasp, Object calisthenics, Demeter’s law и Solid. Кому-то они знакомы все, кто-то встречался лишь с некоторыми, но мы рассмотрим каждую составляющую акронима. Я не ставлю своей целью детально погрузиться в каждую группу правил, так как на просторах интернета они уже много раз освещались. Вместо этого предлагаю выжимку из собственного опыта.

GRASP


Девять шаблонов для назначения ответственностей классам и объектам. Для удобства запоминания я разделяю их на две подгруппы:

  1. В первую подгруппу можно выделить правила, позволяющие писать атомарные модули, которые хорошо тестируются и изменяются. Эти правила не столько про ответственность, а, скажем так, про свойства модулей: слабая связанность, сильное сцепление, полиморфизм, устойчивость к изменениям. Для себя я перекрываю эти правила SOLID’ом, об этом подробнее — в соответствующей части.
  2. Вторая подгруппа — это уже более чёткие шаблоны, которые говорят нам о том, кто создаёт объекты («создатель» — собирательное название для фабричных паттернов), каким образом понизить связанность между модулями (используя паттерны «контроллер» и «посредник»), кому делегировать отдельные обязанности (информационный эксперт) и что делать, если я люблю DDD и одновременно low coupling (чистая выдумка).

Подробнее можно почитать здесь.

Объектная калистеника


Набор правил оформления кода, очень похожих на свод законов codeStyle. Их также девять. Я расскажу о трёх, которые стараюсь соблюдать в повседневной работе (немного видоизменённых), об остальных можно почитать в первоисточнике.

  1. Длина метода — не более 15 LOC, количество методов в классе — не более 15, количество классов в одном пространстве имён — не более 15. Суть в том, что длинные простыни кода очень сложно читать и понимать. К тому же, длинные классы и методы являются сигналом о нарушении SRP (об этом ниже).
  2. Максимум один уровень вложенности на метод.

    public function processItems(array items)
    {
         // 0
         foreach (items as item) {
              // 1
              for (i = 0; i < 5; i++) {
                   // 2
                   … process item 5 times …
              }
         }
    }

    В этом примере пятикратную обработку item уместно вынести в отдельный метод.

    public function processItems(array items)
    {
         // 0
         foreach (items as item) {
              // 1
              this.processItem(item);
         }
    }
     
    public function processItem(Item item)
    {
         // 0
         for (i = 0; i < 5; i++) {
              // 1
              … process item 5 times …
         }
    }

    Опять-таки, цель — иметь возможность понять, что делает метод, кинув на него один взгляд и не компилируя код в голове.
  3. Не использовать else там, где он не нужен.

    public function processSomeDto(SomeDtoClass dto)
    {
         if (predicat) {
              throw new Exception(‘predicat is failed’);
         } else {
              return this.dtoProcessor.process(dto);
         }
    }

    И брюки превращаются:

    public function processSomeDto(SomeDtoClass dto)
    {
         if (predicat) {
              throw new Exception(‘predicat is failed’);
         }
     
         return this.dtoProcessor.process(dto);
    }

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

Закон Деметры


Жёсткая версия слабой связанности из GRASP’a. Накладывает ограничения на то, с кем может взаимодействовать текущий модуль.



Возьмём три объекта: А содержит B, В содержит С. Рассмотрим объект А. Метод А объекта А может иметь доступ только к методам и свойствам:

  1. Самого объекта А.
  2. Объекта, который передан в качестве параметра методу А.
  3. Объекта В.
  4. Объектов, которые непосредственно созданы внутри метода А.

И всё. Проще говоря, объект А взаимодействует только с непосредственными соседями. Взаимодействие типа this.objectB.objectC.getSomeStuff() является нарушением Закона Деметры, потому что из объекта А мы обращаемся к методу объекта С, который не является непосредственным соседом объекта А.

Есть пара интересных следствий. Во-первых, использование фабрик приводит к нарушению Закона Деметры. Посудите сами:

public function methodA()
{
     spawnedObject = this.factory.spawn();
     spawnedObject.performSomeStuff();
}

Равносильно:

public function methodA()
{
     this.factory.spawn().performSomeStuff();
}

Для решения этой проблемы можно выделить некую обёртку, единственным назначением которой будет создание объекта с помощью фабрики и передача его на обработку куда-либо дальше.

public function methodA()
{
     this.processor.process(this.factory.spawn());
}

Второе интересное следствие: DTO/Entity. Многим довольно часто приходится собирать или использовать контейнеры данных.

public function methodA(SomeDtoClass dto)
{
     dto.getAddress().getCity();
}

Если следовать букве Закона Деметры, это будет нарушением, так как мы обратились к соседу соседа. Но на практике с контейнером данных допускают послабление, считая его как единое целое, и, соответственно, обращение к методу getCity DTO Address в нашем случае считается обращением к части контейнера dto.

Принципы SOLID


SRP, OCP, LSP, ISP, DIP — я лишь вкратце коснусь их, потому как в Википедии и на Хабре они описаны довольно подробно.

SRP — принцип единственной ответственности. Один программный модуль — одна задача, одна причина для изменения. Перекликается с High Cohesion из GRASP’a.

Пример: у нас есть контроллер, задача которого — быть связующим звеном между бизнес-логикой и представлением (MVC). Если мы засунем какую-либо часть бизнес-логики в контроллер, то автоматически нарушим SRP.

public function indexAction(RequestInterface request): ResponseInterface
{
     requestDto = this.requestTransformer.transform(request);
     responseDto = this.requestProcessor.process(requestDto);
 
     return this.responseTransformer.transform(responseDto);
}

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

OCP — принцип открытости-закрытости. Пишем код так, чтобы не приходилось его изменять, а лишь расширять.

Если в выполнении кода нужно сделать какую-либо развилку, то обычно используется if/switch. А если нужно добавить ещё одну ветку, то текущий код изменяется. И это нехорошо. Работает — не трогай. Добавляй новое и тестируй новое, а старое пусть себе работает.

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

final сlass Resolver implements ResolverInterface
{
     private mapping;
 
     public function Resolver(array mapping)
     {
          this.mapping = mapping;
     }
 
     public function resolve(Item item)
     {
          return this.mapping[item.getType()].perform(item);
     }
}

Не забудьте про обработку ошибок, её я опустил. Дам совет: если принять за правило, что все классы могут быть либо final, либо abstract, то следовать этому принципу будет гораздо проще.

LSP — принцип подстановки Барбары Лисков. Корнями уходит в контрактное программирование и говорит о том, как нам правильно строить наследование.

Постулаты таковы:

  1. Производные классы не должны усиливать предусловия (не должны требовать большего от своих клиентов).
  2. Производные классы не должны ослаблять постусловия (должны гарантировать, как минимум, то же, что и базовый класс).
  3. Производные классы не должны нарушать инварианты базового класса (инварианты базового класса и наследников суммируются).
  4. Производные классы не должны генерировать исключения, не описанные базовым классом (методы подкласса не могут генерировать никаких дополнительных исключений, кроме тех, которые сами являются подклассами исключений, генерируемых методами надкласса).

    class ParentClass
    {
         public function someMethod(string param1)
         {
              // some logic
         }
    }
    
    class ChildClass extends ParentClass
    {
         public function someMethod(string param1, string param2)
         {
              if (param1 == '') {
                   throw new ExtraException();
              }
    
              // some logic
         }
    }


В данном примере someMethod класса ChildClass требует большего от клиентов (param2), и к тому же предъявляет определённые требования к param1, которых не было в родительском классе. То есть нарушаются правила наследования, в результате мы не сможем без дополнительных ухищрений заменять использование ParentClass на ChildClass.

ISP — принцип разделения интерфейсов. Говорит о том, что не стоит засовывать все возможные методы в один интерфейс. Иными словами, если мы реализуем в классе интерфейс, и при этом часть методов обязательны к реализации, но нам они не нужны, — значит, стоит разделить этот интерфейс.

interface DuckInterface
{
     public function swim(): void;

     public function fly(): void;
}

class MallardDuck implements DuckInterface
{
     public function swim(): void
     {
          // some logic
     }

     public function fly(): void
     {
          // some logic
     }
}

class RubberDuck implements DuckInterface
{
     public function swim(): void
     {
          // some logic
     }

     public function fly(): void
     {
          // can't fly :(
     }
}

RubberDuck реализует интерфейс DuckInterface. Но, насколько мне известно, резиновые утки не летают, поэтому интерфейс DuckInterface имеет смысл разделить на два интерфейса FlyableInterface и SwimableInterface, и реализовывать уже их.

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

class DIPViolation
{
     public function badMethod()
     {
          someService = new SomeService(445, 'second params');
          // operations with someService
     }
}

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

class DIP
{
     private $service;

     public function DIP(SomeServiceInterface $someService)
     {
          $this->someService = $someService;
     }

     public function goodMethod()
     {
          // operations with someService
     }
}

Итак, в данной статье, я постарался собрать воедино правила, которыми руководствуюсь, чтобы писать более удобочитаемый и «ремонтопригодный» код. Надеюсь, вам понравилось это маленькое приключение в принципы хорошего кода. И когда в следующий раз вы услышите, что код, который вы написали, просто «божественен», вы будете знать, что именно это значит :)

Комментарии (167)


  1. cynovg
    26.06.2018 10:22

    GOD’S code — акроним из акронимов — код, написанный в соответствии с принципами Grasp, Object calisthenics, Demeter’s law и Solid

    При том, что SOLID сам по себе является аббревиатурой, состоящей из аббревиатур и включающий в себя Demetra's law — все это выглядит странно. Осталось только каким-то образом сократить GOD'S code и включит его в SOLID, так сказать для рекурсии.


    1. c3037 Автор
      26.06.2018 11:26

      Рад, что вы оценили игру слов =)


      1. igor_suhorukov
        26.06.2018 22:39

        По хорошему, вместо игры слов лучше использовать в сборке проекта continuous code quality подход с применением статического анализа кода и проверок следования стилю кодирования. Тогда упрощается и процедура ревью кода — код который нарушает правила просто не собирается в бранче на CI сервере. Есть ещё фреймворк, которые проверяют базовые архитектурные соглашения и точно так же "рушат" сборку при нарушениях.


        1. c3037 Автор
          26.06.2018 22:52

          Я согласен с тем, что continuous code quality классная вещь, вот только мне как разработчику для ускорения своей работы полезно знать правила по которым проходит оценка качества кода.


          1. igor_suhorukov
            26.06.2018 22:58

            Безусловно, понимать нужно и, например, Sonar показывает что нарушено, почему это плохо и очень часто как нужно было делать, чтобы реализация была правильной с точки зрения этого правила. А плагин к вашей IDE позволит получать предупреждения в процессе написания кода.


    1. dopusteam
      26.06.2018 12:10
      +1

      При том, что SOLID сам по себе является аббревиатурой, состоящей из аббревиатур и включающий в себя Demetra's law — все это выглядит странно


      Разве SOLID включает в себя Demetra's law?


      1. cynovg
        26.06.2018 13:18

        Вы правы, а я — нет


    1. totally_nameless
      27.06.2018 21:27

      То есть архетиктура акронима не удовлетворяет требованиям, которые он содержит :)


  1. mayorovp
    26.06.2018 10:41

    По поводу закона Деметры и фабрик я несогласен категорически.


    Фабрика — это абстрагированный конструктор. А значит, объект который фабрика вернула, должен считаться созданным в вызвавшем фабрику методе.


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


    foo = factory.create();
    foo.bar();
    foo.close();

    Но если бездумно следовать закону Деметры — то вызовы bar и close придется вынести в отдельный метод… который в итоге будет закрывать объект не им открытый.


    1. AxisPod
      26.06.2018 11:10

      Вариантов море
      1. RAII, в этом случае конструктор объекта создаст внутренние и close в этом случае адекватен, в рамках управляемого кода, в рамках же того же C++ вместо close будет деструктор.
      2. DI, здесь close не будет сам закрывать, а просто дёрнет сервис для закрытия внутреннего объекта.
      3. Я частенько в своём коде ипользую некие IGuard объекты, следящие за ресурсами, которые сами знают что и как делать.

      Если же вы говорите вообще как close в противопоставление factory.create(), то здесь вы не ресурс уничтожаете, вы говорите, что надо бы вычистить внутренее состояние объекта, а не сам объект и это нормально.

      В итоге в C# используем IDisposable для этих целей, по нему же и описание ловим и работаем так

      using (var foo = factory.Creat())
      {
        foo.bar();
      }

      В C++ используем std::unique_ptr как результат фабрики, само удалиться, тут по хорошему работать только с RAII.

      И ничего для закрытия вручную не дёргаем.


      1. mayorovp
        26.06.2018 11:29

        Ну, под капотом using — обычный вызов Dispose, а под капотом RAII — деструктора. Не вижу причин почему генерируемый компилятором код имеет право нарушать правила.


        1. sys_int64
          26.06.2018 20:14

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


    1. c3037 Автор
      26.06.2018 11:35

      Не поймите неправильно, я ни в коем случае никого не призываю бездумно следовать тому, что описано в статье. Стоит помнить, что Закон Деметры, как и любой другой принцип стоит применять в меру и не вдаваться в крайности. Я описал следствия, которые действительно имеют место быть. Если в каком-то случае пренебрежение его использованием позволит улучить кодовое решение, то разумнее это сделать.


  1. Aquahawk
    26.06.2018 11:49
    +15

    Критика подхода ограничения размера метода.
    Просто ужасные советы если отнести к производительности. Напишите в таком виде алгоритм который осуществляет свёртку изображения по ядру произвольного размера. У вас там будет 4 вложенных фора в любом случае. А если несколько свёрток за раз по одному исходнику так 5.
    Да и вообще есть миллион ситуаций где нельзя размазывать код. Сложные вещи останутся сложными. Вы можете спрятать эту сложность в тонне классов и кода, но лучше не станет. Станет ещё сложнее новичку увидеть и так сложный алгоритм сквозь размазню методов. Бить нужно логически так как бъётся алгоритм, так чтобы было читаемо и логически понятно что делается и как. А это приходит с опытом. А то бывает смотришь, всё вылизано по кодстайлам, куча маленьких методов, всё туда-сюда передаётся, идилия просто, каждый метод прозрачен как слеза младенца. Но хрен поймёшь что весь этот океан делает. Я не призываю всё сляпывать. Я призываю думать и обеспечивать прозрачность архитектуры и понятность алгоритмов, и это не правилом 15ти строк делается.


    1. Artyomcool
      26.06.2018 16:57

      Чтобы правильно нарушать правила, нужно для начала научиться им следовать. Бить логически — сложно, это думать надо, и как вы правильно заметили — иметь опыт. С чего-то нужно начинать, чтобы этот опыт заполучить. Вероятно, новичку было бы полезно иметь набор простых советов из разряда «15 строк и не больше». Со временем придет понимание, что это далеко не всегда верно.
      Касательно же производительности: в современном программировании производительность за пределами асимптотики нужна крайне редко. И даже в этом случае сначала было бы неплохо написать код понятным, а уже потом аккуратно профилировать и оптимизировать.


      1. Aquahawk
        26.06.2018 23:43
        +1

        Не согласен. Применять практику не понимая зачем она странно. Имхо нужно упасть с велосипеда чтобы понять как ехать. В IT правда для этого надо поработать 2-3 года на одном проекте, чтобы самому оценить как работают твои же решения. Вот тогда появляется червячок мысли о том как читать книги по паттернам и на что на самом деле смотреть и как читать между строк.


      1. Druu
        27.06.2018 05:10
        +1

        Чтобы правильно нарушать правила, нужно для начала научиться им следовать. Бить логически — сложно, это думать надо, и как вы правильно заметили — иметь опыт.

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


        Вероятно, новичку было бы полезно иметь набор простых советов из разряда «15 строк и не больше»

        Совет должен выглядеть как: "если метод длиннее 15 строк, то стоит обратить на него внимание, и возможно вынести часть логики, если это будет разумно". И ни в коем случае не должен выглядеть как: "15 строк и не больше".


        1. Artyomcool
          27.06.2018 18:23

          Пожалуй, вы правы.


    1. gandjustas
      26.06.2018 17:05
      +1

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


      1. arTk_ev
        26.06.2018 17:16
        +1

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


        1. gandjustas
          26.06.2018 17:42
          +1

          Проекты с огромным количеством лапши приносят деньги, в том числе программистам, проекты с красивым кодом, которые никогда не выходят в продакшн — закрываются.


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


          Никто не говорит, что улучшать код не надо. Работаешь на поддержке — улучшай код, а не создавай еще больше лапши, кто тебе мешает?


          Я, вот, работал на проектах поддержки, который писали более 5 лет до этого, и я улучшал код.


          1. arTk_ev
            26.06.2018 18:45
            +4

            image
            Но git помнит ваши имена ))
            Если вы бы работали на поддержке, то бы знали, что любой рефакторинг запрещен в принципе, даже обычное переименование. И говно-код занимает больше времени, он даже больше по объему. И разве не проще сразу писать нормальный рабочий код?


            1. gandjustas
              27.06.2018 01:00

              Я работал на поддержке если что.
              Даже больше скажу, сейчас у меня хобби-проект — моды к компьютерной игре. В модах есть унаследованный код, которому больше 5 лет, код других моддеров, который надо иногда править и обновлять (обновления о наших правках не знают ничего). Из инструментов — только текстовый редактор с подсветкой синтаксиса. Тестирования как такового нет, каждый тестовый цикл это запуск игры и около 3 минут ожидания.


              И ниче: правим, рефакторинг делаем, добавляем и (о ужас!) убираем код. Около 300к строк.


              1. Neikist
                27.06.2018 07:49

                Эм… Ну хз, у нас вот в принципе по максимуму запрещено менять любой код с модификаторами public, только в совсем крайних случаях можно. И это продукт с исходным кодом который клиентам отдается. А запрещено поскольку клиент мог сделать себе доработки которые на этот код опираются. А уж на поддержке и вовсе жуть может быть.


                1. gandjustas
                  27.06.2018 18:13
                  +1

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


                1. zenkz
                  27.06.2018 19:16

                  Давно уже придумали интерфейсы и SOLID-принципы… Клиенты не должны делать доработки внутри общего кода. Если у вас происходит такое, то вам срочно нужно от этого отказываться, т.к. это постоянный источник плохого качества кода и трудноуловимых багов… В идеальном случае клиенту предоставляется интерфейс/API для взаимодействия… А всё что внутри модуля может меняться постоянно и это нормально…


              1. pesh1983
                27.06.2018 10:39

                А представляете, как было бы замечательно, если бы код был понятнее и его поддержка занимала в 2 раза меньше времени, например. На самом деле, очень важно найти золотую середину: не писать лютый говнокод, но и не стараться добиться идеальной структуры кода, который даже не работает ещё. Все упирается естественно в ресурсы, в том числе времени, но важно помнить, что сэкономив время сейчас и набросав говнокод, его поддержка потом может съесть больше времени, чем вы выиграли на данном этапе. Так что важен баланс


                1. gandjustas
                  27.06.2018 18:19
                  -1

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


                  А теперь самая важные фишка о которой все забывают:
                  1) "Идеальная архитектура" и "говнокод" это эмоциональные оценки. К формальным характеристикам сложности, связности, читаемости, скорости работы и плотности багов отношения не имеют.
                  2) "Идеальная архитектура" и "говнокод" это не противоположные стороны спектра. Может быть "говнокод" при почти "идеальной архитектуре".


                  1. GeekberryFinn
                    27.06.2018 18:58
                    +1

                    «Идеальная архитектура» и «говнокод» это не противоположные стороны спектра. Может быть «говнокод» при почти «идеальной архитектуре»

                    Может. Потому что «идеальная архитектура» это идеальное разбиение программы на модули. И внутри модуля может иметься говнокод, но, разница не в этом, а в том что
                    — «идеальная архитектура» — при чистке говнокода ничего не ломается;
                    — «дерьмовая архитектура» — при чистке говнокода ломается ВСЁ и приходится лепить множество заплат;
                    — бывают и промежуточные состояния архитектуры.


                  1. pesh1983
                    27.06.2018 19:07

                    Вы невнимательно читали. Я не писал ничего про архитектуру), я также не писал, что идеальный нерабочий код лучше, чем рабочий говнокод. А писал я про баланс. Если ваш бизнес не выделяет достаточно времени, чтобы писать нормальный код, то это может быть отправдано в следующих случаях: 1. У руководства есть понимание, что мы сейчас пишем быстро говнокод и в дальнейшем, если мы планируем поддерживать этот код, мы его причешем, перепишем и так далее. 2. Вы не планируете поддерживать написанный код, например, пишете прототип. Во всех остальных случаях руководство скорее всего не понимает проблему наличия говнокода и руководствуется планами только на ближайшую перспективу. Это, кстати, довольно распространенный подход. Хотя конечно, еще возможно, что денег много, и ничего страшного, если поддержка и внедрение фичей будут занимать больше времени. Ну тут, как говорится, "кто платит, тот и музыку заказывает", you are the boss)


                    1. gandjustas
                      27.06.2018 19:16

                      Давайте считать, что срок фиксирован и всегда недостаточный, чтобы сделать все хорошо. Делать сразу, чтобы код был SOLID или написать быстро, чтобы работало, а потом поправить там, где архитектура совсем неочень получилась?


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


                      1. GeekberryFinn
                        27.06.2018 19:22
                        -1

                        а потом поправить...

                        Если архитектура дерьмовая, то поправить что-то не сломав другое — не получится, в результате придётся лепить много заплат из-за «одно — лечим, другое — калечим».
                        При правильной архитектуре — можно вносить изменения ничего не ломая.


                        1. gandjustas
                          27.06.2018 19:25

                          Поправить всегда можно. Код это же не кирпичи в стене нижнего этажа.


                          1. GeekberryFinn
                            27.06.2018 19:27

                            Если всегда, то почему при починке говнокода лезут новые баги и глюки?


                            1. gandjustas
                              27.06.2018 19:32

                              У кого лезут? У меня не лезут.


                              1. Neikist
                                28.06.2018 07:00

                                Если не лезут — это как раз значит что архитектура более менее. Возможно вы просто не видели настоящее отсутствие архитектуры. Ну либо вы тратите например 5X времени вместо X.


                                1. gandjustas
                                  28.06.2018 18:32

                                  Я много чего видел, в том числе то, что вы можете назвать "настоящее отсутствие архитектуры".


                                  Ну либо вы тратите например 5X времени вместо X.

                                  Чтобы потратить Х вместо 5Х при поддержке в "будущем" надо потратить 20Х на сейчас. "будущее" в кавычках, потому что оно может и не наступить.


                                  1. Neikist
                                    28.06.2018 19:45

                                    Я много чего видел, в том числе то, что вы можете назвать «настоящее отсутствие архитектуры».

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


                                    1. gandjustas
                                      29.06.2018 03:49

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


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


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


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


                                      1. Neikist
                                        29.06.2018 07:36
                                        +1

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

                                        Ну так это уже можно сказать приемлемая архитектура, пусть еще и не хорошая.


                      1. zenkz
                        27.06.2018 19:58

                        Неужели писать код по SOLID настолько дольше?!
                        Или вы как в Delphi — весь код в button1_click пишите? Конечно важно как можно быстрее выдать продукт рынку, но через 1-2 года поддержка этого кода и добавление новых фич будет стоить компании в разы дороже, чем если бы сразу написать хотя бы приблизительно соблюдая SOLID

                        Когда нужно быстро — я всё равно делю код на классы (хотя бы уровня структура данных — контроллер/accessor — UI), для контроллеров делаю интерфейсы и резолвлю их через ServiceLocator/DI — если этого не делать, то потом юнит тесты замучаешься писать… Да и код по слоям разделён хотя бы…


                        1. gandjustas
                          27.06.2018 20:10

                          Неужели писать код по SOLID настолько дольше?!

                          Зависит от степени упоротости в применении принципов SRP и DIP. В частности у меня был код, который в 10 строках читал данные из двух источников и записывал один. Чтение из каждого источника было одной строкой.
                          Посоны, которые делали по SOLID на каждую строку фиганули по классу. Потом выделили интерфейс, обратили зависимости. Потом из класса сделали базовый и два наследника, один тестовый, а второй обращался к системе. В итоге две строки превратились в двадцать две. Это не считая тестов.


                          Естественно две строки были написаны за 5 минут (вместе с проверкой), а 4 класса и инверсия зависимостей потребовали почти полдня работы.


                          Или вы как в Delphi — весь код в button1_click пишите?

                          Сейчас везде по дефолту MVC и MVVM, батонкликов не найдешь.


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


                          1. GeekberryFinn
                            27.06.2018 20:16

                            Сейчас везде по дефолту MVC и MVVM, батонкликов не найдешь.

                            А что там сейчас?
                            (так вышло, что с Дельфи перешёл на Оракл, и про то как сейчас не в курсе)


                          1. zenkz
                            27.06.2018 21:28

                            Ну ваш пример с SOLID — жесть конечно. Нарушает мои любимые принципы KISS и YAGNI. Чем больше кода напишите, чем тяжелее поддерживать. В данном случае те товарищи слишком буквально поняли SRP и вместо разделения ответственности стали плодить сущности… А YAGNI принцип нарушается, т.к. скорее всего никто не будет добавлять новые источники данных, а значит все эти интерфейсы будут лежать мёртвым грузом в проекте. Я бы рассмотрел эту задачу как конвертер, а значит обойтись одним интерфейсом IDataConverter, который умеет считать (не важно из скольки источников) и записать… Код бы стал на 4 строчки длинее и появилась бы возможность легко написать юнит тесты…


                            1. gandjustas
                              27.06.2018 23:09

                              Почему же не поняли? Очень даже поняли, потом долго рассказывали что теперь в конфиге можно источники данных подменить, не трогая остальную логику.


                              С источниками данных, кстати, не самая большая проблема была. Просто самая показательная.


                          1. Neikist
                            28.06.2018 07:03

                            Сейчас везде по дефолту MVC и MVVM, батонкликов не найдешь.

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


                            1. Druu
                              28.06.2018 07:19

                              А вы можете еще куда-то налепить, если надо на событие формы отреагировать? :)


                              1. Neikist
                                28.06.2018 07:33

                                Ну так одно дело отреагировать, и другое — всю бизнес логику работы с данными в событии формы выполнять.


                              1. mayorovp
                                28.06.2018 08:51

                                В обработчик команды «налепить» можно.


                      1. pesh1983
                        27.06.2018 21:15

                        Мне правда жаль, что у вас всегда недостаточно времени для качественного выполнения своей работы. Это в большинстве случаев проблема менеджмента. Поменяйте работу, поверьте, есть компании, в которых и планирование адекватное, и при этом даже ЗП хорошую платят, ну и проекты уж точно не скучные. А пользователю действительно пофиг, как вы правильно заметили. Ему ж не придётся поддерживать говнокод. А вам придётся, ну или вашим коллегам.


                  1. zenkz
                    27.06.2018 19:21

                    «Идеальная архитектура» — это конечно субъективное понятие, а вот «говнокод» — вполне измеримое. Мои любимые принципы разработки: DRY, KISS and YAGNI. Т.е. любое изменение в коде должно происходить в одном месте (т.е. без необходимости использовать текстовый поиск и замену кода во многих файлах), код должен легко читаться и позволять понять его назначение, весь неиспользуемый код должен быть удалён. Для меня эти принципы важнее SOLID и прочих — и если они нарушаются — это и есть говнокод…


            1. gandjustas
              27.06.2018 01:06
              +1

              Если бы было проще писать "нормальный рабочий код", то его писали все. Но раз "нормального рабочего кода" оказывается в итоге крайне мало, то видимо не проще.


              Не проще, потому что люди сначала долго рисуют диаграммы классов (иногда в голове), разделяют интерфейсы, инвертируют зависимости, делают так, чтобы код соответствовал OCP, SRP и еще каким-то аббревиатурам, а потом сроки подходят и срочно лепится абы-какой код, чтобы все взлетело.


              Достаточно сделать просто в обратном порядке — сначала чтобы работало, а потом чтобы было красиво. Но для этого требуется скилл и выдержка.


              1. sic
                27.06.2018 02:49

                Тут еще проблема, что поди все это объясни людям. У меня аж в трех компаниях подобная практика была, что сначала я пишу по задаче PoC код, причем и на планированиях отмечаю, что это proof of conect и в комментариях все помечаю, и в связной информации в системах контроля версий, трекерах и прочее, и каждый раз на меня смотрят «оО у нас так плохо и джуниоры не пишут». Разумеется, к PoC требований ну почти никаких, запросто бывает и полотнище размером в 1 метод на 1000 строк, и копипаста из интернетов, заместо каких-то уже библиотечных функций, и все константы не вынесены, ну не знаю, чем еще напугать, регистр переменных и идентификаторов рандомный, но зато, в среднем, это раз в 10 быстрее, чем писать сразу нормально, и отрефакторить это нормально займет не более трети времени от «сразу писать нормально».

                Но нет. «А как же мы будем юнит тесты писать?» «У нас к каждому спринту юнит тесты — это обязательная часть acceptance criteria.»

                И пофиг, что большинство фич идет с отстованием в три спринта, пофиг, что несмотря на юнит тесты продакшн все равно постоянно падает (поэтому релизится то, что когда-то считалось не очень падающим), и пофиг, что бизнес через раз говорит, что ну, знаете, а эта фича не очень-то нам и нужна будет.

                MVP? Такого слова большинство коллег и не слышало. А менеджеры работают так, что если большинству что-то не нравится, значит — это что-то плохое.


                1. Neikist
                  27.06.2018 07:51
                  -1

                  А теперь два вопроса. В вашем коде при необходимости что то поправить сколько людей с ходу разберутся? И приводите ли вы его в порядок? А то может и падает то из за такого кода.


                1. pesh1983
                  27.06.2018 21:04

                  У вас фигово процессы настроены. То, что вы не укладываетесь в сроки, в большинстве — результат плохого планирования. Мудрый менеджер возьмёт оценку программиста, умножит ее на 2 и ещё добавит половину, и даже такую оценку стоит брать во внимание с учётом других факторов, которые могут не зависеть от сроков выполнения задачи. А юнит тесты хороши, как вспомогательный инструмент, как гарантия,, что реализованный код работает правильно на данном этапе, ну и для устранения регрессий, но уж точно не как гарантия того, что ваш код и все остальное будет работать в продакшене в связке. Для этого есть ревью кода, процедура приемки со стороны QA, стейджинг и много всего ещё по желанию. А если у вас все это есть, но проекты продолжают падать в проде, значит у вас где-то что-то не отрабатывает как надо, поскольку до прода непротестированный проект при хорошо поставленных процессах просто не доедет) Даже с лютым говнокодом и без тестов можно жить, если есть правильно поставленная проверка QA на стейджинге) Не так конечно хорошо, как с тестами и не так быстро, потому что количество возвратов и багфиксов возрастает, но в целом можно. А вот наоборот — нет.


          1. arTk_ev
            26.06.2018 19:22
            -2

            С математической точки зрения, лапша-код куда более интереснее для меня, с недавних пор). Так что не утверждаю, что такой код объективно хуже.


            1. sshmakov
              26.06.2018 20:21
              +2

              Раскройте тайну, что интересного в лапше с математической точки зрения? Мне тоже интересно.


              1. arTk_ev
                26.06.2018 20:36

                натуральный Deus ex machina. Нынче вновь становятся популярны теория Хаоса и синергетика.
                Думаю у каждого были интересные и весьма неожиданные баги. И в теории, можно получить такую систему, которая будет производить только положительные баги)


  1. arTk_ev
    26.06.2018 16:18

    Задача программиста — упрощать сложную(системная сложность) систему и внедрить метод управления.
    Для проектирования архитектуры и вообще главный принцип — это KISS и декомпозиция модулей.
    Упрощение — это минимизация количества связей и возможных состояний системы. Для этого применяют инкапсуляцию и шаблоны проектирования, SOLID.
    Для минимизации состояний используют минимизацию ветвления кода(все if) и декомпозицию методов(маленькие простые вложенные методы)
    Для управления систем, чаще всего, используют классическую пирамиду власти из кибернетики.
    Для понимания как нужно строить архитектуру и откуда берутся баги, нужно прочитать системный анализ. Прочитать можно за пару дней, никаких доп. знаний не нужно.


  1. gandjustas
    26.06.2018 16:58
    +1

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


    1. c3037 Автор
      26.06.2018 19:39
      -1

      Позволю себе не согласиться. Не вижу ничего плохого в том, что бы думать об архитектуре в момент написания кода. У кода, как и бизнеса, есть одно постоянное свойство — меняться. Да, разумеется, иногда нужно по-быстрому сделать MVP и выпустить его в продакт, но по моему опыту нет ничего более вечного, чем временное. Если конечно у вас не конвеерное производство MVP, то закладывание возможности изменений принесёт больше пользы, чем времени отнимет. К тому же, следование описанным принципам со временем уже просто откладывается на подкорке и каких-то особых усилий не требует — проверено на себе и коллегах.


      1. gandjustas
        27.06.2018 00:54

        Плохо то, что программисты не могут остановиться. Улучшать код можно почти до бесконечности. А если кода еще нет, то мыслительный процесс может вообще не остановиться.


        Многократно себя ловил на мысли о том, что начинаю улучшать код до его написания, что приводит к тому, что я не могу начать писать.


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


        1. Neikist
          27.06.2018 07:52

          то закладывание возможности изменений принесёт больше пользы, чем времени отнимет.

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

          Не видите некоторого противоречия? Не закладывания возможностей, а закладывания возможности изменения кода. Это все таки разные вещи.


          1. gandjustas
            27.06.2018 18:23

            Не вижу. В обоих случаях это добавление кода, который не нужен сейчас, но возможно понадобится в будущем.
            Плотность багов величина почти постоянная, поэтому чем больше кода, тем больше ошибок. Чем больше кода, тем больше требуется времени на поддержку.


            1. GeekberryFinn
              27.06.2018 18:34

              Нет, это не добавление кода «который не нужен сейчас, но возможно понадобится в будущем», а создание возможности легко добавить такой код в случае необходимости не сломав ничего.
              Что достигается продуманной архитектурой = без кривого прибивания гвоздями «тяп-ляп и в продакшн».


              1. gandjustas
                27.06.2018 18:38

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


                1. GeekberryFinn
                  27.06.2018 18:54

                  Продуманное разбиение программы на модули, которые возможно изменять и дополнять не ломая всего остального. То есть не создаётся «который не нужен сейчас, но возможно понадобится в будущем», а создаётся код который сейчас нужен и используется для взаимодействия между модулями.

                  есть некоторый код, который за это «создание возможностей» отвечает?
                  По сути да, но этот код не висит мёртвым грузом «который не нужен сейчас, но возможно понадобится в будущем», а реально используется и работает.
                  Это как разъёмные соединения — вместо жёсткого прибивания гвоздями.
                  И если, что-то нужно изменить, то достаточно изменить конкретный модуль, а не лепить поверх всего заплату.


                  1. gandjustas
                    27.06.2018 19:20

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


                    Но в жизни не встречал ни разу такого. Скорее было обратное. Ради "дополнительных возможностей" люди тратили в 10 раз больше времени, писали в 4 раза больше кода (а в совокупности с тестами в 8 раз больше), делали "идеальную" SOLID архитектуру, но код был нерабочим. Тупо не делал то, что нужно.


                1. GeekberryFinn
                  27.06.2018 19:04
                  -1

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

                  Или есть некоторый код, который за это «создание возможностей» отвечает?

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


                  1. gandjustas
                    27.06.2018 19:21

                    Доказательство по аналогии ущербно изначально. Потому что надо сначала доказать верность аналогии.


                    1. GeekberryFinn
                      27.06.2018 19:26
                      -1

                      Вы gandjustas сначала приписали другим "добавление кода, который не нужен сейчас, но возможно понадобится в будущем".
                      А когда вам сказали о том, что вы не правы и это не так — начали придумывать отмазки!
                      Слив засчитываем?


                      1. gandjustas
                        27.06.2018 19:27

                        Я чето даже хотел ответить, но понял что смысла в этом нет


                  1. Druu
                    28.06.2018 06:45

                    Если у вас на машине колёса — жёстко приколочены к оси по принципу «тяп-ляп и в продакшн», то вам чтобы на машине повернуть придётся поверх уже имеющихся колёс лепить «заплату» из дополнительных колёс, которые могут поворачиваться.

                    Если у вас требования ВНЕЗАПНО меняются с "приколотить колеса намертво" к "обеспечить возможность руления", то какие-то архитектурные недостатки (возможные) — наименьшая из ваших проблем. И уж точно не надо пытаться эту проблему решать архитектурными костылями.


                    1. JobberNet
                      28.06.2018 07:57

                      Увы, на практике часто такое встречается, когда клиент захотел именно именно такую «мелкую фичу», и искренне считает, что это «легко сделать».

                      А «приколотить колёса намертво» — это не требования клиента, которому на такие «мелочи» — глубоко пофиг, а изначальная архитектура доставшаяся в качестве легаси.


                1. mayorovp
                  28.06.2018 08:55

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


    1. sys_int64
      26.06.2018 20:21

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


    1. sic
      27.06.2018 00:56
      +2

      Тут главное не запутаться. Излишней приверженности правильной архитектуры не бывает, ибо архитектура практически всегда, для любого приложения будет морфироваться в худшую сторону со временем. Ну кроме приложений «написал и выбросил», но имхо к ним вообще не может быть никаких требований кроме надежности/безопасности.

      Другое дело, что стилистика написания кода к архитектуре имеет лишь условное отношение. И излишняя декомпозиция на ранних этапах, да, зло, не меньшее, чем преждевременная оптимизация. Хотя бы потому, что декомпозировать всегда проще, чем объединить уже разбитые (и особенно сомнительно) сущности.

      А об оптимизациях, ну вот в восновном все и тормозит, ибо из-за слепых требований к объему методов, простоты форматов, у нас в доброй части приложений, чтобы 2+2 сложить, в 15 LOC методе эти данные сначала из JSON парсятся, потом обратно запихиваются, и чтоб скучно не было, это все еще сидит под жирным мьютексом, ибо чего плодить объекты синхронизации. Зато SRP, OCP, LSP, ISP, DIP, и прочее, что нужно каждому сеньору потрогать в продакшн на уровне эксперимента, чтобы потом понять, что в принципе, программирование никогда об этом и не было.


    1. JobberNet
      27.06.2018 08:47

      Излишняя приверженность «правильной» архитектуре и коду стала сродни преждевременно оптимизации.

      Ошибки лучше предотвращать на стадии проектирования, чем потом выяснить что архитектура настолько кривая, что лучше бы переписать весь проект с нуля. :(


      1. Neikist
        27.06.2018 09:20

        Именно, вспоминаются исследования приводимые макконелом в «Совершенный код», которые показывали что стоимость ошибок на каждом этапе: выявление требований->проектирование->конструирование повышается в разы, а то и на порядок.


      1. Druu
        27.06.2018 10:14
        +1

        Ошибки лучше предотвращать на стадии проектирования, чем потом выяснить что архитектура настолько кривая, что лучше бы переписать весь проект с нуля. :(

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


        1. Neikist
          27.06.2018 10:57

          А тут вступает в игру другой фактор. Часто после того как накидали что то для проверки концепта — это что то остается в таком виде поскольку «Ну переписать это же ресурсы нужны, а у вас вон уже продукт есть, давайте быстро еще фичи.» И похрен что все страдают как из-за непродуманной бизнес логики, так и из за говнокода в который изменения просто страшно вносить. Ибо «что за тесты такие? Это же ресурсы нужны».


          1. Druu
            27.06.2018 12:18

            А тут вступает в игру другой фактор. Часто после того как накидали что то для проверки концепта — это что то остается в таком виде поскольку «Ну переписать это же ресурсы нужны, а у вас вон уже продукт есть, давайте быстро еще фичи.»

            Вот видите. Продукт оказался востребованным, есть бюджет, есть требование новых фич. А если бы планировали — то никто бы новых фич не требовал, потому что продукт оказался бы нафиг никому не нужен к тому моменту, как вы там чего-то допланировали :)


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


            Ну и:


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

            Вы страдаете, конечно, но в итоге вносите. Вам за это деньги и платят. И пофиг заказчику на ваши страдания, ему главное чтобы вы фичи вовремя внедряли.


            1. Neikist
              27.06.2018 12:25

              А вот тут есть еще пара но. Таким образом шла разработка второй версии продукта, когда первой уже было лет 7, и она нормально продавалась. И страдаю не я, страдают пользователи и заказчики из за того что фичи долго внедряются и новые ломают старые из за кучи костылей и отсутствия тестов. Собственно этой второй версии уже почти 4 года и почти ничего не меняется.


              1. Druu
                27.06.2018 12:32

                А вот тут есть еще пара но. Таким образом шла разработка второй версии продукта, когда первой уже было лет 7, и она нормально продавалась.

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


                В общем, суть в том, что часто бывают рассуждения вида: "вот сразу не делали все по науке, не учли роста, а теперь приходится жрать кактус", но они не учитывают, что очень часто именно тот факт, что делали вначале "херакс-херакс и в продакшн", и позволил "вырасти" и жрать теперь кактус.


                1. Neikist
                  27.06.2018 12:46

                  Я не пойму, вы всерьез считаете что если все время разработчиков тратится на исправление ошибок и ком только растет — это нормально? Что когда из соседних отделов людей выдергивать приходится на латание ошибок? И иногда еще фичи добавляются (если не добавить — вообще вздернут клиенты).
                  Окей, выпустили MVP, оценили что нужно, самое критичное реализовали, может пора уже и что нибудь поправить? Хотя бы избавиться от методов на тысячи строк, конструкций if… else вложенных в одной процедуре по 7-10 уровней, от функций сигнатура которых больше чем наполовину состоит из флагов которые передаются через 5 уровней вызова чтобы где нибудь влепить очередной костыль в if… else, и подобного?


                  1. Druu
                    27.06.2018 13:09

                    Я не пойму, вы всерьез считаете что если все время разработчиков тратится на исправление ошибок и ком только растет — это нормально?

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


                    Что толку от грамотного планирования и затрат на контроль за архитектурой, если в итоге ваш проект просто не доживет до того момента, когда плохая архитектура могла бы оказать какое-либо влияние?


                    Окей, выпустили MVP, оценили что нужно, самое критичное реализовали, может пора уже и что нибудь поправить?

                    Может, и пора. Но, очевидно, есть и еще какие-то задачи (они всегда есть, согласитесь). Есть какой-то актор, ответственный за определение приоритета задач. Он не выбрал рефакторинг и исправление архитектуры. Возможно ли такое, что он основывался на каких-то объективных факторах?


                    Я не говорю, что плохая аритектура, недостаток планирования и некачественный код лучше, чем хорошая архитектура, грамотное планирование и качественный код. Очевидно, не лучше. Я говорю о том, что все имеет свою цену и вполне возможно, что в данный момент — она, ну, слишком велика. Возможно же?


                    1. Neikist
                      27.06.2018 14:44

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


                      1. Druu
                        27.06.2018 14:47

                        В принципе если компания на это решилась — ну, окей.

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


                1. JobberNet
                  27.06.2018 13:15
                  +1

                  ведь вторая версия не просто так появилась? В ней, видимо, возникла какая-то необходимость. Решили реализовать какой-то функционал, который давал бы конкурентное преимущество, так?

                  Видел вторую версию отличавшуюся от первой версии лишь наличием новомодного интерфейса, а все внутренности состоящие из многократно залатанного легаси так и остались.


        1. JobberNet
          27.06.2018 11:02

          Кисло с нуля переписывать запутанное легаси в десятки тысяч строк, где заплатка сидит на заплатке и заплаткой погоняет. :(


          1. Druu
            27.06.2018 12:14

            > Кисло с нуля переписывать запутанное легаси в десятки тысяч строк, где заплатка сидит на заплатке и заплаткой погоняет. :(

            Если переписывать с нуля — запутанность и количество заплаток не играет ровным счетом никакой роли :)


            1. JobberNet
              27.06.2018 12:34

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


              1. Druu
                27.06.2018 12:36

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


      1. gandjustas
        27.06.2018 19:34

        Проблема в том, что процесс проектирования кода, для соответствия принципам SOLID и GRASP не уменьшает количество ошибок.


        1. GeekberryFinn
          27.06.2018 19:40

          Потому что такие принципы часто, увы, бездумно превращают в «культ Карго», вместо того чтобы сначала сесть и продумать архитектуру.


          1. Druu
            28.06.2018 06:48

            Тут интересно отметить следующую штуку — вот в стандартах нередко вводятся понятия вроде: "обязательно к исполнению", "очень рекомендовано к исполнению", "рекомендовано к исполнению" (с четким описанием этих понятий). И, с-но, те или иные вещи маркируются соответствующими понятиями.
            По логике-то ко всем принципам надо относиться тем же самым образом — есть какие-то вещи которые необходимо исполнять, есть которые можно нарушать в определенных случаях, есть "просто рекомендации", но нет же! Все всё воспринимают как: "шаг вправо/влево — расстрел, подпрыгивание на месте — попытка улететь".


  1. TigraSan
    26.06.2018 18:22
    +4

    Меня все чаще смущает поголовный отказ от


    public function processSomeDto(SomeDtoClass dto)
    {
         if (predicat) {
              throw new Exception(‘predicat is failed’);
         } else {
              return this.dtoProcessor.process(dto);
         }
    }

    И если этот пример я еще могу понять, так как в реальности это скорее всего было бы так:


    public function processSomeDto(SomeDtoClass dto)
    {
         if (predicat) {
              throw new Exception(‘predicat is failed’);
         } 
         if (anotherPredicat) {
              throw new Exception(‘another predicat is failed’);
         } 
         return this.dtoProcessor.process(dto);
    }

    И это хорошо, это точки выхода, которые легко вынести в что-либо вроде


    public function processSomeDto(SomeDtoClass dto)
    {
         this.validateParams()
         return this.dtoProcessor.process(dto);
    }

    Но основной посыл всегда — отказаться от else, если работать будет так же без него. А это имхо, зло.


    Ибо следующие два варианта работают одинаково, но читаются абсолютно по разному. И активно продвигается первый вариант, с чем в корне не согласен


    function abs(int num){
         if(num < 0){
              return -num
         }
         return num
    }
    
    function abs(int num){
         if(num < 0){
              return -num
         } else {
              return num
         }
    }

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


    1. deej
      28.06.2018 09:18

      Это две разные ситуации. В случае с Exception — это "отсекающие" условия. В вашем примере уместнее оставить else.


  1. slonopotamus
    26.06.2018 21:03

    Максимум один уровень вложенности на метод.

    То есть цикл с break/continue вы написать не можете?


    1. c3037 Автор
      26.06.2018 21:40
      -2

      Как вы верно отметили

      if (predicat) {
           break;
      }
      внутри цикла будет нарушением одного из принципов объектной калистеники. Но когда такая необходимость действительно возникает, я предпочитаю не впадать в крайности и не бросаться исключениями, а сознательно нарушить это правило и оставить как есть. Цель данного правила в том, что бы не усложнить код, а избавиться от многоуровневых лесенок. Для себя вы можете взять не цифру 1, а более. Я в практике стараюсь использовать 1 уровень. И каждое его превышение обосновать.


      1. slonopotamus
        26.06.2018 21:59
        +4

        Печалька с ограничением вложенности и длины методов — падает code locality в исходнике. Вместо обозримого метода, пусть на 25 вместо 15 строк получаем пяток функций, а это лишние прыганья по коду туда-сюда, мучительное выдумывание этим функциям названий. Опять же, при необходимости поменять входные аргументы на глубоком уровне приходится менять все сигнатуры по пути, просовывать новые аргументы через все уровни, в результате дифф разбухает. В общем я готов поспорить с тем что искусственные ограничения на вложенность или длину функций являются безусловным добром.


      1. sic
        27.06.2018 00:34
        +2

        Кроме того, как с подобными ограничениями можно реализовать хоть какой либо алгоритм? Если не нарушать принцип локальности, то элементарно посчитав количество вложенных for можно сразу сделать предположения о сложности алгоритма, но если размазать их на кучу вложенных методов с одним циклом, то… да что тут говорить, на реальной практике команда программистов в компании где я работал не замечала 3 года, что у них один из алгоритмов работы с данными имеет кубическую сложность, и элементарно приводится к O(nlogn), но как догадаться, если все размазно на методы по 15 LOC?

        Кроме того, неизбежно, если мы пишем многопоточный код (а мы ведь пишем?) будет уйма проблем с синхронизацией/блокировкой. Запихивать блокировку в каждый 15 LOC метод жирновато, не правда ли? И дело не в том, что рекурсивные мьютиксы как-то плохо с этим справляются, вовсе нет, но если слишком часто отпускать блокировки, то в большинстве нетривиальных ситуаций мы лишь потеряем огромное время, если у нас больше одного связанного логикой объекта блокировки.

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


  1. Druu
    27.06.2018 05:03
    +2

    Для решения этой проблемы можно выделить некую обёртку, единственным назначением которой будет создание объекта с помощью фабрики и передача его на обработку куда-либо дальше.

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


  1. Akon32
    27.06.2018 12:11

    Максимум один уровень вложенности на метод.

    Есть такое понятие "цикломатическая сложность". Упрощённо — количество ветвлений и циклов в методе + 1. Во времена изучения мною этого вашего ООП рекомендовалась цикломатическая сложность не более 4-5 на метод.
    Позднее, я сталкивался с написанием алгоритмов с цикломатической сложностью порядка 100 (специфическая обработка полилиний в ГИС). Это было реализовано примерно тридцатью методами с цикломатической сложностью в основном 4-6, иногда до 10.


    В статье же предлагается ограничить цикломатичекую сложность значением около двух ("около" — потому что вложенность и цикломатическая сложность не эквивалентны). Для достаточно сложных алгоритмов это предложение звучит бредово.


    Более того, с ограничением "1 уровень вложенности" даже линейный поиск не написать.
    for(...){if(...){return ...}} — уже два уровня вложенности.


    1. Druu
      27.06.2018 12:22
      -1

      Есть такое понятие "цикломатическая сложность".

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


      1. mayorovp
        27.06.2018 13:26

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

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


        1. Druu
          27.06.2018 14:31

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

          Это не важно. Если в ф-и есть виртуальный вызов или лямбда — цикломатическая сложность не определена.


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

          Правда? Вот вам два куска кода:


          function test(x: number, y: number) {
              if (x > y) {
                  console.log('bigger');
              } else {
                  console.log('lesser');
              }
          }

          и:


          function yobaIf<T>(pred: boolean, then: () => T, els: () => T) {
              return pred ? then() : els();
          }
          
          function test(x: number, y: number) {
              yobaIf(x > y, () => console.log('bigger'), () => console.log('lesser'));
          }

          какова цикломатическая сложность ф-и test в первом случае и какова во втором? если она в обоих случаях одинаковая — то ответьте, считаете ли вы способ расчета цикломатической сложности, при котором она в указанных ф-ях одинакова, "сколько-нибудь разумным", учитывая что после простейшей механической замены ифов на лямбды (продемонстрированной выше) цикломатическая сложность любой ф-и становится равной единице?


          1. mayorovp
            27.06.2018 14:50

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


            1. Druu
              27.06.2018 15:01

              А еще если всю функцию простейше-механически перенести в другую оставив один вызов — то цикломатическая сложность тоже станет равной единице

              Так я ничего здесь не переносил. Вся логика осталась в ф-и test, и она вообще говоря не изменилась, я только поменял синтаксис. Есть вообще языки, где if — функция (хаскель, например). По-вашему, в хаскеле любая ф-я имеет нулевую цикломатическую сложность? Какая цикломатическая сложность у программы на языке нетипизированного лямбда-исчисления? комбинаторной логики? на форте? Почему бы тогда просто не определить, что "цикломатическая сложность равна единице для любой ф-и", раз для языков с виртуальными ф-ми и лямбдами она все равно всегда единица? Вы вообще понимаете, что цикломатическая сложность рассчитывается не на программном коде а на графе потока управления, который для вышеприведенных ф-й test одинаковый?


              1. mayorovp
                27.06.2018 15:05

                Что же до вашего примера — то у измененной функции test на языке javascript формальная цикломатическая сложность — 1, общая же 3 (поскольку функций-то три), что даже больше чем у исходной или даже 5 если считать yobaIf.

                Если же рассматривать эту функцию как программу на «птичьем языке» построенном над javascript — то ее цикломатическая сложность остается 2, ведь в этом языке yobaIf — условный оператор.

                К Хаскелю понятие «цикломатическая сложность» не применимо потому что там нет алгоритмов в традиционном понимании этого слова.


                1. Druu
                  27.06.2018 15:21

                  Что же до вашего примера — то у измененной функции test на языке javascript формальная цикломатическая сложность — 1

                  Почему 1? как вы посчитали?


                  общая же 3 (поскольку функций-то три), что даже больше чем у исходной или даже 5 если считать yobaIf

                  Почему три? Это вы как посчитали?


                  на самом деле, в обоих случаях — ровно 2, т.к. у вас в графе 5 узлов, 5 ребер и 1 компонента связности


                  К Хаскелю понятие «цикломатическая сложность» не применимо потому что там нет алгоритмов в традиционном понимании этого слова.

                  В смысле, нет? Конечно же, есть, точно такие же алгоритмы, как и везде. Вы имеете ввиду, что там нету процедур и ф-и все чистые? Так это как раз вычислению цикломатичесой сложности никак не мешает. Если запретить в хаскеле лямбды и ввести if спецформой, то прекрасно там все будет считаться.
                  Неприменимо оно к хаселю именно потому, что лямбды там по факту есть. И точно так же оно неприменимо ко всем остальным языкам с лямбдами, потмоу что вы не можете по ф-и построить ее control flow. Именно по-этому при определении цикломатической сложности строго оговаривается, что исследуемая функция — структурная.


                  1. mayorovp
                    27.06.2018 15:24

                    Я бы все-таки ослабил это требование до требования императивности.


                    1. Druu
                      27.06.2018 15:40

                      Я бы все-таки ослабил это требование до требования императивности.

                      Так дело не в императивности, отсутствие императивности расчету цикломатической сложности никак не мешает, мешает наличие лямбд. Из-за лямбд не получается построить граф потока управления локально. В струутурной ф-и вы любой другой ф-й вызов заменяете на узел графа и все. В случае ф-и test вы этого сделать не можете, т.к. внутренне устройство yobaIf влияет на связи между узлами, которые являются составляющими графа потока управления самой test! Не зная реализации yobaIf вы не можете построить граф для test.


                      Ну вы попробуйте сами логически рассудить — вот вы согласны, что в чистом бестиповом лямбда-исчислении цикломатическая сложность "не считается", так? Но ведь любую ф-ю, которую вы можете написать на чистой бестиповой лямбде вы можете написать точно так же в js! получается каким-то магическим образом одна и та же ф-я, написания одним и тем же образом при помощи одних и тех же примитивов и, в принципе, даже, в принципе, в одном и том же синтаксисе (ну нам же никто не мешает в лямбда-исчислении сделать синтаксис со стрелками вместо lambda-x.t?), то есть просто один и тот же терм (!) с той же самой семантикой (!) не имеет цикломатической сложности в рамках чистой лямбды, но сразу эту сложность преобретает, когда мы говорим: "а, так это написнао на js!"!
                      Вам не кажется это странным?


                      1. mayorovp
                        27.06.2018 15:51

                        Чистые безтиповые лямбды не являются императивными независимо от того на каком языке написаны.


                        1. Druu
                          27.06.2018 15:58

                          Чистые безтиповые лямбды не являются императивными независимо от того на каком языке написаны

                          То есть цикломатическая сложность для чистых ф-й не считается, вы так предлагаете? Вы сформулируйте четко, чтобы не было непоняток.


      1. Akon32
        27.06.2018 15:40

        В ООП (из-за виртуальных вызовов) или ФП (из-за лямбд) цикломатическая сложность не определена. Точнее, если попытаться определить ее хоть сколько-нибудь разумным образом, то она будет выходить бесконечной.

        Если виртуальный метод делает что-то в соответствии с его спецификацией, то не важно, виртуальный он или нет. Виртуальность (или не виртуальность) вызываемого метода не меняет число путей, по которым может идти выполнение вызывающего кода.
        Или у вас есть пруфы, что цикломатическая сложность по определению не может использоваться с виртуальными методами?


        Для ФП, наверно, эта метрика действительно не определена из-за отсутствия графа потока управления.
        Также не вполне ясно, как считать её для всяких там цепочек map — filter — reduce или list comprehensions, которые сегодня есть в ООП-языках.


        Но речь шла о процедурном и ООП-коде, а не о хаскеле.


        1. Druu
          27.06.2018 15:45
          -1

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

          Вообще-то зависит. Если у вас вызывается какой-то метод (виртуальный) и есть десяток классов в которых он реализован, то у вас 10 путей исполнения. А если 5 реализаций — то всего 5 путей исполнения.


          Или у вас есть пруфы, что цикломатическая сложность по определению не может использоваться с виртуальными методами?

          Конечно, она может использоваться, но она не будет показывать что-либо осмысленное. Вы можете все if-ы заменить виртуальными вызовами через диспатч по таблице виртуальных ф-й и станет у вас везде единичная сложность. Какой смысл в такой метрике?


          Для ФП, наверно, эта метрика действительно не определена из-за отсутствия графа потока управления.

          Дык и виртуальный вызов и лямбда — это одного характера вещи. Лямбда-исчисление эмулируется на ООП (ну, как и наоборот).
          Так что у вас если нету лямбд но есть ооп, то вы все ранво можете писть "как на хаскеле" (ну как на лиспе скорее, что не существенно в рамках дискуссии :))


          1. mayorovp
            27.06.2018 15:52

            Если у вас вызывается какой-то метод (виртуальный) и есть десяток классов в которых он реализован, то у вас 10 путей исполнения...

            … за пределами оцениваемого кода.


            1. Druu
              27.06.2018 16:01

              … за пределами оцениваемого кода.

              Но без которых не получится достроить граф. Вы же понимаете, что какая-нибудь лямбда в шарпожавах — это семантически тот же анонимный класс, реализующий интерфейс типа IRunnable? То есть я пишу на ООП себе лямбды, потом на этих лямбдах — пишу себе yobaIf и потом ломаю этим yobaIf вам расчет сложности. Вопрос же здесь в том, до каких пор можно использовать лямбды/ооп, чтобы "все было хорошо"? Ну вот когда мы уже до yobaIf дошли, то так уже, очевидно, нельзя, весь расчет смысл теряет, а где граница?
              И тут сразу чтобы определиться — на самом-то деле я могу и не реализовывать лямбды в явном виде, а воспроизвести логику yobaIf неявно.


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


              1. mayorovp
                27.06.2018 16:02

                Да без проблем достраивается граф! Виртуальный вызов — это один оператор. Что он делает внутри — уже не важно.


                1. Druu
                  27.06.2018 16:06
                  -1

                  Да без проблем достраивается граф! Виртуальный вызов — это один оператор. Что он делает внутри — уже не важно.

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


          1. Akon32
            27.06.2018 16:07
            +1

            Если у вас вызывается какой-то метод (виртуальный) и есть десяток классов в которых он реализован, то у вас 10 путей исполнения. А если 5 реализаций — то всего 5 путей исполнения.

            Метрика рассчитывается только для написанного метода. Иначе, при использовании вашего подхода, нужно учитывать все пути во вложенных вызовах. Боюсь тогда даже представить цикломатическую сложность функции
            void pf(float n){printf("%.3f\n", n);}
            или какой-нибудь функции, содержащей recv().


            Лямбда-исчисление эмулируется на ООП (ну, как и наоборот).

            Естественно. Но я не уверен насчёт применимости метрики цикломатической сложности к коду, содержащему несколько map (т.е. циклов) в строчке. С одной стороны, код простой, с другой — в нём куча условий и циклов.


            1. Druu
              27.06.2018 16:11
              -1

              Естественно. Но я не уверен насчёт применимости метрики цикломатической сложности к коду, содержащему несколько map (т.е. циклов) в строчке.

              Ну то есть вы согласны, что если в коде используется ф-я, в которую вы передаете другую ф-ю, то сложность ломается, т.к. по факту эта ф-я может быть семантически эквивалентна ветвлению или циклу, или чему-то еще более сложному.
              Но передача ф-и аргументом в принципе ничем не отличается от передачи объекта, реализующего IRunnable.
              То есть — если у вас есть ф-я, которая принимает ф-ю или ф-я, которая принимает объект с виртуальными методами, то все ломается. Но если исключить такие передачи то у вас фактически и останется чистое структурное программирование :)


              1. Akon32
                27.06.2018 16:54

                Ну то есть вы согласны, что если в коде используется ф-я, в которую вы передаете другую ф-ю, то сложность ломается, т.к. по факту эта ф-я может быть семантически эквивалентна ветвлению или циклу, или чему-то еще более сложному.

                Вообще-то, нет. Я считаю сложность для вызывающей функции. Её код (call *%rax) никак не меняется от того, сколько имплементаций есть у переданной функции. Из вызывающей функции все вызываемые выглядят одинаковыми чёрными ящиками, независимо от их числа или реализаций. Поэтому вызов на сложность не влияет.


                Мне это кажется более логичным, чем ваш вариант. Но это больше вопрос определений.


                1. Akon32
                  27.06.2018 17:00

                  Upd.: switch(...){...} и (*f)(); могут иметь схожие графы потока управления, но вызов функции имеет одну и ту же семантику для всех её реализаций, все они выполняются одинаково для вызывающего кода. А ветки в switch — это разные варианты, их все нужно тестировать отдельно. Я вижу в этом разницу.


                1. Druu
                  28.06.2018 06:53

                  Вообще-то, нет. Я считаю сложность для вызывающей функции. Её код (call *%rax) никак не меняется от того, сколько имплементаций есть у переданной функции.

                  Дык а как вы считаете сложность вызывающей ф-и, если не можете построить ее граф без ф-и вызываемой?


                  function test(x: number, y: number) {
                      yobaIf(x > y, f, g);
                  }

                  откуда вы знаете, какие дуги соединяют узлы yobaif, f и g, если не знаете реализации yobaIf?


                  Из вызывающей функции все вызываемые выглядят одинаковыми чёрными ящиками

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


                  Upd.: switch(...){...} и (*f)(); могут иметь схожие графы потока управления, но вызов функции имеет одну и ту же семантику для всех её реализаций, все они выполняются одинаково для вызывающего кода.

                  ну можно написать так, что (*f)() будет выполняться для вызывающего кода точно так же как switch. В чем тогда разница?


                  1. mayorovp
                    28.06.2018 08:56

                    откуда вы знаете, какие дуги соединяют узлы yobaif, f и g, если не знаете реализации yobaIf?

                    Внутренних дуг между ними — нет. Это известно.


                    1. Druu
                      28.06.2018 09:17

                      > Внутренних дуг между ними — нет. Это известно.

                      Откуда? Если у меня yobaIf(pred, f, g) { f(); g(); }, то будет граф yobaIf -> f -> g, то есть как раз внутренняя дуга.


                      1. mayorovp
                        28.06.2018 09:28

                        Это внешние дуги


                        1. Druu
                          28.06.2018 10:20

                          что значит «внешние»? не бывает никаких внешних дуг у графа. Есть узлы yobaIf, f, g. Все дуги, которые соединяют эти узлы, принадлежат графу, который содержит эти узлы. У вас после узла yobaIf выполнение переходит на узел f а потом на узел g. С-но дуги графу принадлежат, иначе как управление-то переходит?


                  1. Akon32
                    28.06.2018 10:33

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


                    ну можно написать так, что (*f)() будет выполняться для вызывающего кода точно так же как switch. В чем тогда разница?

                    Примеры:


                    1:
                    obj->f();


                    1. Druu
                      28.06.2018 11:47

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

                      Тут вышло недопонимание, речь не о вызове obj->f() речь о вызове p(obj), внутри которой уже, в свою очередь как-то вызывается (или не вызывается, мы не знаем) obj->f(). Чтобы построить граф ф-и, в которой совершен вызов p(obj) нам надо знать, как, с-но, внутри р происходят вызовы методов obj.


                      1. Akon32
                        28.06.2018 12:03

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


                        1. Druu
                          28.06.2018 12:19
                          -1

                          Однако метрика "цикломатическая сложность" применяется для оценки сложности кода с точки зрения написания и понимания, а не выполнения процессором. Поэтому вызов функции не добавляет сложности вообще, независимо от его виртуальности.

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


                          Вот я взял ф-ю с большой сложностью, заменил там все ифы и циклы на виртуальные вызовы или лямбды. Код при этом изменился только синтаксически и совсем немного. Цикломатическая сложность стала 1. Что вам это число показывает в данном случае? Ф-я же проще не стала, она, может, сложнее стала, но точно не проще.


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


                          Если ф-и структурные, вы можете сказать: "ага, вот тут цм 5, а тут цм — 15. первая функция норм, а вторая — сложная, надо подумать, может ее переписать?". В случае анализа в ООП/ФП у вас вполне может оказаться, что сложность ф-и с цм 5 выше чем сложность ф-и с цм 15, а когда вы перепишите цм 15 в цм1 то в итоге может оказаться что вы сложность не уменьшили а просто замели под ковер, спрятав за лямбдами/виртуальными вызовами, то есть сделали еще хуже чем было. Я вот об этом говорю.
                          Оценка по цм работает в структурном программировании, потому что там граф потока управления нагляден. В ООП/ФП вы можете куски графа управления тасовать и перекидывать туда-сюда.
                          То есть когда кто-то в ФП/ООП считает цикломатическую сложность и что-то рефакторит, чтобы ее уменьшить — это просто карго-культ, человек не понимает, что он делает.


                          1. Akon32
                            28.06.2018 12:39
                            +1

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


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


                            1. Druu
                              28.06.2018 13:38
                              -1

                              И считается, что это уменьшает сложность кода (особенно когда таких switch было много по всему коду).

                              Но на самом-то деле не уменьшает, вот в чем штука :)


                              Уточните, пожалуйста, про невозможность подсчёта цикломатической сложности при наличии виртуальных вызовов — это вы сами придумали или это где-то описано в литературе?

                              Это прямое следствие из определения цикломатической сложности. Явно оно, конечно, нигде не написано, в силу своей очевидности, но известно любому знакомому с основами cs человеку, который когда-то цикломатическую сложность пытался смчитать :)
                              Можете сами взять определение, попробовать посчитать и наглядно убедиться, что ничего не получается :)


                              1. Neikist
                                28.06.2018 13:45
                                +1

                                Но на самом-то деле не уменьшает, вот в чем штука :)

                                Но ведь на самом деле уменьшает сложность понимания текущего метода. В текущем методе больше не нужно отслеживать разные ветки switch, мы их в другой метод скрываем.


                                1. Druu
                                  28.06.2018 14:23

                                  Но ведь на самом деле уменьшает сложность понимания текущего метода.

                                  Так ведь нет.


                                  В текущем методе больше не нужно отслеживать разные ветки switch, мы их в другой метод скрываем.

                                  С чего бы? У вас остается тот же самый свитч, просто вы ветки исполнения разбрасываете по всему коду вместо того, чтобы держать их в одном месте.
                                  Это упрощает понимание в том случае, если ветки одинаковы по семантике, то есть все ветки свитча делают одно и то же действие, параметризованное параметром свитча, но без возможности записать это в виде ф-и от параметра в силу ограниченной выразительности конкретного ЯП.
                                  Если же у вас какой-то алгоритм, который в разных ветках свитча предпринимает семантически разные действия, то вы только все усложните, т.к. вам чтобы понять алгоритм придется по куску все ветки свитча собрать.


                                  1. Neikist
                                    28.06.2018 14:34

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

                                    Эм… Почему? Из текущей процедуры он исчезает. И в текущей процедуре, не включая вызываемые процедуры, количество путей выполнения снижается.


                                    1. Druu
                                      28.06.2018 14:37

                                      Из текущей процедуры он исчезает.

                                      Куда исчезнет?


                                      1. Neikist
                                        28.06.2018 15:09

                                        В ту процедуру в которую мы switch вынесли.


                                        1. Druu
                                          28.06.2018 15:20
                                          -1

                                          А мы вынесли? Вроде, в описанном вами способе нету никакой такой процедуры, куда оказался вынесен свитч, разве не так? :)


                                          1. Neikist
                                            28.06.2018 15:32

                                            Да, тут косячнул, не обратил внимания на «виртуального», совсем другой рефакторинг.


                              1. Akon32
                                28.06.2018 14:22

                                Можете сами взять определение, попробовать посчитать и наглядно убедиться, что ничего не получается :)

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


                                Не вижу больше смысла спорить с тем, что вам "очевидно".


                                1. Druu
                                  28.06.2018 14:30

                                  Много раз считал.

                                  Значит, неправильно считали. Вот вам ф-я:


                                  function someFunction(arg: ISomeInterface) {
                                      justFunctiion();
                                      arg.someMerhod();
                                  }

                                  Если вы считали цикломатическую сложность, то должны знать, что дуга в flow графе обозначает переход на инструкцию. Начинаете вы с начала ф-и, start, в случае justFunction все понятно — вы переходите на start JustFunction, а на какую инструкцию вы переходите при вызове someMethod()?


                                  в которых метрика цикломатической сложности успешно применяется в ООП-проектах.

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


                                  1. Neikist
                                    28.06.2018 14:36

                                    А какая разница? Для понимания текущей функции нам это неважно. Мы знаем эта функция делает justFunctiion, а также делегирует someMerhod на arg, все.


                                    1. Druu
                                      28.06.2018 14:39
                                      -1

                                      А какая разница? Для понимания текущей функции нам это неважно. Мы знаем эта функция делает justFunctiion, а также делегирует someMerhod на arg, все.

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


                                      1. Neikist
                                        28.06.2018 15:14

                                        Мы говорим сейчас про цикломатическую сложность конкретного метода, с чего эта ветка и началась, а именно с того что ее нужно на метод ограничивать. А для ее расчета нам важно только сколько ветвлений собственно в этом методе, исключая вызывающие и вызываемые методы.


                                        1. Druu
                                          28.06.2018 15:21
                                          -1

                                          А для ее расчета нам важно только сколько ветвлений собственно в этом методе, исключая вызывающие и вызываемые методы.

                                          Для ее расчета нам важно, сколько путей в соответствующем cfg. А cfg у этой ф-и не существует. Нельзя посчитать пути в графе, которого не существует.


                                  1. Akon32
                                    28.06.2018 14:45
                                    +1

                                    Нарисуйте, пожалуйста, блок-схему этой функции.


                                    1. Druu
                                      28.06.2018 15:04
                                      -1

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


          1. michael_vostrikov
            28.06.2018 10:27
            +1

            Мне кажется, все проще.


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

            Сложность простой функции 1. Лямбда это функция. Если в функции определяется другая функция, ее сложность увеличивается на 1.


            1. Druu
              28.06.2018 11:49
              -1

              Сложность простой функции 1. Лямбда это функция. Если в функции определяется другая функция, ее сложность увеличивается на 1.

              Ну прекрасно, я объявил ф-и где-то в другом месте и только их использую, то есть:
              yobaIf(pred, f, g), где f и g — определеные где-то там ф-и. Или даже: yobaIf(pred, f(x), g(x)), где х — обычная переменная (не ф-я), а f и g возвращают ф-и. Какая в данном случае сложность?


              1. michael_vostrikov
                28.06.2018 13:36

                В обоих случаях либо 1 либо 3. С одной стороны, путь выполнения один — вызов yobaIf с аргументами. С другой, аргументы сами являются ссылками на выполняемый код, то есть являются возможными путями выполнения. Думаю, логично будет считать, что сложность 3, так как пути выполнения обозначаются здесь.


                1. Druu
                  28.06.2018 13:41

                  Думаю, логично будет считать, что сложность 3, так как пути выполнения обозначаются здесь.

                  А почему именно 3? откуда вы знаете что там столько путей выполнения? Может, их 1? или 23? Откуда вообще взялось число три, как вы его получили?


                  1. michael_vostrikov
                    28.06.2018 13:53

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


                    1. Druu
                      28.06.2018 14:35
                      -1

                      Один тело самой функции, один первый callback, один второй callback.

                      Так какая разница сколько колбеков? Это же не пути выполнения, это узлы в графе, и внутри вашей ф-и никаких путей между этими узлами не задано. 2 в данном случае — это просто случайное число. Можно так же было бы сказать, что их 100 или 42, ничем не хуже числа. А чтобы узнать, сколько между этими узлами путей — вам уже нужно заглянуть в реализацию вызываемой ф-и.


                      1. michael_vostrikov
                        28.06.2018 15:11

                        Это возможные пути выполнения. Как и ветки в if. Вы же не знаете в момент анализа, какая из веток выполнится.
                        Внутри нашей функции они связываются оператором вызова другой функции. 2 это число кусков кода, которые могут выполняться. Очевидно, 100 или 42 числа хуже, так как они не показывают число кусков кода.
                        Внутри анализируемой функции у них один путь — в вызываемую функцию.


                        1. Druu
                          28.06.2018 15:17

                          Это возможные пути выполнения.

                          Еще раз, это не пути выполнения — а узлы. Точки, между которыми можно пути проложить. Или не проложить. Вы понимаете разницу между путем в графе и узлом в графе?


                          Вы же не знаете в момент анализа, какая из веток выполнится.

                          Так это и не ветки. Может, никакая вообще не выполнится. Может, выполнятся обе. В том или ином порядке. Может — каждая по нескольку раз.


                          Очевидно, 100 или 42 числа хуже, так как они не показывают число кусков кода.

                          А какое отношение число кусков кода имеет к количеству путей через эти куски кода? То есть, отношение определенно есть, но какое? Вы сможете сформулировать?


                          Внутри анализируемой функции у них один путь — в вызываемую функцию.

                          Если путь один, то почему вы считаете 2? Что-то я утратил нить повествования. Вы можете четко и ясно определить ваш способ расчета? Вы пути считаете или не пути? Если пути, то какие и в чем?


                          1. michael_vostrikov
                            28.06.2018 18:13

                            Вы понимаете разницу между путем в графе и узлом в графе?

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


                            "Цикломатическая сложность части программного кода — количество линейно независимых маршрутов через программный код."
                            Функция это линейно независимый маршрут? Да. Ссылка на нее находится в программном коде функции? Да. Значит каждую можно считать за 1.
                            И самое главное что проблема с вынесением if в функцию исчезает. Раньше были куски кода в фигурных скобках, которые считались за 1, и теперь они есть.


                            Может, никакая вообще не выполнится. Может, выполнятся обе.

                            if (inputA == inputB)
                            if (inputC == inputD)


                            Ровно то же самое. Не будем их считать?


                            А какое отношение число кусков кода имеет к количеству путей через эти куски кода?

                            Они сами по себе линейно независимые куски, которые присутствуют в коде функции. Их я и считаю.


  1. Druu
    27.06.2018 14:30

    deleted


  1. McAaron
    27.06.2018 14:52

    Это какой язык? Код похож на с++, но это не c++, поскольку в с++ нет никаких «методов» (В ISO/IEC 14882 нет такого термина).


    1. mayorovp
      27.06.2018 15:10

      Это какая-то смесь Java и PHP.


    1. Akon32
      27.06.2018 15:44

      Похоже на ActionScript.


      1. Aquahawk
        27.06.2018 15:51

        нет, в ActionScript как минимум типизация не так указывается


  1. dim2r
    28.06.2018 12:32
    -1

    Я когда эту литературу читаю, то все время вхожу в когнитивный диссонанс изза неточных переводов. Например Inversion — в английском имеет больше смыслов, чем в русском.
    ссылка на словарь. Больше по смыслу подходят «перестановка; изменение порядка;»


    1. mayorovp
      29.06.2018 08:35

      Это вы про Dependency inversion principle?

      Почему вы считаете что «перестановка зависимостей» является более точным переводом чем «инверсия зависимостей»?