Як писати код так, щоб він був зрозумілий іншим розробникам

Привіт! Мене звати Тетяна і я .Net Intermediate Software Engineer в ІТ-компанії SoftServe.

Я би сказала, що тему для статті я обрала досить філософську, але і конкретну водночас. Ми всі ніби маємо приблизно однаковий набір літератури на початку і по ходу розвитку своєї кар’єри. Кожен С++ розробник точно читав частково чи повністю Страуструпа, а для шарпістів біблією є Дж.Ріхтер та його «CLR via C#». Ми всі знаємо «Чистий код» Роберта Мартіна, а ще більшість з вас точно чули про його хорошого приятеля Роя Ошероува та його «Мистецтво автономного тестування».

Кожен програміст від джуніора до техліда має раціональні, але й суб’єктивні погляди на красу, чистоту та зрозумілість свого чи чужого коду. Але чи всі ми пишемо класний код? Чи для всіх нас це мистецтво і першочергова мета роботи?

Почнемо з базового: що таке «зрозумілий код»

Означення 1. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку.

З першого погляду здається, що і причепитися нема до чого, але припустимо, що для певної нової фічі, цей гарний естетичний код треба розширити чи написати до нього юніт-тести. Чи буде це так легко і прозоро для програміста, як в той момент, коли він вперше читав код — питання.

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

Але згодом стало зрозуміло, що цей клас неможливо покрити тестами. Проблема була в тому, що кожен метод містив в собі дуплікований код (до речі, явне порушення дизайн-принципу DRY — don`t repeat yourself) для приватної змінної unitOfWork цього класу, яка залежить від статичного класу Bootstrapper. Потрібно це замокати через залежність від БД, але неможливо, якщо не відрефакторити код.

Рішення я знайшла таке: позбутись дублікації коду в кожному методі, винести цю логіку в protected virtual property, яка легко мокається і дає можливість покрити тестами весь клас. Результат — клас покрито тестами на 82% з нуля.

Так, це був взагалі не болісний рефакторинг, але попри красу коду з першого погляду, він спочатку був не досить «чистий».

Тут можна було б додати ще пару слів про те, що TDD — це круто, але з цим вам краще піти до Ошероува, бо він в тому однозначно майстер.

Єдине, що додам, хороша практика хоча б покривати юніт-тестами функціональність, яку ви заімплементили або той код, що змінили в рамках багу. Навіть якщо проєкт, на якому ви працюєте, не розроблюється за принципами TDD або не має суворої вимоги все покривати тестами, ви несете відповідальність за свій код і маєте бути впевнені у його коректності. Професіоналізм якраз і криється у таких принципових підходах до розробки. А ваші колеги в майбутньому зможуть легко розібратись у певній частині коду саме за його тестами, що також додає вам плюсик в карму.

Очевидно, що я не заспокоюсь, поки ми не виведемо гарне і чітке формулювання зрозумілого коду. Тому в означення 2 треба включити ще й базові характеристики чистого коду.

Означення 2. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний та легко розширюваний.

Самодокументований код — реальність чи нездійсненна мрія

Я часто чула тезу про те, що код, який потребує коментарів — поганий і має бути переписаним. Але коли ти трейні чи джун, то слухаєш дорослих і мачурних девів, видаляєш всі свої коментарі з коду, рефакториш і залишаєш остаточний, безсумнівно, найкращий варіант коду суб’єктивно зрозумілий для тебе та твоїх рев’юверів. А через два місяці прилітає баг, який асайнять на knowledge keeper’а цієї фічі.

Доводиться дебажити код, ніби бачиш його вперше, бо складність коду, його вкладеність або ж складність вимог даної функціональності були настільки високими, що, у підсумку, самодокументованого коду недостатньо для його розуміння.

Спасінням є додаткова документація у вигляді коментарів конкретних рядків коду чи xml-коментарі до одиниць класу. Тут особливо додати нічого, крім рекомендації не використовувати сленг чи локальні метафори, які зрозумілі тільки вам чи регіону вашого проживання. Пам’ятаємо, що ми працюємо в основному з колегами різних національностей, тому технічна і граматична коректність ваших коментарів є надважливим показником професіоналізму в тому числі.

Також, я би хотіла відзначити високу необхідність додаткової документації і слідуванню процесам. Всі вимоги до функціональностей мають бути детально прописані на Confluence чи хоча б в Jira-айтемах, а саме з такими пунктами, як Current state, Acceptance Criteria та, в ідеалі, зі сценаріями Gherkin.

На жаль, команди без бізнес-аналітиків є зараз розповсюдженою практикою, тому розробники можуть бути відповідальними за опис фічей, і тут важливо не лінуватись документувати навіть найменші user story. Але те, що я реально оцінила з досвідом — це UML-діаграми складних фічей.

Наведу приклад з мого поточного проєкту. Для клієнта зробили класну складну фічу декілька років назад, а користуватись нею почали зараз. На мене заасайнили багу від супорт-команди клієнта, яка насправді не є багою.

Майже не втрачаючи час, ми з QC визначили, що все описане — 100% не баг, тому що знайомі з цією функціональністю. Але щоб довести це, недостатньо просто почитати код і описати, як воно працює. Архітектор з проєкту пошарив на нас сторінку конфлюєнс з UML-діаграмами цієї функціональності, що стало неперевершеним залізним аргументом в тих дискусіях з супорт-командою.

Я думаю, ви тепер розумієте, чому самодокументований код — це скоріше нездійсненна мрія, ніж реальність. Тому гіпотезу про зрозумілий код ми розширимо ще більше.

Означення 3. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний, легко розширюваний, має чіткі та компактні коментарі щодо логіки чи вимог конкретної функціональності.

Або KISS, або створюй власну мову програмування

Насправді це гучний заголовок до абсолютно простої тези — не ускладнюйте життя собі й іншим там, де це не потрібно, тобто KISS — keep it simple stupid — один з найпростіших і водночас найскладніших принципів розробки. І хай там як, але без SOLID тут не обійтись. Бо слідування цим принципам поліпшує якість, простоту коду і його логіку, а також по суті зводиться до дизайн-принципу KISS.

Наведемо деякі мої улюблені тези з книги «Чистий код» дядюшки Боба, але без опису всіх-всіх принципів:

  • Кожен програмний модуль повинен мати одну й тільки одну причину для зміни — перший принцип з SOLID — Single Responsibility. Той же принцип лягає і на зв’язки між компонентами систем: до одного компонента повинні включатися класи, що змінюються за одними причинами і, одночасно, до різних компонентів повинні включатися класи, що змінюються у різний час та з різних причин — Common Closure Principle.
  • Interface Segregation Principle — розробники програмного забезпечення повинні уникати залежностей від усього, що не використовується, в іншому випадку, такі залежності можуть стати причинами несподіваних проблем. По суті синонімом цього принципу, але для компонентів є Common Reuse Principle, згідно з яким класи, які не мають тісного зв’язку, не мають існувати в одному компоненті.

Не рідко програмісти-початківці, ознайомившись з першими патернами, намагаються всунути їх скрізь, де тільки можна, щоб, так би мовити, «набити руку». Це може призвести і до порушень принципів SOLID, і до зниження зрозумілості коду. Бо навішування патернів на ту логіку, де це не виправдано, ускладнить розуміння коду для програміста будь-якого рівня, чи то джуніор буде розбиратись, чи то сініор. Іншими словами, не порушуйте принцип YAGNI — You aren’t gonna need it — і не старайтесь додати якусь круту логіку, тому що просто захотіли потренувались у її реалізації. Ми імплементуємо суворо за вимогами і тільки ту функціональність, яка дійсно потрібна в даній ситуації.

Проте, якщо тобі не подобаються всі ці принципи і загальноприйняті правила та стандарти, вихід є — створи власну мову програмування. В такому випадку ти зможеш адаптувати свою мову програмування під конкретні задачі, зробити код більш зручним і зрозумілим для тебе та твоїх колег, визначити стандарти та стиль кодування і, можливо, навіть прискорити процес розробки та вирішення інноваційних задач. Бунтівники рухають цим світом, тому або KISS в універсальних мовах програмування, або власний шлях створення нової мови.

Означення 4. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний, легко розширюваний, має чіткі та компактні коментарі щодо логіки чи вимог конкретної функціональності, відповідає принципам SOLID.

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

Auto/var — це зручно і зрозуміло

Використовуйте var в шарпі чи auto в плюсах там, де це можливо при ініціалізації змінної, тобто майже скрізь. Це буде спонукати вас називати змінні так, щоб тип самої змінної був зрозумілим з контексту назви, що значною мірою покращить читабельність вашого коду, а також процес розробки. Тобто ви зможете орієнтуватись в коді тільки за назвами змінних без їх типізації, і в ідеалі розуміти, що це, наприклад, за Entity, не заглиблюючись у його праву частину. Приклад наведено нижче.

Explanatory variables

Це незалежні змінні або змінні predictor’и, які є пояснювальними для певної логіки.

Розглянемо на прикладі. На першому скріні ми бачимо доволі складний за структурою Dictionary і не менш складний IF. Якщо в майбутньому такий код доведеться дебажити, то я щиро не заздрю деву, який полізе в це розбиратись.

Але гарна звичка використовувати explanatory змінні, як наведено на скріні нижче. Такий код легко дебажити і легко читати, бо зрозуміло, чим є кожен предикат.

Код вище я написала в рамках імплементації однієї складної фічі з реального проєкту, але адаптувала його для вас так, щоб залишилась тільки форма для прикладу без конкретики і порушень NDA.

Класи хелпери — це зло

Якщо у назві класу є слово Helper, то це сигнал до того, що вже є проблеми з розумінням, для чого вам цей клас. Скоріше за всього під час рефакторингу у вас не виникло кращої ідеї, ніж винести сумнівно підходящий функціонал з основного класу, наприклад DbProcessor, і ви зробили ще один клас під назвою DbProcessorHelper.

Таким чином, ви даєте можливість будь-якому деву перетворити DbProcessorHelper на клас-монстр чи клас-смітник з різною функціональністю, бо це ж Helper. І в майбутньому такий клас мало того, що не дасть розуміння, навіщо він створений, та ще й майже напевно виникнуть складнощі у роботі через відсутність його Single Responsibility.

Виходить, що єдиною причиною для змін є будь-яка причина, адже всю нову функціональність можна віднести до категорії Helper.

Не залишайте в коді кортежі з великою кількістю елементів в них

Кортежі представляють нам короткий синтаксис для групування кількох елементів даних у спрощеній структурі даних. Але, навіть маючи контекст, далеко не завжди зрозуміло, які дані містить в собі Tuple.

Розглянемо приклад нижче. Ми маємо Dictionary для зберігання номера в черзі як ключ та для значення — Id/Db одруженої пари. В коменті описано, що міститься в ключі та значенні, але працюючи з таким Dictionary ми будемо постійно змушені прописувати довгий Tuple, тобто дублювати код, який без пояснювального коментаря над методом не буде зрозумілим жодному розробнику.

В такій ситуації швидке та очевидне рішення — обернути такий кортеж в using із лаконічною назвою MarriedCouple, щоб і надалі використовувати його в зрозумілішій формі.

Також є варіант винести цю логіку в структуру, наприклад, замість використання Tuple. Але це вже залишається на розсуд розробника використовувати той чи інший функціонал.

Синтаксичний цукор — то є сіль зрозумілого коду

Іншими словами, як в реальному житті, так і в програмуванні — зловживання цукру є шкідливим для вашого здоров’я.

Я знаю девів, які люблять навішати новомодного синтаксичного «цукру», аби всі бачили, що вони стежать за новими фічами і завжди в тренді. Але іноді я бачу в тому проблему з точки зору читабельності коду, а також його дебагінгу.

Боротьба з ворнінгами та перевірка complexity

Встановіть extension SonarLint для Visual Studio та включіть «Treat warnings as errors» — так ви позбудетесь різних code smells, помилок компілятора і таке інше. Безумовно, не менш хороша практика — аналізувати та поліпшувати код метрики для вашого solution за всіма проєктами та їх класами, наприклад, такі метрики, як Cyclomatic Complexity, Depth of Inheritance, Class Coupling.

Підсумок та порада

Більшість розробників не аналізує необхідність моніторингу Code Quality, написання юніт-тестів та і взагалі рефакторингу, як такого. Клієнти не бачать в тому сенс, щоб переплачувати, наприклад, за час на рефакторинг, а розробники через жорсткі дедлайни і не доестімейчені сторі, не завжди можуть заімплементувати якусь нову фічу зрозуміло і гарно, не переписавши при цьому старий код.

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

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

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

Дякую за увагу і буду рада вашим коментарям з думками на цю тему.

👍ПодобаєтьсяСподобалось21
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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

itemNotFoundByOrderButExistInDB

Це жах. Звертайтесь до ChatGPT з запитом на зразок “Give me 10 shortest variants for var itemNotFoundByOrderButExistInDB”

Це дуже абстрактна назва змінної лише для прикладу (яку звісно в реальний код я би не додала), приклад лише для демонстрації форми.
От навіть цитата зі статті:

Код вище я написала в рамках імплементації однієї складної фічі з реального проєкту, але адаптувала його для вас так, щоб залишилась тільки форма для прикладу без конкретики і порушень NDA.
protected virtual property

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

Коли чую «синтаксичний цукор» — в роті одразу неприємний присмак Kotlin.

Запитання до Tetiana Orlova чи до інших інженерів із SoftServe, ... чи є у SoftServe дефолтні вимоги щодо синтаксису? яким чином погоджується конкретно на ваших проектах стиль написання коду, наприклад, якщо річ про якусь модифікацію від Google Style Guides google.github.io/styleguide ?

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

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

Мені подобаються оці підходи, які я завжди рекомендую замість усяких патернів, солідів, процентів покриття тестами, для мене це робить написання коду максимально інтуітивним й мінімізує суперечки:
1. Розділяй та володарюй замість sngle responsibility. Код писати та читати легко. Якщо тобі важко щось писати — значить треба розбивати на декілька класів або функцій. Це вирішує питання «а цей код треба виносити в сервіс чи ні, це sngle responsibility чи вже god object?»
2. Коментарі тільки пояснюють «чому» зроблено саме так, «як це працює» — пояснює код. Фішка в тому що мова йдеться не тільки про коментарі у коді, а й в реальному житті. Людина пальцем обводить мені на екрані 10 строк кода й пояснює «а ось тут цей код робить....» — значить вона тут ж бере й виносить ці 10 строк в окрему функцію або змінну. Так само й ти робиш, розібрався що роблять 10 строк коду в легасі — виніс в функцію з нормальним імʼям.
3. Записати текст таски кодом, тобто якщо в тасці написано «ордер може бути створений та відредактований», значить ми пишемо клас Order, де є фабричний метод create, та метод edit. Тільки потім переходимо до реалізації. Такий собі DDD на мінімалках.
4. Жодних тестів руками — це задача QA, усі тести тільки кодом. Вирішує проблему скільки відсотків й яких саме тестів треба написати. Якщо ти хочеш протестувати фічу вискорівнево — пишеш e2e. Якщо ти написав важкий алгоритм, тобі само по собі захочеться його протестувати в ізоляції, й ти пишеш юніт тест. Якщо функція настільки проста що ти готовий її запушити не тестуючи взагалі — то і тест не треба писати.

Ідея в тому щоб задати загальний напрямок, замість жорстких правил. Це правда скоріш відповіді про те як знайти золоту середину між різними підходами й правилами в програмуванні, які дуже часто протирічать один одному. Тому спочатку треба все ж таки вивчити ті самі підходи.

я простіше це називаю рівні в команді.

цей код може зрозуміти моя мама
цей код може зрозуміти моя бабушка

2 рівні прозорості. але, це абстракція, бо ні моя мама, ні бабушка англійською не володіють. тільки німецькофашистською ))

Синтаксичний цукор — то є сіль зрозумілого коду

Іншими словами, як в реальному житті, так і в програмуванні — зловживання цукру є шкідливим для вашого здоров’я.

Я знаю девів, які люблять навішати новомодного синтаксичного «цукру», аби всі бачили, що вони стежать за новими фічами і завжди в тренді. Але іноді я бачу в тому проблему з точки зору читабельності коду, а також його дебагінгу.

Можна приклади, коли синтаксичний цукор робить код гіршим?
У Java теж є синтаксичний цукор, але він навпаки додається в мову, щоб робити код простіше та зрозуміліше.
Наприклад, try-with-resources, pattern matcing, Java records.

Наприклад, в C# можна написати string[] subArray = mainArray[^2..^0] . Без гугління я б не зрозумів що саме відбувається.
Проблема в тому, що не всі знають новий синтаксичний цукор. Ще є ризик, що фіча не приживеться, залишиться екзотикою і буде важко читатись через 5-10 років.

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

Як компроміс можна використовувати консервативний варіант: використовувати тільки те, що вже себе зарекомендувало

типа за остальными языками подбирать фичи, которые лет 5 будут востребованы и быть вечным аутсайдером?

Та ні, я про девелоперів — використовувати тільки фічі, які були додані в мову більше 5 років назад і прижилися. Не скажу, що я прихильник такого підходу, але частину проблем зловживання цукром це прибере.

Кортежі представляють нам короткий синтаксис для групування кількох елементів даних у спрощеній структурі даних. Але, навіть маючи контекст, далеко не завжди зрозуміло, які дані містить в собі Tuple.

У Java немає tuples, а от наприклад у TypeScript є.
Але й там писати код у стилі Tuple вкрай не рекомендується, тому що для новачка потім аналіз такого коду перетворюється на детективне розслідування.
Цікаво, навіщо вони потрібні?

За таплы вобще бить палкой следует программиста.

А чому? Вони тобі зробили щось погане?

Краще скажіть що вони зробили хороше :-) Я пару раз спробував як зявились і потім майже одразу викинув оті горабті response.Item1 та response.Item2. Херня якась.

Auto/var — це зручно і зрозуміло

Іронія долі. Ті поради, що даються для однієї мови програмування, навпаки, не рекомендуються для іншої.
Наприклад, у Scala/Kotlin не рекомендуються додавати модифікатор var змінним, тому що це робить їх mutable.
Аналогічно в JavaScript замість var краще використовувати нові const/let.

В пораді про var суть в невказуванні конкретного типу, а не в відмові від const. А як саме буде константність показуватись — const, final окремими, val, let обʼєднаними, відсутністю mut — це вже локальна специфіка. Не чіпляйтесь до слів.

Продовжуємо...

> Тут можна було б додати ще пару слів про те, що TDD — це круто, але з цим вам краще піти до Ошероува, бо він в тому однозначно майстер.

TDD — ще одна інфоциганщина. TDD в чистому вигляді працює тільки на інтелектуально банальному коді, який пишеться з нуля. Якщо того нема, він просто непридатний за визначенням.

Але відомо, що >90% розробників сприймають TDD як «код має бути покритий тестами» незалежно від того, як ці тести писались — до чи після, красний/зелений чи без них. Коли був голосовий чат DOU про TDD, це було зʼясовано і сформульовано в явному вигляді.

Хто такий Ошеуров — не знаю, не гуглиться, але в моєму світі (за Спольським, знову), де напів-embedded плюс мережеві сервіси, такого нема. Чесно кажучи, і не цікаво.

> Означення 3. Зрозумілий код — це естетично гарний, структурований і не перевантажений код, який має єдине форматування, оптимальну структуру та логіку, тестабельний, легко розширюваний, має чіткі та компактні коментарі щодо логіки чи вимог конкретної функціональності.

А ось це гарно звучить. Ідеала не буде, але як ціль, до якої тягнутись — добре.

Але: якщо є «означення 2» і «означення 3» про те ж саме і не сказано явно, що будуть виправлення — половина читачів загубить це ;)

SOLID, як уже пояснив, я навмисно не приймаю тут за обовʼязковий компонент.

> Класи хелпери — це зло

Тут одна річ незрозуміла: які саме хелпери. Їх можна робити з десь десятка причин і стільки ж задач. Деякі дійсно поважні. Може, взагалі назва не краща, але без пояснень, у якому випадку краще без хелперів, я б це не писав.
Боюсь, тут головне в абьюзінгу слова «хелпер».

> Не залишайте в коді кортежі з великою кількістю елементів в них

Це якийсь дуже специфічний для вашої области рецепт.
Підказка з struct очевидна, а ось хто писав такі кортежі і чому — тут складно зрозуміти.
Я дуже рідко бачив саме таке, хоча _патерн_ «групування даних у структуру з зрозумілим іменем» це основа.

Здається, все :)

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

TDD в чистому вигляді.

В чистому ви мали б видалити весь тестуємий код, відтворити падіння, написати код знову і задовільнити тест :)
А якщо ви його не видалили, а модифікуєте — все, чистота методу безповоротньо зіпсована.
І це краще, бо відповідає реальности, а не релігійні паморочення.
А взагалі — да, я зазвичай саме так і роблю і раджу так робити.

То ви сприймаєте дуже буквально. Ніхто не змушує писати тести спочатку. Головне — покриття тестами.

Ніхто не змушує писати тести спочатку.

Що, дійсно ніхто? «А якщо знайду?» ©

en.wikipedia.org/...​/Test-driven_development

«„„​1. Add a test““»
«„„2. Run all tests. The new test should fail for expected reasons
This shows that new code is actually needed for the desired feature. It validates that the test harness is working correctly. It rules out the possibility that the new test is flawed and will always pass.““»

Українська версія:

«„„1. Не можна писати жодного вихідного коду, доки спершу не написано падаючого юніт-тесту (англ. unit test).
2. Не можна писати більше юніт-тесту ніж необхідно для падіння (непроходження тесту). Помилка компіляції — це також падіння (англ. failing).““»

Продовжувати?

Головне — покриття тестами.

В нормальній методиці — да, так.

То ж і кажу шо сприймаєте буквально, по написаному, як справжній програміст )

як справжній програміст )

1) Не програміст, а бюрократ.
2) Да, увімкнув цей режим, бо коли подібні питання обговорюються з колегами, а особливо зі всяким начальством, треба максимально близько використовувати стандартні означення термінів (якщо нема ну зовсім уж поважної причини цього не робити).

Стаття гарна майже всім. Але є деякі проблемки...

Наведемо деякі мої улюблені тези з книги «Чистий код» дядюшки Боба
Кожен програмний модуль повинен мати одну й тільки одну причину для зміни — перший принцип з SOLID — Single Responsibility.

«Дядько Боб» — «інфоциган», у якого єдина гарна риса — він вміє писати і говорити красиво, щоб заохочувати. Після цього верхнього прошарку починається повна маячня. Вже його приклади з книг самопротирічні і не відповідають його ж декларованим принципам (посилань не наводжу, це обговорювалось 100500 разів). Формулювання у нього теж це суцільні криві милиці замість нормального пояснення.

Що таке «одна і тільки одна причина для зміни»? Ось у мене одна причина — замовник щось захотів і платить. Причина — гроші. Без грошей не міняю. Все. Спростовуйте:))

І не треба казати, що він далі пояснює, що це за «одна причина» — ні, не пояснює. Точніше, його пояснення не проходить мінімальний контроль на розумову адекватність. Адекватне пояснення потрібно було починати з чогось на зразок такого:

1. Ми всі міркуємо у термінах сутностей-«обʼєктів» розумого моделювання дійсності і предметної області, тому що це єдиний спосіб впоратись зі складністю загальних задач.
Не обовʼязково ці сутности-«обʼєкти» моделі відповідатимуть обʼєктам мови програмування, але головна їх риса — _межі_ що відділяють від інших, _звʼязки_, і групування на їх основі. Саме цьому «обʼєктно-орієнтоване» програмування має сенс: його корінна сутність це виділення меж і звʼязків.

2. Нам зручно, коли ці сутности незалежні, хоча б частково, бо зменшення кількости складних звʼязків і складности кожного звʼязку допомагає розглядати тільки одну частину картини (декілька сутностей), тимчасово ігноруючи інші.

3. Виконання багатьох ролей-вимог одною сутністю підвищує її як складність її самої, так і її звʼязків. Чим простіше, тим краще.

4. Що саме вкладається в роль/вимогу до сутности — залежить від задачі. Тут гарний приклад дає Фізерс (Feathers, «Working Effectively with Legacy Code»): так, відриваємо код друку для конкретного принтера від коду формування звіта; але обмеження формату звіту таблицею залишається у структурах даних. А якщо не лінійна таблиця? Все, переробляти. Або: років 30 всі принципи планування I/O залагались на риси магнитних дисків. Прийшли SSD, піднялись швидкості — додався trim, повністю переробили шедулери, а головне — паралельні асинхронні запити, які були тільки плюсом для окремих задач, стали тим, навколо чого все будується взагалі. І так далі.

І така зміна вимог, після якої те, що здавалось цільним, починає бути складеним (або навпаки) — типова ситуація.

І тільки маючи уявлення про роль і задачу сутності/обʼєкту — можна проєцювати їх на задачу вже конкретного модулю/обʼєкту програми.
Але _роль_, а не «_причина_».

Мартин найбільш розкручений, але це просто дитячий сад. Серйозно сприймати можна хоча б Макконнела. Принципи — GRASP більш важливі, ніж SOLID.

Патернів більше ніж 25 що у GoF: відбір цих 25 дивний і однобокий. Їх сотні. Навіть звичайний if це вже патерн. Чому його так не називають?
Я жодного разу не бачив Builder у стилі як у них — зовнішній — але знаю приклади на internal builder і на attribute collection. Або: групування даних для контексту — на кожному кроці. Адаптер і мост — не використовував.

Взагалі, ваша стаття повністю показує належність до табору «внутрішнього софту» за Спольським. Це і плюс, і мінус.

Хмм обговореня програмування на dou це прям ...незвично ))

Бо «девелопер» у сучасній діловій мові це забудовник.

За термін explanation variables спасибі!
Завжди ними корисиувався, але не знав що для цього є окремий термін.

Є така річ, як метрики коду. Особливо корисна, з мого досвіду, це Maintainability index:
learn.microsoft.com/...​-and-meaning?view=vs-2022
Якщо у клієнта виникають питання чому невеличка зміна у старому проекті займає два дні — то я можу з цифрами на руках довести йому що це тому що код — гівно.
На кожному старому проекті я намагаюся довести одну банальну істину — писати новий код завжди окупається! Не треба правити існуючий код, рефакторити його чи покривати тестами — бо вам доведеться спочатку виплатити технічний борг. Не треба фіксити кожен баг окремо — бо це буде або дуже дорого, або нова милиця.
Витрачаючи гроші на старий код — ви забираєте їх у свого майбутнього! Пишучи новий, чистий код з юніт-тестами ви навпаки — наближаєте час позбавлення від старої версії.

писати новий код завжди окупається! Не треба правити існуючий код, рефакторити його чи покривати тестами — бо вам доведеться спочатку виплатити технічний борг.

Одночасно погоджусь і не погоджусь. Інколи, дійсно, найдешевша опція — викинути стару функціональність на підтримку і переписати все з нуля, використовуючи нормальні фреймворки/підходи/принципи. Але бува і так, що дешевше порефакторити та виправити баги. Залежить. До того ж, вимоги все одно постійно змінюються. Тобто, як би ідеально не була спроектована робота за самого початку, рефакторити рано чи пізно все одно доведеться. :)

Особливо корисна, з мого досвіду, це Maintainability index

Як порахувати? На прикладі якогось проекту з github.

Якщо ви працюєте з класом-монстром, не додавайте в нього нічого нового, подумайте, як можна винести вашу функціональність в інший клас чи спростити impact.

І з таким теж доводилося стикатися. Майже класичний приклад. Людині треба додати функціонал, який, на перший погляд, потребує змін в класі-монстрі, якого навіть торкатися лячно. Людина доволі логічно розмірковує, що краще написати цей функціонал десь окремо, добре його протестувати, а потім загорнути в одну обгортку із класом-монстром (не торкаючись коду власне монстра) і таким чином вирішити проблему. Потім цей код переходить до іншого розробника, в якого виникає схожа проблема, але замість одного класа-монстра, є ще додатково клас-напівмонстр, який робить те саме, але «с пєрламутравимі пуґовіцамі» та ще додатково обгортка. Торкатися всього цього зоопарку, навіть довгою палицею, не те що лячно, а навіть думки про це вже псують настрій. Таким чином, виникає ще один напівмонстр і додаткова обгортка. Але є проблема: за допомогою додаткової обгортки можна вирішити багато проблем, але точно неможливо зменшити кількість обгорток, яка сама по собі може бути проблемою. :)

Для цього приймається вольове рішення, заганяєте цього монстра в клітку, (покриваєте тестами), потім робите новий class Monstr2 і перевиховуєте його.

Якщо клас можна відносно легко покрити юніт-тестами не змінюючи його кода, це означає що не такий вже це і монстр. Справжня монструозність — це купа конче неочевидних залежностей, які приховані в самих неочікуваних місцях, і які неможливо замокати або проемулювати в інший спосіб під час тестування. Тобто те, що ви описали звичайно працює, але тільки у відносно простих випадках. Дійсно складні, якщо вирішуються, то тільки за рахунок індивідуальної майстерності та багаточасової роботи з дебагером. :)

Ну мова про те що в будь-якому випадку потрібно переробляти, інакше це шлях у прірву.

Залежить. Інколи простіше дійсно викинути та переписати з нуля. Але з тим, що намагання нескінченно підтримувати і модифікувати погано написаний код навіть не намагаючись його порефакторити — це шлях у прірву, згоден. Зазвичай, найскладніше тут — пояснити ситуацію менеджменту або замовнику, який часто не має зеленої уяви про те, що таке технічний борг.

Отличная статья. Понятный и читабельный код — ключевая вещь программной инженерии. Задача программиста — превратить исходные требования в ясную и понятную модель которую в бушующем другие программисты ВНЕ ЗАВИСИМОСТИ ОТ СВОЕГО УРОВНЯ смогут легко сопровождать и расширять. Очень многие люди стараются применять абстракции, дженерики и паттерны там где они не нужны и стремясь достичь O(1) они создают странный синтаксис который с трудом читается даже матерыми инжинерами. Я встречал проект где архитектор создал столько абстракций, что никто не хотел в результате работать в его команде, так как сложность читаемости кода просто зашкаливала. Написание читаемого кода это наверное один из самых востребованных скилов которым на сегодняшний день должен обладать хороший программист

ВНЕ ЗАВИСИМОСТИ ОТ СВОЕГО УРОВНЯ

Ось тут, на мою думку, є певна пастка. Буває так, що людина без використання Stackoverflow або ChatGPT не взмозі написати навіть FizzBuzz, і тим не менш, вважає себе розробником ПЗ. Інколи навіть сіньором. Тобто мабуть все ж таки залежність від рівня є в будь-якому випадку. Її неможливо цілком позбавитись. Хіба що зменшити. :)

Тому таких крутих спеців і валить hackerrank, до того, як вони почнуть писати код з стековерфлоу.
Вони ж потім і створюють топіки накшталт «тестове завдання — це зло»

До чого тут hackerrank чи тестові завдання?

Auto/var — це зручно і зрозуміло

Ок, Но существуют разные подходы, например некоторые считают, что var лучше использовать для классов, а для интегральных типов лучше написать тип как есть, потому что чтобы понять, какой интегральный тип скрывается под var (byte, short, ushort, int, uint, long, ulong) нужно распарсить все выражение справа от =
+ в последних версиях C# появилась возможность скипать имя конструктора в new, что для многих может оказаться предпочтительным, чем использовать var

Explanatory variables

Ок. Но у того же Фаулера, которого упомянули подробно описывается что для эстетики важно не только хорошее имя переменной, но и ее длина. Он рекомендует придерживать диапазона 7-15 символов вроде. Вы использовали более 30 символов на имя в вашем скрине и теперь везде где будет эта переменная, код по ширине будет прыгать вправо. Короткие строчки будут чередоваться с длинными и это выглядит неэстетично.

Класи хелпери — це зло

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

Не залишайте в коді кортежі з великою кількістю елементів в них

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

Встановіть extension SonarLint для Visual Studio

Ну подключенный Sonar к репозиторию, это фактически стандарт + еще не помешает spell checking в студии включить

ЗЫ: а почему у вас все методы с маленькой буквы? Джависты проект писали? Я не против маленькой буквы, просто сам фреймворк используют другую нотацию и вы другую, это убивает эстетику

Код не должен быть «красивым» — это так же, как пытаться написать «красивый» роман :))) Код должен быть лаконичным, даже аскетичным, тогда он будет легко читаем, но инкапсуляция скрывает детали, полиформизм переопределяет скрытые детали, а наследование плодит мутантов :))))

Багато є порад, як писати зрозумілий код. Але код не може бути водночас зрозумілим для всіх. Точніше, є лише один спосіб досягти максимальної зрозумілості — використовувати DSL (domain specific languages) або тотальна фреймворкізація, де код пишеться на одному, або навіть двох рівнях абстракції вище. Тоді джуни пишуть рішення використовуючи фреймворк, мідли пишуть додаткові можливості фреймворка, а сіньйори та сіньйорити — ядро. Ідилія!

З цього ж ніби-то все починалося. Мідли і джуни писали апки на VBA, а сіньори — копмоненти для цих апок на C++. Чомусь ця модель проіснувала не так вже багато часу...

Вона існує навіть зараз. Хтось пише фреймворки, хтось на фреймворках. Головне, щоб другі не лізли зайвий раз до перших.

Але є певний нюанс. В командах, які пишуть фреймворки так само є свої джуни і сіньори, як і в командах, які ними користуються. Я саме про це. :) «Сіньоріті» — це вже давно не про рівень інструментів. ;)

Я б на розробку фреймворків джунів не підпускав на постріл гармати. Фреймворк — інструмент, він мусить бути суцільним, послідовним, консистентним, з мінімумом багів. Баги фреймворка дуже дорого обходяться тим, хто його використовує.

:) Можна нескінченно обговорювати, що і як ми б робили, якби жили в ідеальному світі. Але, насправді, все відбувається так, як відбувається. Вкрай неякісні інструменти розробки існують, і ми нічого не можемо з цим зробити. :) JavaScript — це реальність, яка надається нам у відчуттях. Можна намагатися її ігнорувати, але ні до чого, крім помилок це не призведе. ;)

Я не впевнений, що нам щось взагалі треба робити з неякісними інструментами. Що тільки ігнорувати...

Я свого часу дуже вперто намагався ігнорувати JavaScript. Ні, нічого гарного з цього не вийшло. Хоча, звісно, в кожного з нас є власний досвід. І універсальних рецептів не існує. :)

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

JS — з самого початку — суцільний треш. Перше — динамічна типізація. Ні, сама по собі динамічна типізація, як можливість, це не так вже погано за умови, що існує також статична, але ж в JS її немає взагалі, як такої. Друге — дуже, скажімо так, нетипова типізація. Починаючи із вбудованих типів, де наприклад, немає відмінності між int, float та decimal, закінчуючи тим, що інкапсуляція реалізується тільки через closure. А щоб запам’ятати як правильно користуватися booleanish, треба забути все, що ти колись знав про нормальні мови програмування. А ’12′ + 3 === ’123′ бісить ще й досі, хоча вперше стикнувся років десять тому. Третє — робота з багатопотоковістю. Це окремий треш. Четверте — скоупи. Щоб зрозуміти класичний var, треба, знову ж таки, забути всі нормальні практики і завернути мозок у трубочку. Ну, в останніх версіях хоч це виправили. let & const — це вже більш менш. Трожи виправляє ситуацію TS, але і там все ще залишається багато вкрай неочевидних особливостей.

Перше — динамічна типізація. Ні, сама по собі динамічна типізація, як можливість, це не так вже погано за умови, що існує також статична, але ж в JS її немає взагалі, як такої.

Це не недолік, це перевага. Не забуваймо, це скриптова мова. Статична типізація переускладнює код в рази. Там де в JS можна написати простий if (x) { f() }, в Java або C# потрібно буде обмазатися кодом по самі помідори. Бо треба знати тип «x». Але зазвичай це втрата флексібіліті. Особливо це дається в знаки при обробці потокових даних, де всередині може бути що завгодно.

Третє — робота з багатопотоковістю

О так... тут плюсую. Можна було б й краще реалізувати.

Четверте — скоупи.

Ну, це як подивитися. Є варіанти, коли вони дійсно спрощують код. А є, коли заважають.

Статична типізація переускладнює код в рази.

Ні. Ніяких «разів».

потрібно буде обмазатися кодом по самі помідори. Бо треба знати тип «x».

Просто помітити його типами.

Але зазвичай це втрата флексібіліті. Особливо це дається в знаки при обробці потокових даних, де всередині може бути що завгодно.

Для «чого завгодно» у мовах є особливі типи, які звуться variant, any, і так далі.
Але постійно використовувати їх не можна.

Це не недолік, це перевага.

На жаль, ні.

Для «чого завгодно» у мовах є особливі типи, які звуться variant, any, і так далі.

Якщо в системі все any, то чим тоді це краще за JS, в якому це не треба писати? ;)

Але постійно використовувати їх не можна.

Чому? Не канонічно та не співпадає з внутрішніми переконаннями як треба робити правильно? А немає «правильно», воно сильно залежить від задач.

На жаль, ні.

Ну, для когось ні, для когось — так. На смак та колір всі фломастери різні.
Приведу приклад, коли статична типізація заважає. Є дві темплейт системи — Freemarker та Smarty (не запитуйте, чому саме вона, на її місці може бути що завгодно з аналогів) Через статичну типізацію заміна int на string зламає фрімаркерні темплейти, якщо там є перевірки на кшталт if (x?? && x > 0), бо не той тип на вході. В Смарті такого взагалі не буде, там буде if(x), яке працює однаково як для строк, так й для числових форматів. Не треба казати, що фрімаркер краще, бо ти точно знаєш, який тип очікується, бо це з точки зору ефективності виробництва в рази гірше Смарті, бо там на операцію заміни типа у вхідних даних треба НУЛЬ зусиль. Економічний ефект тут важко не побачити...

Ще одна річ, на статичній типізації вкрай важко писати fault tolerant системи. Бо тут додається ще одна додаткова вимога — толерувати неспівпадіння типів даних.

Якщо в системі все any, то чим тоді це краще за JS,

Тим, що «в системі все any» це не норма, а принципова криворукість розробника. «any» має бути мала частина даних, тільки там, де без нього не обійтись.

та не співпадає з внутрішніми переконаннями як треба робити правильно?

Тому, що в >99% слабка або динамічна типізація це пряма провокація помилок, які тяжко ловляться і ховаються роками.

зламає фрімаркерні темплейти, якщо там є перевірки на кшталт if (x?? && x > 0), бо не той тип на вході.

Ось сам допуск того, що «не той тип на вході», і є першопричина.

І не треба тут розповідати про різні фреймворки, бо якщо вони взагалі таке допускають, то помилка вже на етапі архітектурування.

Ще одна річ, на статичній типізації вкрай важко писати fault tolerant системи.

Навпаки, типізація допомагає перевіряти типи даних раніше, ніж невідповідність вибухне десь всередині.

толерувати неспівпадіння типів даних.

«Толерувати неспівпадіння» в нормально спроектованій системі треба одним шляхом — чітко виписати, які типи даних можуть і як їх інтерпретувати, і слідувати цьому.

А ваш підхід призводить до того, що передається, наприклад, 4.149e+15 як номер платіжної карти, а до юзера приїжджає таксі номер [object Object], і ніхто крім юзера цього не бачить.

«any» має бути мала частина даних, тільки там, де без нього не обійтись.

А ти думав колись, що навколо умовного «any» можна побудувати архітектуру, яка буде гнучкішою, простішою, та куди більш пристосованою до змін, ніж все те, що ти пишеш зараз?

Тому, що в >99% слабка або динамічна типізація це пряма провокація помилок, які тяжко ловляться і ховаються роками.

А статична типізація не звільняє від помилок. Вони просто переїжджають в інше місце. 1:1

Ось сам допуск того, що «не той тип на вході», і є першопричина.

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

Навпаки, типізація допомагає перевіряти типи даних раніше, ніж невідповідність вибухне десь всередині.

В fault-tolerant системах вибухи всередині — норма, а не виключення. Це Ok state, не Exception та не Error.

чітко виписати, які типи даних можуть і як їх інтерпретувати

І що це дасть? Які переваги? Нічого не поламається? А потім прийде бізнес та захоче бачити щось новеньке, і певний гамуз рішень піде в смітник.

А ваш підхід призводить до того, що передається, наприклад, 4.149e+15 як номер платіжної карти, а до юзера приїжджає таксі номер [object Object], і ніхто крім юзера цього не бачить.

А ваш до того, що бот-трейдер за 10 хвилин доводить компанію до банкрутства. А я впевнений, що там все було і статично типізовано, і покрито тестами, і ще й симуляції проводилися. І все марно.

А ти думав колись, що навколо умовного «any» можна побудувати архітектуру, яка буде гнучкішою, простішою, та куди більш пристосованою до змін, ніж все те, що ти пишеш зараз?

Я не просто «думав», я працював декілька років в такому режимі. Спочатку на Erlang. Але там хоча б еквіваленту мовчазного дозволу []+{} не було. Потім на kdb+ q/k, в яких основні операції перевантажені так, що залежно від того, кажучи стандартной мовою, з якого боку вектор — виконуються різні дії.

І да, висновок з цього досвіду — таке «any» має бути тільки там, де йому дозволять.

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

А статична типізація не звільняє від помилок. Вони просто переїжджають в інше місце. 1:1

От неправду кажеш же, мʼяко кажучи. Не 1:1, а 1:10...1:100 на користь статичної типізації, бо тих помилок, що «переїхали в інше місце», меньше у вказану кількість разів. А якщо сприймати повну вартість пошуку помилки — то ще більше.

Не проблема. В стилі Erlang, на вхід поступає не {’moo_v3′, 1, 2}, а {’moo_v4′, {1,3,5}, {2,4,6}}. Код пристосований до цього — деяка група версій уміє приймати обидва. Потім код для moo_v3 видаляється. І варіативність входу є, і точний контроль, що можна, чого не можна.
Ось так роблять дорослі люди, які набили собі реальних гуль на таких проблемах.

В fault-tolerant системах вибухи всередині — норма, а не виключення.

Комусь іншому це розповідай, будь ласка. Я на Erlang/OTP декілька великих проектів і 5 років відробив. Так от: норма — не «вибухи всередині» самі по собі — їх мінімізують — а те, що їх наслідки не виходять за межі ізольованих компонентів.
Але з цього не значить, що обробка конкретного запиту не буде зіпсована, і тому локальний error/exception/etc. це те, що все одно мінімізують, в рамках доступних ресурсів.

А ти поки що вперто показуєш себе ідейним бракоробом, який свої принципові недоробки пояснює принципами. І це не вперше з тобою.

А потім прийде бізнес та захоче бачити щось новеньке, і певний гамуз рішень піде в смітник.

Не піде. Читай уважно і думай.

А ваш до того, що бот-трейдер за 10 хвилин доводить компанію до банкрутства.

Це від мови не залежить. Але чіткий контроль типів дозволяє виловити помилку раніше і надійніше.

А я впевнений, що там все було і статично типізовано, і покрито тестами, і ще й симуляції проводилися. І все марно.

Бо проблема у більшості таких випадків була на адміністративному рівні, а таке вирішити технічними засобами нереально. «Дело было не в бобине».

І да, висновок з цього досвіду — таке «any» має бути тільки там, де йому дозволять.

Все ж таки Erlang трохи дивакуватий як приклад.

це шлях до пекла.

Так, якщо вимагати від мови програмування робити все те, що звик робити в іншій. Перші слова, що кажуть джавісти, які почали писати на JS, це «яка тут всрата об’єктна модель» та «як тут встановлювати типи для змінних?». Ми ж не плачемо, що не маємо прямого доступу до пам’яті у всіх мовах, що виконуються в віртуальних машинах або інтрепретуються? Просто сприймаємо це як даність.

бо тих помилок, що «переїхали в інше місце», меньше у вказану кількість разів.

Це твоя суб’єктивна оцінка. В своїх проектах я помилки правлю не частіше, ніж ти, а може й навіть рідше. Але це каже лише про те, що ми досвідчені розробники, а не про переваги статичної чи динамічної типізації. Якщо я не помиляюся, ти й на С писав?

А якщо сприймати повну вартість пошуку помилки — то ще більше.

Знову суб’єктивна оцінка. В проектах, які побудовані на реактивному підході чи на будь-якому, де під капотом сповіщення, ловити помилки важко за замовчуванням. А в проектах, які ще й fault-tolerant, це цілий квест. Але це наслідки архітектури, а не мови програмування.

Комусь іншому це розповідай, будь ласка. Я на Erlang/OTP декілька великих проектів і 5 років відробив

Ти п’ять, а я вже як 15. Ну давай без цього, це не ті аргументи, які треба опоненту казати.

Не піде. Читай уважно і думай.

Що саме я мушу прочитати уважно? Ти хочеш сказати, що ти можеш з першого разу написати такий код, який ти потім протягом наступних 5 років без змін використовуєш? Ні, я в курсі про твій рівень, знаю проекти, що ти робив, але є сумніви, що вони не потребують змін та вийшли з-під рук майстра одразу гарними. ;)

Це від мови не залежить. Але чіткий контроль типів дозволяє виловити помилку раніше і надійніше.

Помилку проґавили попри всі міри обережності. Значить фраза «статична типізація призводить до більш руйнівних наслідків» може бути правдивою. Я не кажу, що вона такою є у всіх випадках, але вона має підтвердження в реальному світі. Варто це тримати в голові.

Бо проблема у більшості таких випадків була на адміністративному рівні, а таке вирішити технічними засобами нереально.

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

Так, якщо вимагати від мови програмування робити все те, що звик робити в іншій.

Не «те що звик», а те, що має бути універсально.

Перші слова, що кажуть джавісти, які почали писати на JS, це «яка тут всрата об’єктна модель»

І вони праві, якщо згадувати те, як будь-якому обʼєкту можна змінити любий аспект реалізації заміною прототипа. Коли це робиться для знавадження — то ще годі, але коли це використовується в продуктині — за це треба бити ногами.

Ми ж не плачемо, що не маємо прямого доступу до пам’яті у всіх мовах, що виконуються в віртуальних машинах або інтрепретуються? Просто сприймаємо це як даність.

А ось і те, що ти не розумієш :) У випадку прямого доступа до памʼяти є шари _обмеження_ які затверджують цілістність бази реалізації. А у випадку нетипізованих даних, підміни прототипів і т.д., навпаки, ці шари, навпаки, знецінюють гарантії, вибиваючи підгрунтя з-під ніг.

Це твоя суб’єктивна оцінка.

Не моя і обʼєктивна.

Якщо я не помиляюся, ти й на С писав?

Писав, да. І що?

В проектах, які побудовані на реактивному підході чи на будь-якому, де під капотом сповіщення, ловити помилки важко за замовчуванням. А в проектах, які ще й fault-tolerant, це цілий квест. Але це наслідки архітектури, а не мови програмування.

Реактивний підхід не обовʼязково дорівнює fault-tolerant чи є його необхідною умовою. Він ортогональний їм та якостям мови.
І у випадку викликаного десь казна звідки колбеку мати ще й непередачувані типи даних — це, як на мене, за межами всіх норм.

Ти п’ять, а я вже як 15. Ну давай без цього, це не ті аргументи, які треба опоненту казати.

15 на Erlang? Не вірю:)
І аргумент був про те, що я взагалі знаю проблеми, а не про довше чи щось таке. Ось не треба тут приписувати.

Все ж таки Erlang трохи дивакуватий як приклад.

Чому? Це майже найкращий приклад, як зробити fault-tolerant рішення, які нормально працюють навіть при динамічній типізації.

Помилку проґавили попри всі міри обережності. Значить фраза «статична типізація призводить до більш руйнівних наслідків» може бути правдивою. Я не кажу, що вона такою є у всіх випадках, але вона має підтвердження в реальному світі. Варто це тримати в голові.

Це типове «а у вас негров линчуют» і не більше того.
Що треба тримати в голові початкові цілі і не писати дурниць — 100%. І що якщо це порушити буде погано — теж.
А ось коли вже на верхньому рівні нормальне мислення — знаходиться, що чим сильніша (в першу чергу) і статичніша типізація, тим більше проблем і помилок ловляться раніше, ніж вибухнуть.
І рівень типу «int, а не string» це тільки перший рівень, бо далі має йти «якого біса ти присвоюєш тут 100, коли дозволені значення від 0 до 30», «я можу вкласти це значення в int8_t, гарантую» чи «ви маєте розкодувати цю строку з urlsafe+ encoding перш ніж подавати її до бази». Чи навіть «точности float недостатньо для цього алгоритму, переробіть на double».
Але щоб такі висоти підкорити — треба хоча б базис зробити.

Що саме я мушу прочитати уважно?

Що якщо і піде в смітник, то не тому, що була статична типізація.

І вони праві, якщо згадувати те, як будь-якому обʼєкту можна змінити любий аспект реалізації заміною прототипа.

Але це не є доказом всратості. Бо всратість — суб’єктивна оцінка. Це як казати на машини з автоматичною коробкою, що реалізація всрата, бо я не можу самостійно контролювати зчеплення.

Реактивний підхід не обовʼязково дорівнює fault-tolerant чи є його необхідною умовою. Він ортогональний їм та якостям мови.

Абсолютно не дорівнює. Але ці два патерни можуть працювати разом та ще й ефективно.

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

Колбеків може й не бути, реактивність не дорівнює колбекам. Її можна реалізовувати й без них.

15 на Erlang? Не вірю:)

Свят свят... Я писав про підходи в цілому.

Чому? Це майже найкращий приклад, як зробити fault-tolerant рішення, які нормально працюють навіть при динамічній типізації.

Тому що він доволі спеціалізований, як на мене. На смак та колір всі фломастери різні.

Це типове «а у вас негров линчуют» і не більше того.

:) Ні, це про вірогідність виникнення помилок. Вона збільшується зі зростанням розміру проекта незалежно від типу мови програмування, типізації, наявності чи відсутності тестів, код рев’ю та іншого.

Ні, це про вірогідність виникнення помилок. Вона збільшується зі зростанням розміру проекта незалежно від типу мови програмування, типізації, наявності чи відсутності тестів, код рев’ю та іншого.

Ось це і є головна неправда і маячня, що ви тут стверджуєте і від чого залежить вся ваша позиція.

Ні, вірогідність, щільність помилко дуже залежні від всіх цих факторів.

І поки ви цього не зрозумієте — говорити толком нема про що.
Так що я тут зупиняюсь.

Ось це і є головна неправда і маячня, що ви тут стверджуєте і від чого залежить вся ваша позиція.

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

Ні, вірогідність, щільність помилко дуже залежні від всіх цих факторів.

Це дуже легко перевіряється. Скільки тікетів на багфікси були створені за останній рік у вашому проекті в вашому коді? В мене нуль. При цьому проект продовжує розвиватися. Останній фікс був, якщо не помиляюся, роки три тому, некритичний. Сам код не зазнавав суттєвих змін 8 років.

І поки ви цього не зрозумієте — говорити толком нема про що.

Є про що ;)

Перше — динамічна типізація.

Треш спочатку не в тому, що динамічна, а в тому, що слабка і з дебільним вибором поведінки у майже всіх випадках, коли постає проблема.
Неконтрольована динамічність додає цьому особливого смаку нечищеного смаженого субпродукту, мʼяко кажучи.

Спочатку все ж таки саме в неконтрольованій динамічності. Те, що сама система типів дуркувата та ще на додаток є купа вкрай неочевидних перетворень типів за замовчанням, додає звісно неповторного шарму. Але, насамперед, відсутність статичної типізації робить неможливим написання maintainable code. TS — теж, за великим рахунком погана мова, але вона принаймні дозволяє більш-менш чітко контролювати середні та великі об’єми коду. А TS має всі недоліки JS, крім відсутності статичної типізації.

Але, насамперед, відсутність статичної типізації робить неможливим написання maintainable code

Я привів вище приклад, який показує, що в певних умовах code maintainablility у динамічної типізації вище.

What?! Використання booleanish призводить до кращої mainatinability коду ніж використання строгої типізації?! Вочевидь в нас трошки різне розуміння maintainability. Але, оскільки ми не на курсах програмування, мені здається на цьому варто дискусію завершити. Принаймні головна розбіжність тепер зрозуміла.
Можу тільки дати натяк. Код, який не контролюється компілятором, може містити ДУЖЕ СУТТЄВІ помилки, які НЕМОЖЛИВО знайти інакше, як протестивши всі можливі кейси, в які він може потрапити.
Порівняйте, наприклад
if ('0') {   // do something }
та
if (+'0') {   // do something }
Тобто без стовідсоткового покриттся всього коду юніт-тестами, навіть найпростіші зміни можуть привести до критичних багів, або критичні баги можуть виплисти в процесі експлуатації коду. В компільованому коді тривіальні кейси, зазвичай, можна правити доволі сміливо. Звісно за умов, що вони не містять «безтипових» змінних. Де краще maintainability? Але ж, знову таки, все залежить від розуміння, що саме є maintainability.

Можу тільки дати натяк. Код, який не контролюється компілятором, може містити ДУЖЕ СУТТЄВІ помилки, які НЕМОЖЛИВО знайти інакше, як протестивши всі можливі кейси, в які він може потрапити.

Навіть код, який КОНТРОЛЮЄТЬСЯ компілятором, все одне може МІСТИТИ помилки. Тому саме така невизначеність призводить до вимоги покривати все тестами.

Всі кейси не треба перебирати. В будь-якій нетипізованій мові програмування є чіткі правила приведення типів. Вони не складні, їх можна запам’ятати доволі швидко. На них так само можна спиратися при написанні коду, як на статичну типізацію. Ваш код мусить чітко визначати, що робити в певних умовах, наприклад, якщо на вхід прийшла функція, виконуємо, якщо об’єкт — ігноруємо, скаляри обробляємо. Цей метод, хоч й перевантажений, буде працювати без змін роками. Тому що він вміє «самостійно» приймати рішення, як змінювати поведінку в залежності від вхідних даних. Розширювати його функціональність теж легко, бо якщо вам треба окремо обробляти, наприклад, масиви, то ви лише додаєте умовно ще один if.

В компільованому коді тривіальні кейси, зазвичай, можна правити доволі сміливо

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

  • Визначитися з вхідними параметрами
  • Вихід, якщо дані не можуть бути опрацьовані
  • Визначитися з алгоритмом обробки даних
  • Обробити дані за потрібним алгоритмом
  • Повернути результат
Як бачимо, тут купа місць, які можна спокійно розширювати остраху щось поламати. Якщо вам треба інший алгоритм обробки, ви його визначаєте окремо, старі при цьому продовжують працювати. Якщо ви хочете змінити імплементацію алгоритму, ви міняєте тільки його.
Навіть код, який КОНТРОЛЮЄТЬСЯ компілятором, все одне може МІСТИТИ помилки. Тому саме така невизначеність призводить до вимоги покривати все тестами.

Нормально працююча типізація скорочує кількість помилок, які треба виявляти тестами, на порядок чи два.

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

Саме тому від цих правил для JS рже весь світ (включаючи тих хто на ньому пише кожного дня).

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

Ага, і так на кожний аргумент кожної функції :))

Навіть ті параноїдальні правила для JS, що я бачив, говорили тільки щось на зразок "вимагаєш на вході рядок? пиши s=""+s на початку функції", а не той маразм, що ви пропагуєте:)

Нормально працююча типізація скорочує кількість помилок, які треба виявляти тестами, на порядок чи два.

Нормальна архітектура проекту робить те саме в любій мові програмування, незалежно від того, статична там типізація, чи динамічна. Якщо система ще й при цьому fail-safe та fault-tolerant, то ви ніколи можете не дізнатися, що ваша система містить незначні помилки, які не впливають на загальне функціонування системи.

Саме тому від цих правил для JS рже весь світ (включаючи тих хто на ньому пише кожного дня).

Сміятися можна з чого завгодно, в будь-якій мові програмування є схожі кейси. Але це не відміняє того, що ці правила прості та їх можна вивчити за пів дня.

Ага, і так на кожний аргумент кожної функції :))

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

Навіть ті параноїдальні правила для JS, що я бачив, говорили тільки щось на зразок "вимагаєш на вході рядок? пиши s=""+s на початку функції", а не той маразм, що ви пропагуєте:)

Так роблять тільки адепти статичної типізації, яких випадково занесло до JS. Вимагати щось на вході — моветон для JS.

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

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

Якщо система ще й при цьому fail-safe та fault-tolerant, то ви ніколи можете не дізнатися, що ваша система містить незначні помилки, які не впливають на загальне функціонування системи.

Якщо ти в fault-tolerant системі не дізнався, що були помилки, то тебе треба гнати поганою метлою з проектувачів. Нормальний архітектор такої системи робить хоча б лічильник таких падінь у компонентах, краще — лог хоча би з 1 рядком на падіння, ще краще — вмиковний подробний лог останніх падінь, який можна періодично збирати і аналізувати... варіанти різні, але щоб зовсім не знати — см. вище.

А ти вкотре показуєш, що ти бракороб, який ніколи не розробляв і не експлуатував такі системи так, щоб вони пройшли хоча б дві версії і відгуки від користувачів.

Але це не відміняє того, що ці правила прості та їх можна вивчити за пів дня.

Вивчити — просто. Додержуватись на практиці — ні. Саме для цього і придумали 100500 засобів від strict mode до повноцінного TS (і далі).

Вимагати щось на вході — моветон для JS.

:))
мені вже зрозуміло.

Бо саме те, які протоколи взаємодії і які дані між компонентами, і визначає архітектуру проекту в плані структурування.

Протоколи взаємодії можна описувати на різних рівнях. Можна навіть упоротися та вважати виклик функції та передачу параметрів протоколом взаємодії. Але мова не про це, а про звички та патерни. Якщо людина звикла думати в імперативному стилі та використовувати класичні патерни ООП, які були розроблені років 40 тому (не пам’ятаю точну цифру), то для такої людини протоколом буде вважатися одне, скоріше за все щось, що базується на певній структурі класу. Але, якщо будувати реактори з недетермінованими execution flow, які взмозі обробляти будь-який треш на вході, то структура даних буде приведена до максимально абстрактної.

Якщо ти в fault-tolerant системі не дізнався, що були помилки, то тебе треба гнати поганою метлою з проектувачів. Нормальний архітектор такої системи робить хоча б лічильник таких падінь у компонентах, краще — лог хоча би з 1 рядком на падіння, ще краще — вмиковний подробний лог останніх падінь, який можна періодично збирати і аналізувати... варіанти різні, але щоб зовсім не знати — см. вище.

Ти приводиш задачу до того, що тобі звично та зрозуміло. Це не є поганим, ти просто так звик думати. Але є інші підходи, де твої звичні практики не працюють або не є важливими. Якщо компонент завжди повертає OK state, то вже зрозуміло, що він не падає або падає дуже рідко, але для системи в цілому це пофігу.

Наприклад, в тебе є ланцюг виконання задачі A-B-C. Кожен з компонентів повертає результат, або не повертає його, якщо щось пішло не так. На виході ланцюга ABC буде завжди два стани, або є результат, або немає результату. Як в квантовій фізиці, результат не детермінований, як кіт Шрьодінгера. Поки не виконаєш, не знаєш, що на виході. Тут в тебе одразу почнуть виникати думки, якщо я не можу гарантувати результат, то навіщо мені така система? Вона виглядає поламаною з твоєї точки зору, але це не так. Щоб підвищити гарантованість результату, можна використовувати купу практик. Наприклад, якщо ми на виході компонента B ми не бачимо результату, ми робимо реплей/ретрай. Наш ланцюг стає A-B-R-C. Якщо в нас треба зберігати всі дані, які не були оброблені, в окремому місці, то ми додаємо в ланцюг ще компонентів. Ти живеш в світі, де все мусить бути чітко визначено та детерміновано, а я в тому, де в тебе немає нічого визначеного та детермінованого, та стан речей змінюється постійно. Тому твої практики в моєму світові будуть працювати зі скрипом, а мої в твоєму легко. ;)

Вивчити — просто. Додержуватись на практиці — ні.

Тоді є два виходи. Або йти в статичну типізацію, або йти в практики, які будуть мінімізувати потенційні проблеми, які пов’язані з недостатнім рівнем розуміння мови програмування. Я обрав другий шлях та навчився робити системи, які це толерують.

Ти живеш в світі, де все мусить бути чітко визначено та детерміновано

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

Статична типізація чи не може розглядатися як чітке визначення типів даних? А обов’язкове сповіщення про помилки?

А TS має всі недоліки JS, крім відсутності статичної типізації.

Ну так TS і націлюється як прошарок над JS. Було б надто дивно вимагати від нього чогось іншого...

Я власне до того, що сама можливість статичної типізації з точки зору maintainability виглядає важливішою, ніж логічна, гарно продумана система базових типів. Хоча, остання безумовно теж є важливою.

власне до того, що сама можливість статичної типізації з точки зору maintainability виглядає важливішою, ніж логічна, гарно продумана система базових типів.

Тут не знаю що твердо сказати. Є приклади динамічної сильної типізації (Python, Erlang), є статичної слабкої (асемблери, C, C++ базовий), мабуть, починає залежати від цілі. Практично, мені обидва давали проблеми, але протилежного характеру :))
А ось де і слабка, і динамічна — туди зовсім не хочеться:))

Краще бути заможним та здоровим, ніж бідним і хворим. :)

Та навіть мідл це вже трохи ризиковано, а джуну не місце в команді фреймворків. Крім того тоді дуже великі витрати на оверхед, що простіше мати просто сіньорів чим створювати всі ці ревью й інше.

Життя — це взагалі вкрай ризикована дія. Але ж альтернатива — все одно набагато гірша. :)

Підписатись на коментарі