Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SnowOne 2025] Александр Чесноков: "Мудрость из...

[SnowOne 2025] Александр Чесноков: "Мудрость из прошлого: как реанимировать забытые проекты"

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

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

Видео: https://youtu.be/CibBBV3CRfo

Avatar for jugnsk

jugnsk

May 07, 2025
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. 5 5

  2. 7 7 Звездочки как НЕудачная метрика оценки проекта 4.5 Million

    (Suspected) Fake Stars in GitHub: A Growing Spiral of Popularity Contests, Scams, and Malware Hao He, Haoqin Yang, Philipp Burckhardt, Alexandros Kapravelos, Bogdan Vasilescu, Christian Kästner, 2024. arXiv:2412.13459
  3. 8 8 Звездочки как НЕудачная метрика оценки проекта 6 млрд

    Обработанных событий 20 ТБ Данных 4.5 Million (Suspected) Fake Stars in GitHub: A Growing Spiral of Popularity Contests, Scams, and Malware Hao He, Haoqin Yang, Philipp Burckhardt, Alexandros Kapravelos, Bogdan Vasilescu, Christian Kästner, 2024. arXiv:2412.13459
  4. 9 9 Звездочки как НЕудачная метрика оценки проекта 22915 Сомнительных

    репозиториев 6 млрд Обработанных событий 20 ТБ Данных 15835 Сомнительных аккаунтов 4.5 Million (Suspected) Fake Stars in GitHub: A Growing Spiral of Popularity Contests, Scams, and Malware Hao He, Haoqin Yang, Philipp Burckhardt, Alexandros Kapravelos, Bogdan Vasilescu, Christian Kästner, 2024. arXiv:2412.13459
  5. 10 10 Звездочки как НЕудачная метрика оценки проекта 3.1млн Фейковых

    звезд 22915 Сомнительных репозиториев 6 млрд Обработанных событий 20 ТБ Данных 15835 Сомнительных аккаунтов 4.5 Million (Suspected) Fake Stars in GitHub: A Growing Spiral of Popularity Contests, Scams, and Malware Hao He, Haoqin Yang, Philipp Burckhardt, Alexandros Kapravelos, Bogdan Vasilescu, Christian Kästner, 2024. arXiv:2412.13459
  6. 11 11 Время жизни проекта 3113 Java проектов 2016 Год

    сбора статистики 2009 Год создания репозитория
  7. 12 12 Статистика по проектам на GitHub Aseel Aldabjan, Robert

    Haines, Caroline Jay "How Should We Measure the Relationship Between Code Quality and Software Sustainability? " // CEUR Workshop Proceedings, Vol. 1686, 2016.
  8. 13 13 Статистика по проектам на GitHub Aseel Aldabjan, Robert

    Haines, Caroline Jay "How Should We Measure the Relationship Between Code Quality and Software Sustainability? " // CEUR Workshop Proceedings, Vol. 1686, 2016.
  9. 18 18 Однодневные проекты Жизненные причины завершения проектов Мейнтейнера уволили

    Проект завершил свою миссию Бесполезность проекта У мейнтейнера родились дети
  10. 19 19 Однодневные проекты Жизненные причины завершения проектов Мейнтейнера уволили

    Проект завершил свою миссию Бесполезность проекта У мейнтейнера родились дети Мейнтейнер скончался
  11. 23 23 Количество строк кода Технические причины завершения проектов Сильная

    зависимость объектов между собой High Cohesion & Low Coupling
  12. 24 24 Количество строк кода Технические причины завершения проектов Сильная

    зависимость объектов между собой Мало документации
  13. 25 25 Количество строк кода Технические причины завершения проектов Сильная

    зависимость объектов между собой Мало документации
  14. 26 26 Количество строк кода Технические причины завершения проектов Сложная

    сборка проектов Сильная зависимость объектов между собой Мало документации
  15. 27 27 Количество строк кода Технические причины завершения проектов Сложная

    сборка проектов Сильная зависимость объектов между собой Мало документации Мало тестов
  16. 28 28 Количество строк кода Технические причины завершения проектов Сложная

    сборка проектов Сильная зависимость объектов между собой Мало документации Мало тестов Legacy code is simply code without tests Michael Feathers
  17. 29 29 Количество строк кода Технические причины завершения проектов Сложная

    сборка проектов Сильная зависимость объектов между собой Мало документации Мало тестов Устаревшие технологии
  18. 33 33 jSymbolic by Cory McKay https://jmir.sourceforge.net/jSymbolic.html • Pitch Statistics

    • Melodies and Horizontal Intervals • Chords and Vertical Intervals • Rhythm Instrumentation • Texture • Dynamics
  19. 34 34 jMIR by Cory McKay • ACE 2 •

    ACE XML • jAudio 2 • jLyrics • jMei2Midi • jMIRUtilities • jMusicMetaManager • jProductionCritic • jSongMiner • jSymbolic 2 • jWebMiner 2
  20. 39 39 Electric Guitar Prevalence Доля всех нажатий клавиш, выполненных

    на электрогитарных инструментах Number of Pitches Число уникальных звуковых частот Easy Извлекаемые характеристики Medium
  21. 40 40 Electric Guitar Prevalence Доля всех нажатий клавиш, выполненных

    на электрогитарных инструментах Number of Pitches Число уникальных звуковых частот Prevalence Ratio of Two Most Common Vertical Intervals Сравнение частоты второго по популярности вертикального интервала с частотой самого популярного интервала Easy Hard Извлекаемые характеристики Medium
  22. 43 43 Метрика jSymbolic music21 musif Avg CPU Time (s)

    2.20 55.3 66.30 Avg Real Time (s) 1.98 4.72 5.62 Avg RAM (GB) 7.97 7.12 9.10 Max RAM (GB) 16.1 9.87 14.2 Сравнение с аналогами Optimizing Feature Extraction for Symbolic Music Federico Simonetta, Ana Llorens, Martín Serrano, Eduardo García-Portugués, Álvaro Torrente (2023)
  23. 44 44 Optimizing Feature Extraction for Symbolic Music Federico Simonetta,

    Ana Llorens, Martín Serrano, Eduardo García-Portugués, Álvaro Torrente (2023) Метрика jSymbolic music21 musif Avg CPU Time (s) 2.20 55.3 66.30 Avg Real Time (s) 1.98 4.72 5.62 Avg RAM (GB) 7.97 7.12 9.10 Max RAM (GB) 16.1 9.87 14.2 Сравнение с аналогами x25 Speed Up
  24. 45 45 Optimizing Feature Extraction for Symbolic Music Federico Simonetta,

    Ana Llorens, Martín Serrano, Eduardo García-Portugués, Álvaro Torrente (2023) Метрика jSymbolic music21 musif Avg CPU Time (s) 2.20 55.3 66.30 Avg Real Time (s) 1.98 4.72 5.62 Avg RAM (GB) 7.97 7.12 9.10 Max RAM (GB) 16.1 9.87 14.2 Сравнение с аналогами x2 RAM
  25. 48 48 Отзывы пользователей 2012 год • Java 7 •

    Ant 2025 год • Java 23 • Gradle • CI/CD (GitHub Actions)
  26. 50 50 if (((MetaMessage) message).getType() == 0x58) { ((LinkedList) overall_metadata[1]).add(new

    Integer((int) (data[0] & 0xFF))); ((LinkedList) overall_metadata[2]).add( new Integer((int) (1 << (data[1] & 0xFF)))); }
  27. 51 51 if (((MetaMessage) message).getType() == 0x58) { ((LinkedList) overall_metadata[1]).add(new

    Integer((int) (data[0] & 0xFF))); ((LinkedList) overall_metadata[2]).add( new Integer((int) (1 << (data[1] & 0xFF)))); } IT’S MAGIC
  28. 52 52 if (((MetaMessage) message).getType() == 0x58) { ((LinkedList) overall_metadata[1]).add(new

    Integer((int) (data[0] & 0xFF))); ((LinkedList) overall_metadata[2]).add( new Integer((int) (1 << (data[1] & 0xFF)))); }
  29. 53 53 public static List<String> extractAndSaveDefaultFeatures ( List<File> paths_of_files_or_folders_to_parse ,

    String feature_values_save_path , String feature_definitions_save_path , boolean save_features_for_each_window , boolean save_overall_recording_features , double window_size, double window_overlap, boolean save_arff_file, boolean save_csv_file, PrintStream status_print_stream , PrintStream error_print_stream , boolean gui_processing )
  30. 54 54 public static List<String> extractAndSaveDefaultFeatures ( List<File> paths_of_files_or_folders_to_parse ,

    String feature_values_save_path , String feature_definitions_save_path , boolean save_features_for_each_window , boolean save_overall_recording_features , double window_size, double window_overlap, boolean save_arff_file, boolean save_csv_file, PrintStream status_print_stream , PrintStream error_print_stream , boolean gui_processing ) 12 аргументов
  31. 55 55 public static List<String> extractAndSaveDefaultFeatures ( List<File> paths_of_files_or_folders_to_parse ,

    String feature_values_save_path , String feature_definitions_save_path , boolean save_features_for_each_window , boolean save_overall_recording_features , double window_size, double window_overlap, boolean save_arff_file, boolean save_csv_file, PrintStream status_print_stream , PrintStream error_print_stream , boolean gui_processing ) 12 аргументов
  32. 60 60 Сборка проекта 1) git pull 2) ant error:

    package mckay.utilities.staticlibraries does not exist error: package org.ddmal.jmei2midi does not exist error: package ace.datatypes does not exist …
  33. 61 61 Сборка проекта 1) git pull 2) ant error:

    package mckay.utilities.staticlibraries does not exist error: package org.ddmal.jmei2midi does not exist error: package ace.datatypes does not exist … 1) git pull jmei2midi
  34. 62 62 Сборка проекта 1) git pull 2) ant error:

    package mckay.utilities.staticlibraries does not exist error: package org.ddmal.jmei2midi does not exist error: package ace.datatypes does not exist … 1) git pull jmei2midi 2) Внешнее API отстреливает ногу
  35. 63 63 Сборка проекта 1) git pull 2) ant error:

    package mckay.utilities.staticlibraries does not exist error: package org.ddmal.jmei2midi does not exist error: package ace.datatypes does not exist … 1) git pull jmei2midi 2) Внешнее API отстреливает ногу 3) Качаем зависимость ace.datatypes 4) Внешнее API отстреливает вторую ногу 5) ….
  36. 69 WORKING WITH LEGACY Больше тестов == больше уверенности что

    ничего не сломали Test Driven Development Не надо рефакторить все подряд. Нужно иметь план и придерживаться его Локальный рефакторинг Working Effectively with Legacy Code by Michael Feathers,September 2004 Publisher(s): Pearson
  37. 70

  38. 73 Code duplication String code = "RT-13"; String name =

    "Average Note Duration" ; String description = "Average duration of notes (in seconds)." ; String code = "..."; String name = "..."; String description = "...";
  39. 80 80 //MethodCall[ starts-with(@MethodName, 'print') ] /FieldAccess[ @Name = ('err',

    'out') ] /TypeExpression[ pmd-java:typeIsExactly('java.lang.System') ] Создание правила
  40. 81 81 <rule name="AvoidSystemPrintMethods" language="java" message="Avoid using System.out or System.err

    for printing; use a logger instead." class="net.sourceforge.pmd.lang.java.rule.XPathRule"> <properties> <property name="xpath"> <value> //MethodCall[ starts-with(@MethodName, 'print') ] /FieldAccess[ @Name = ('err', 'out') ] /TypeExpression[ pmd-java:typeIsExactly('java.lang.System') ] </value> </property> </properties> </rule>
  41. 82 82 «Когда мера становится целью, она перестает быть хорошей

    мерой» Чарльз Гудхарт, главный советник по денежно-кредитной политике Банка Англии Метрики Закон Гудхарта
  42. 85 85 Профи • Архитектура приложения • SOLID • Шаблоны

    проектирования Любитель • Нейминг • Извлечение методов • Извлечение классов Easy Рефакторинг Gang-of-Four enjoyer
  43. 86 86 Профи • Архитектура приложения • SOLID • Шаблоны

    проектирования Любитель • Нейминг • Извлечение методов • Извлечение классов Гуру Easy <?> Рефакторинг Gang-of-Four enjoyer
  44. 87 87 Профи • Архитектура приложения • SOLID • Шаблоны

    проектирования Любитель • Нейминг • Извлечение методов • Извлечение классов Гуру Easy Познал дзен Рефакторинг Gang-of-Four enjoyer
  45. 88 88 Профи • Архитектура приложения • SOLID • Шаблоны

    проектирования Любитель • Нейминг • Извлечение методов • Извлечение классов Гуру • Видит общие паттерны существующего кода • Использует сторонние инструменты Easy Познал дзен Рефакторинг Gang-of-Four enjoyer
  46. 89 Правим документацию /** * Extract this feature from the

    given sequence of MIDI data and its associated information. * @throws Exception Throws an informative exception if the feature cannot be calculated. */ @Override double[] extractFeature(...) throws Exception /** {@inheritDoc} */ @Override double[] extractFeature(...) throws Exception 256 Classes
  47. 97 97 Правим документацию CompilationUnit unit = StaticJavaParser.parse(inputStream); ClassOrInterfaceDeclaration classDeclaration

    = unit.getClassByName(className); classDeclaration.getMethods().stream() .filter(methodDeclaration -> methodDeclaration.isAnnotationPresent(Override.class)) .forEach(methodDeclaration -> methodDeclaration.setJavadocComment("{@inheritDoc}"));
  48. 98 98 Правим документацию CompilationUnit unit = StaticJavaParser.parse(inputStream); ClassOrInterfaceDeclaration classDeclaration

    = unit.getClassByName(className); classDeclaration.getMethods().stream() .filter(methodDeclaration -> methodDeclaration.isAnnotationPresent(Override.class)) .forEach(methodDeclaration -> methodDeclaration.setJavadocComment("{@inheritDoc}"));
  49. 99 99 Правим документацию CompilationUnit unit = StaticJavaParser.parse(inputStream); ClassOrInterfaceDeclaration classDeclaration

    = unit.getClassByName(className); classDeclaration.getMethods().stream() .filter(methodDeclaration -> methodDeclaration.isAnnotationPresent(Override.class)) .forEach(methodDeclaration -> methodDeclaration.setJavadocComment("{@inheritDoc}"));
  50. 100 100 Правим документацию CompilationUnit unit = StaticJavaParser.parse(inputStream); ClassOrInterfaceDeclaration classDeclaration

    = unit.getClassByName(className); classDeclaration.getMethods().stream() .filter(methodDeclaration -> methodDeclaration.isAnnotationPresent(Override.class)) .forEach(methodDeclaration -> methodDeclaration.setJavadocComment("{@inheritDoc}"));
  51. 101 101 Анализируем код Set<String> dependencies = new HashSet() classDeclaration.findAll(FieldDeclaration.class).stream()

    .filter(f -> f.getVariables().get(0).getNameAsString().equals("dependencies")) .findFirst() .ifPresent(field -> { field.getVariables().get(0).getInitializer().ifPresent(init -> { if (init.isArrayInitializerExpr()) { init.asArrayInitializerExpr().getValues() .forEach(expr -> dependencies.put(expr.asStringLiteralExpr().getValue()); } }); });
  52. 102 102 Анализируем код Set<String> dependencies = new HashSet<>(); classDeclaration.findAll(FieldDeclaration.class).stream()

    .filter(f -> f.getVariables().get(0).getNameAsString().equals("dependencies")) .findFirst() .ifPresent(field -> { field.getVariables().get(0).getInitializer().ifPresent(init -> { if (init.isArrayInitializerExpr()) { init.asArrayInitializerExpr().getValues() .forEach(expr -> dependencies.put(expr.asStringLiteralExpr().getValue()); } }); });
  53. 103 103 Анализируем код Set<String> dependencies = new HashSet<>(); classDeclaration.findAll(FieldDeclaration.class).stream()

    .filter(f -> f.getVariables().get(0).getNameAsString().equals("dependencies")) .findFirst() .ifPresent(field -> { field.getVariables().get(0).getInitializer().ifPresent(init -> { if (init.isArrayInitializerExpr()) { init.asArrayInitializerExpr().getValues() .forEach(expr -> dependencies.add(expr.asStringLiteralExpr().getValue()); } }); });
  54. 104 104 Анализируем код Set<String> dependencies = new HashSet<>(); classDeclaration.findAll(FieldDeclaration.class).stream()

    .filter(f -> f.getVariables().get(0).getNameAsString().equals("dependencies")) .findFirst() .ifPresent(field -> { field.getVariables().get(0).getInitializer().ifPresent(init -> { if (init.isArrayInitializerExpr()) { init.asArrayInitializerExpr().getValues() .forEach(expr -> dependencies.add(expr.asStringLiteralExpr().getValue()); } }); });
  55. 105 105 Анализируем код Set<String> dependencies = new HashSet<>(); classDeclaration.findAll(FieldDeclaration.class).stream()

    .filter(f -> f.getVariables().get(0).getNameAsString().equals("dependencies")) .findFirst() .ifPresent(field -> { field.getVariables().get(0).getInitializer().ifPresent(init -> { if (init.isArrayInitializerExpr()) { init.asArrayInitializerExpr().getValues() .forEach(expr -> dependencies.add(expr.asStringLiteralExpr().getValue()); } }); });
  56. 108 108 POCM VOPR VMF SAT VOF MIL TPP CF

    BMN TPB RV PSP TNQ RPGT BBN AROV DAS POC CSA VLS PSTS SFC DSA WVI CRM SRT NPQ IPB VLA SMC V POC MCV SMC PRTM
  57. 109 109 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  58. 110 110 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  59. 111 111 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  60. 112 112 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  61. 113 113 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  62. 114 114 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  63. 115 115 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  64. 116 116 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  65. 117 117 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  66. 118 118 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  67. 119 119 Prevalence Ratio of Two Most Common Vertical Intervals

    Prevalence Of Most Common Vertical Interval Prevalence Of Second Most Common Vertical Interval Wrapped Vertical Interval Histogram Second Most Common Vertical Interval Most Common Vertical Interval
  68. 122 122 Counted Completers private final ConcurrentMap<String, CompletableFuture<double[]>> featuresResult; @Override

    public void compute() { List<Worker> workers = featureExtractor.getDependencies().stream() .filter(dep -> !map.containsKey(dep)) .map(dep -> map.computeIfAbsent(dep, k -> new CompletableFuture<>())) .map(future -> new Worker(this, midiRepresentation, featuresResult, name2feature.get(dep))) .collect(Collectors.toList()); addToPendingCount(workers.size()); workers.forEach(ForkJoinTask::fork); tryComplete(); }
  69. 123 123 Counted Completers private final ConcurrentMap<String, CompletableFuture<double[]>> featuresResult; @Override

    public void compute() { List<Worker> workers = featureExtractor.getDependencies().stream() .filter(dep -> !map.containsKey(dep)) .map(dep -> map.computeIfAbsent(dep, k -> new CompletableFuture<>())) .map(future -> new Worker(this, midiRepresentation, featuresResult, name2feature.get(dep))) .collect(Collectors.toList()); addToPendingCount(workers.size()); workers.forEach(ForkJoinTask::fork); tryComplete(); }
  70. 124 124 Counted Completers private final ConcurrentMap<String, CompletableFuture<double[]>> featuresResult; @Override

    public void compute() { List<Worker> workers = featureExtractor.getDependencies().stream() .filter(dep -> !map.containsKey(dep)) .map(dep -> map.computeIfAbsent(dep, k -> new CompletableFuture<>())) .map(future -> new Worker(this, midiRepresentation, featuresResult, name2feature.get(dep))) .collect(Collectors.toList()); addToPendingCount(workers.size()); workers.forEach(ForkJoinTask::fork); tryComplete(); }
  71. 125 125 Counted Completers private final ConcurrentMap<String, CompletableFuture<double[]>> featuresResult; @Override

    public void compute() { List<Worker> workers = featureExtractor.getDependencies().stream() .filter(dep -> !map.containsKey(dep)) .map(dep -> map.computeIfAbsent(dep, k -> new CompletableFuture<>())) .map(future -> new Worker(this, midiRepresentation, featuresResult, name2feature.get(dep))) .collect(Collectors.toList()); addToPendingCount(workers.size()); workers.forEach(ForkJoinTask::fork); tryComplete(); }
  72. 126 126 Counted Completers @Override public void onCompletion (CountedCompleter <?>

    caller) { double[][] dependencies = featureExtractor.getDependencies().stream() .map( dep -> map.get( dep).join()) .toArray(double[][]::new); double[] result = featureExtractor.extractFeature(midiRepresentation, dependencies); featuresResult .get(featureExtractor.getName()).complete( results); }
  73. 127 127 Counted Completers @Override public void onCompletion (CountedCompleter <?>

    caller) { double[][] dependencies = featureExtractor.getDependencies() .stream() .map( dep -> map.get( dep).join()) .toArray(double[][]::new); double[] result = featureExtractor.extractFeature(midiRepresentation, dependencies); featuresResult .get(featureExtractor.getName()).complete( results); }
  74. 128 128 Counted Completers @Override public void onCompletion (CountedCompleter <?>

    caller) { double[][] dependencies = featureExtractor.getDependencies().stream() .map( dep -> map.get( dep).join()) .toArray(double[][]::new); double[] result = featureExtractor.extractFeature(midiRepresentation, dependencies); featuresResult .get(featureExtractor.getName()).complete( results); }
  75. 129 129 Counted Completers @Override public void onCompletion (CountedCompleter <?>

    caller) { double[][] dependencies = featureExtractor.getDependencies().stream() .map( dep -> map.get( dep).join()) .toArray(double[][]::new); double[] result = featureExtractor.extractFeature(midiRepresentation, dependencies); featuresResult.get(featureExtractor.getName()).complete(results); }
  76. 133 133 Покрытие тестами 20% -> 80% Итоги Внедрение CI/CD

    Внедрение Gradle Сборка одной командой
  77. 134 134 Покрытие тестами 20% -> 80% Итоги Внедрение CI/CD

    Дублирование кода 30% -> 20% Внедрение Gradle Сборка одной командой
  78. 135 135 Покрытие тестами 20% -> 80% Итоги Внедрение CI/CD

    Дублирование кода 30% -> 20% Ускорение на 20-40% Внедрение Gradle Сборка одной командой
  79. 137 137 Покрытие тестами 20% -> 80% Итоги Внедрение CI/CD

    Дублирование кода 30% -> 20% Ускорение на 20-40% Внедрение Gradle Сборка одной командой GitHub user friendly