Разбор статьи из журнала «Код» (Яндекс Практикум)

    Пояснительная часть


    Недавно мне попалась статья в журнале "Код" под названием "Сравнение: классы против функций". Я прочитал ее и она показалось мне… странной. Журнал позиционирует себя как издание для начинающих программистов. Но даже со скидкой на уровень аудитории статья вызывает много вопросов.


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


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


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


    Для полноты контекста прочитайте оригинал, это не займет много времени.


    Что не так в статье


    Итак, цель статьи — сравнить две парадигмы: процедурный стиль и ООП. Вполне хорошая тема для начинающих. Что же пошло не так?


    Два языка


    Первая ошибка в том, что автор использует два языка: Python и JavaScript. Какой в этом смысл? Наоборот, сравнение должно протекать в рамках одного языка, чтобы разница была видна наглядно. Другой язык — это переключение контекста и отличия в синтаксисе. Это как сравнивать две программы для телефона, но одну запускать на айфоне, а другую на андроиде, упуская факт, что разница в платформах может быть разительной.


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


    Какую цель преследовал автор, когда добавил JavaScript? Зачем было это делать, если далее идет плашка:


    Мы знаем, что в JS тоже есть классы и с ними даже можно работать. Но для обучения и понимания принципов ООП классы в JS не очень подходят: в них всё сложно с private-переменными и видимостью; технически классы — это функции, да и с методом определения там не всё так просто. Поэтому мы выбрали Python как классический пример объектного подхода: строгие классы и все возможности ООП. В этой статье мы намеренно упрощаем использование JavaScript, чтобы показать, как работает чистый процедурный подход. Программисты, не ругайтесь на нас за это.

    Лично мне этот параграф кажется жалким. Если язык "не очень подходит", то не стоит брать его в обзор, потому что иначе вы путаете читателя. В сравнении должен быть только один язык, потому что вы сравниваете парадигмы, а не языки.


    Ненужные и опасные выражения


    Вторая ошибка — в коде встречается выражение, которое не имеет отношения к статье и вдобавок таит опасность. Речь о последней строке в примере ниже:


    class  User:
        user_count = 0
    
        def __init__(self, name, age, adress):
            self.name = name
            self.age = age
            self.adress = adress
            user.user_count += 1

    При создании пользователя счетчик user_count увеличивается на единицу. Совершенно не ясен смысл этого счетчика. В статье нет ни одного примера, где бы он использовался. Поиск по user_count на странице выдает два случая: объявление и присваивание. Зачем вводить лишнюю сущность?


    Второй минус в том, что грамотное объяснение этого счетчика выходит за рамки новичка. Переменная user_count относится к классу, а не к экземпляру объекта. По этой причине в методе __init__ на нее ссылаются именно через класс, а не self. Между прочим, имя класса в нижнем регистре, и поэтому код не сработает.


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


    В третьих, любой программист скажет, что счетчик в классе — плохой пример. С этим подходом код зависит от глобального состояния, и совершенно неясно, как им управлять. Предположим, новичок создал трех пользователей, и счетчик это подтверждает:


    user1 = User("ivan", 20, "addr1")
    user2 = User("huan", 30, "addr2")
    user3 = User("juan", 40, "addr3")
    
    print(User.user_count)
    # 3

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


    del user3
    print(User.user_count)
    # 3

    Придется задать метод удаления объекта с понижением счетчика:


        def __del__(self):
            User.user_count -= 1
    
    del user3
    print(User.user_count)
    # 2

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


    Что посоветовать новичку, если нужно считать пользователей? Просто добавлять их в список, а при необходимости — исключать. Длину списка легко получить функцией len. Схема очень проста:


    users = [user1, user2, user3]
    print(len(users))
    # 3
    
    users.remove(user3)
    print(len(users))
    # 2

    Неверный тип коллекции


    Когда автор переходит к процедурному стилю, он совершает ошибку: выбирает массив для хранения данных о пользователе:


    // Создаём первого покупателя
    user1 = ['Вася', 23, 'Чебоксары'];
    
    // Создаём второго покупателя
    user2 = ['Маша', 19, 'Белгород'];

    Я искренне удивлен этому решению. Пользователь — это набор полей с разной семантикой, и на эту роль лучше всего подходит словарь (или объект в JS):


    var user1 = {
        name: 'Вася',
        age: 23,
        adress: 'Чебоксары'
    };
    
    var user2 = {
        name: 'Маша',
        age: 19,
        adress: 'Белгород'
    };

    Да, слово "объект" вызывает путаницу с ООП, потому что в данном случае мы работаем с ним как со словарем. Это прямая отсылка к первому пункту — не надо брать разные языки, потому что одинаковые термины в них имеют разное значение.


    Далее рассмотрим, как автор работает с данными в процедурном стиле. Он пишет две функции для работы с ними:


    function user1_add_bonus(bonus_count) {
        user1[3] += bonus_count;
        user1[4] = Math.floor(user1[3]/10000);
        if (user1[4] > 3) {
            user1[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user1[0], ' : ', user1[4])
    }
    
    function user2_add_bonus(bonus_count) {
        user2[3] += bonus_count;
        user2[4] = Math.floor(user2[3]/10000);
        if (user2[4] > 3) {
            user2[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user2[0], ' : ', user2[4])
    }

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


    function user_add_bonus(user, bonus_count) {
        user.bouns_count += bonus_count;
        user.bonus_level = Math.floor(user.bouns_count / 10000);
        if (user.bonus_level > 3) {
            user.bonus_level = 3;
        }
        console.log('Бонусный уровень покупателя ', user.name, ' : ', user.bonus_level)
    }

    Вдобавок, код автора опять не работает. Если вызывать функцию user1_add_bonus с каким-то числом бонусов, получим NaN-ы на конце массива. Они появились, потому что автор не инициализировал поля с индексами 3 и 4:


    [ "Вася", 23, "Чебоксары", NaN, NaN ]

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


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


    Повторение кода


    Давайте обсудим второй момент, функци user1_add_bonus и user2_add_bonus. На полном серьезе автор пишет две одинаковые функции. Разница в том, что они ссылаются на глобальные переменные user1 и user2. Далее автор пишет:


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

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


    Дальше хуже: автор создает пять пользователей и копирует пять функций:


    function user1_add_bonus(bonus_count) {
        user1[3] += bonus_count;
        user1[4] = Math.floor(user1[3]/10000);
        if (user1[4] > 3) {
            user1[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user1[0], ' : ', user1[4])
    }
    
    function user2_add_bonus(bonus_count) {
        user2[3] += bonus_count;
        user2[4] = Math.floor(user2[3]/10000);
        if (user2[4] > 3) {
            user2[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user2[0], ' : ', user2[4])
    }
    
    function user3_add_bonus(bonus_count) {
        user3[3] += bonus_count;
        user3[4] = Math.floor(user3[3]/10000);
        if (user3[4] > 3) {
            user3[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user3[0], ' : ', user3[4])
    
    }
    
    function user4_add_bonus(bonus_count) {
        user4[3] += bonus_count;
        user4[4] = Math.floor(user4[3]/10000);
        if (user4[4] > 3) {
            user4[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user4[0], ' : ', user4[4])
    }
    
    function user5_add_bonus(bonus_count) {
        user5[3] += bonus_count;
        user5[4] = Math.floor(user5[3]/10000);
        if (user5[4] > 3) {
            user5[4] = 3;
        }
        console.log('Бонусный уровень покупателя ', user5[0], ' : ', user5[4])
    }
    
    // Создаём первого покупателя
    user1 = ['Вася',23,'Чебоксары',0,0];
    
    // Добавляем ему 15000 бонусов
    user1_add_bonus(15000);
    
    // Создаём второго покупателя
    user2 = ['Маша',19,'Белгород',3000,0];
    
    // Добавляем ей 5000 бонусов
    user2_add_bonus(5000);
    
    // Создаём третьего покупателя
    user3 = ['Максим',31,'Москва',0,1]
    
    // Создаём четвёртого покупателя
    user4 = ['Аня',45,'Казань',5000,2];
    
    // Создаём пятого покупателя
    user5 = ['Наташа',32,'Брянск',8000,1];
    
    // Добавляем ей 10000 бонусов
    user5_add_bonus(10000);

    После которых идет комментарий:


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

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


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


    Такую же вакханалию мы могли бы устроить и в ООП. На каждого пользователя напишем по классу:


    class User1:
        def __init__(self, name, age, adress):
            self.name = name
            self.age = age
            self.adress = adress
    
    class User2:
        def __init__(self, name, age, adress):
            self.name = name
            self.age = age
            self.adress = adress
    
    # ...
    
    user1 = User1(...)
    user2 = User2(...)
    user3 = User3(...)
    user4 = User4(...)
    user5 = User5(...)
    
    # ...
    

    Получилось в пять раз больше кода! А что, если пользователей будет сто? Только представьте издержки, чтобы все это поддерживать. Вот почему ООП вам не подходит, а процедуры — самая крутая штука.


    Ошибочный вывод


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


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


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


    Огрехи в коде


    Если запустить код из статьи, он не сработает. Где-то ошибка в имени переменной: вместо User написано user. В Питоне и Javascript регистр имеет значение.


    В коде на Питоне пляшут пробелы: где-то их четыре, а где три. В этом языке отступы нужны не для красоты, а для вложенности. Строка с тремя пробелами относится к другому логическому блоку.


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


    Комментарии читателей игнорируются


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


    Писать в переменную суперкласса — дурной тон.

    Комментарий от Emil Orekhov:


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

    Еще:


    вы написали пять функций, чтобы выполнять одно и тоже действие. Это противоречит всей концепции процедурного программирования. На самом деле туда надо и user передавать и отличие от кода на Питоне будут минимальными.

    Еще:


    в тех примерах, где класс писался с большой буквы, там есть строчка: user.user_count += 1 и по факту вылезет ошибка, так как user не существует — есть только User :)

    Заключение


    После всего сказанного у меня несколько вопросов к редакции.


    1. Приходилось ли показывать код из статьи сотруднику, который программирует по работе, а не в качестве хобби? Если да, разве он не обратил внимание на неточности?
    2. Почему редакция игнорирует комментарии? Понимаю, что всем не угодишь, особенно по части стиля и именования. Но читатели указывают на явные ошибки. Комментариям уже полгода, но в статье ничего не изменилось.
    3. И последний — мне кажется, что материал из статьи скорее вредит читателю, потому что вводит в заблуждение. Что думает об этом редакция?

    Средняя зарплата в IT

    111 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 000 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +1
      user1 = ['Вася', 23, 'Чебоксары'];

      Вот это прям печаль, новички же часто усваивают вот такими примерами из обучения.
        0
        Писать в переменную суперкласса — дурной тон.

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


        И иногда удобней чем Singleton класс и прочие способы (которых много в C++, например).
        Хотя python… ни области видимости, ни… (классы убогие).
        python явно не лучший пример для демонстрации OOП.


        Или имеется в виду засада с "User.user_count -= 1" в многопоточном приложении?
        что так писать — это делать закладку на "она себя странно ведет" при переходе на multithread модель программы?


        Хотя я согласен. общий уровень статьи "Сравнение: классы против функций" ужасен.

          0
          Константа да, но в каких случаях нужна переменная в классе, особенно тем, что только начал программировать?
            +1
            Константа да, но в каких случаях нужна переменная в классе

            Вам действительно не приходилось пользоваться глобальными переменными?
            Вообще и никогда?


            Переменная в классе python, это некий аналог static member в C++, Java.
            Если учесть отсутствие нормальной инкапсуляции (по доступу) в python, то это фактически "подсказка разработчику того, что эта глобальная переменная по задумке связана с этим классом"


            (И да. Не надо мне рассказывать про "инкапсуляцию" в python путем добавления "_" "__" к имени переменной. Это типичная "заплата" по принципу "на и отвяжись".)


            особенно тем, что только начал программировать?

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


            Существование глобальных переменных (и переменных класса), в частности, к этим базовым знаниям и относится.


            Впрочем увы. Я все больше и больше вижу:


            • Магия "Spring Boot" и прочая магия.
            • Зачастую, ужасно корявые рецепты с stackoverflow, копируемые как есть.
            • Дурацкий споры насчет императивного и декларативного программирования. Как будто базовые принципы с 70-х годов поменялись и компьютеры стали императивными на уровне железа (как аналоговые компьютеры).
            • И т.п. и т.д.

            Стоп. меня понесло…

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


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

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

          Ну оочень спорное и без апелляционное заявление. По разному бывает. От конкретного разработчик азависит.

            0
            Иван, привет!

            Я — тот самый автор статьи из Кода. Спасибо за подробный разбор и за полезную критику. Мы удалили ту статью и объяснили, почему мы это сделали: thecode.media/oop_battle

            Сейчас мы работаем над новой версией этой статьи, и, если вы не против, возьмём часть примеров из вашего разбора. Как только появится новый материал — сразу скину ссылку на него в комментариях.
              +1
              Спасибо. Конечно, берите код без вопросов.
                0
                Добрый день. Ощущение что Вы совершенно не представляете возможности функций в JavaScrit. Для ознакомления и подхода к этому очень тяжелому для мозгов снаряду github.com/MostlyAdequate/mostly-adequate-guide-ru

                Из моего опыта. Если человек новичок то однозначно классы. Если опытный, то эмоционально придет к программированию функциями.
                0
                Почему про ООП вообще рассказывали на примере языков с очень слабым и неполноценным ООП? Нужна статья на примере Java или C#.
                  0

                  К сожалению, большинство "групп для программистов" не ставят целью научить программировать грамотно и красиво, а делают упор на привлечение внимания. А причина, думаю, и так понятна

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

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

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