The post has been translated automatically. Original language: Russian
In a previous article, I described how the simple idea of filtering exercises by sport turned into an architectural problem for Potrenim.
Initially, everything looked logical: a person chooses a sports direction, and the application shows him only the appropriate exercises. But over time, it became clear that exercise does not belong to any one sport. Pull-ups are found in workout, rock climbing, gymnastics, and regular strength training. The plank can be part of a running program, home workout, or rehabilitation.
The connection between exercise and sports was supposed to help sort the catalog, but I turned it into a limitation.
Then it seemed to me that the fix would be quite local: remove the hard filter, open a global search and change several queries. In reality, the processing took much longer than I had planned.
Because it was worth removing the wrong restriction, as the following question appeared: if exercise is not defined by a sport, then what is it defined by?
It wasn't just the catalog that changed the answer to this question. A mechanism has gradually grown out of it, which allows you to adapt the training program to the equipment available to a particular person.
I thought I was redoing the filter.
At the first stage, the task looked technical.
I should have stopped treating the Exercise ↔ Sport connection as a resolution. The sports direction was supposed to be one of the signs of relevance: exercises related to the chosen sport are shown above, but a person can still find and add any other.
But it quickly became clear that the problem was deeper.
If you remove sports from the center of the model, the catalog ceases to be just a list of titles arranged in sections. The system needs to understand how the exercises differ from each other and under what conditions each of them can be performed.
For example, pull—ups are not just a workout exercise. It has more specific properties.:
- movement pattern;
- muscles involved;
- necessary equipment;
- Difficulty level;
- execution options;
- the method of calculating the result: repetitions, time, weight, or distance.
The sport also remains a useful property, but it is no longer the main one. He answers the question "in what context is this exercise often used," rather than "is it allowed to do it here."
This difference seems small, as long as the catalog is only needed to manually add an exercise to a workout. But it becomes crucial when ready-made programs appear.
The program should not be disrupted due to the lack of a turnstile.
Let's present a simple program for a person who wants to start exercising with his own weight.
It includes pull-ups, push-ups on the uneven bars and squats. Everything is working on the platform with a horizontal bar and bars. But at home, a person only has a chair. The other one has a horizontal bar, but no bars. The third one trains in the gym, where the upper block is available.
If the program stores only a rigid list of exercises, it is suitable only for a person with a previously known set of equipment.
You can create multiple versions of the same program.:
- the program for the site;
- a program for the home;
- the program for the gym;
- the rubber program;
- the program is without hardware.
But then we get manual classification again, only on a different level. Each change in methodology has to be transferred to several almost identical programs. The more equipment, training levels, and restrictions we support, the faster the number of combinations grows.
Here, rethinking the exercise gave a different option.
The program can describe not only a specific movement, but also its task within the training. For example: perform a vertical thrust of a suitable difficulty level. And already the catalog helps to find an exercise that solves this problem with the equipment available to a person.
If there is a horizontal bar, it may be pull-ups. If you don't have enough strength yet, pull—ups with rubber or a lightweight version. If a person is in the hall, pull the upper block. If there is no suitable replacement, the system should honestly report this, and not substitute a random exercise for the same muscles.
That is, we are not talking about a primitive replacement of one name with another. Exercises should be comparable in terms of movement, purpose, difficulty, and conditions of performance.
The catalog has become part of the logic of the program
Previously, the catalog answered a simple question: "What exercise does the user want to add?"
After processing, he must answer several questions at once.:
- is it possible to perform the exercise with the available equipment;
- does it correspond to the task of a specific stage of the program;
- is it suitable for a person in terms of difficulty level;
- what option can be used to replace it without losing its meaning?;
- how to record the result and track the progress.
The catalog is gradually turning from an ordinary reference book into a layer of knowledge about training.
And the program ceases to be a static array of exercise identifiers.
Conventionally, the old model looked like this:
Program → Day → Pull-ups → 3 × 5The new logic is closer to the following:
Program → Day → Vertical deadlift → suitable exercise → 3 × 5A specific exercise can still be fixed if the technique requires it. But where adaptation is acceptable, the program gets the opportunity to take into account the real conditions of a person.
This is an important point. I don't want Potrenim to generate random workouts from a set of similar movements. The program must preserve the author's intent. Adaptation is allowed only within the limits that are determined in advance by the structure of the program and the properties of the exercises.
Otherwise, instead of a useful engine, you will get a generator of a plausible, but methodically meaningless set of exercises.
Why did the processing take longer?
Initially, I assessed the task as correcting the wrong connection between sports and exercise. But changing the meaning of one entity has dragged along almost all the places where it is used.
I had to reconsider:
- the structure of the directory itself;
- exercise characteristics;
- equipment model;
- creating and editing programs;
- selecting exercises within a workout;
- search and recommendations;
- custom exercises;
- ways to account for the result.
The main difficulty wasn't even the amount of code. The code can be rewritten. It took much longer to define the boundaries of the model.
Are pull-ups with rubber bands considered a separate exercise or a variant of pull-ups? Is the chair an equipment? Can the upper block thrust be considered a full-fledged replacement for pull-ups? Where does the adaptation of the program end and another program begin? What should I do if two exercises use the same muscles, but have a completely different movement pattern?
Such questions cannot be answered with a single new table in the database. First, you need to determine how the system understands training, and only then transfer this understanding to types, connections, and interfaces.
Therefore, the rethinking took longer than the initial implementation of the filter. I did not fix a separate function. I was removing an incorrect assumption that other parts of the product had already grown on.
The error did not become a good solution in itself.
It's easy to tell a beautiful story here: I made an architectural mistake, fixed it, and accidentally created a powerful new feature.
But in reality, the process didn't look so neat.
The mistake really took time. Some of the logic had to be rewritten. Some solutions that seemed almost ready have returned to the design stage. The deadlines have increased, but the result visible to the user has not appeared for a long time.
The value did not arise from the error itself. It came from an attempt to understand why the original model stopped working.
It was possible to limit ourselves to a cosmetic fix: leave the current catalog, remove the ban and allow the search for all exercises. This would solve the immediate problem. But it would not answer the question of how to create programs for people with different training conditions.
A deeper analysis showed that sports, equipment, difficulty, and movement patterns are not competing ways to organize exercises into folders. These are different attributes, each of which is needed by the system for its task.
Sport helps to define the context. Equipment — the ability to perform. The difficulty is accessibility for humans. The movement pattern and the goal is the permissibility of substitution. The way to account for the result is the logic of the diary and progress.
When these concepts ceased to be one common filter, it became possible to assemble a more flexible system from them.
What has changed in the product as a result
The original idea Potrenim was simple: a person chooses a sport, sees the appropriate exercises and records the workout.
Now I look at this scenario differently.
A person can specify where they are going to train and what equipment they have. The program retains its purpose and structure, and the system selects an acceptable execution option. The result is recorded in the same diary, so the next workout can take into account the history and progress.
It turns out a connected path:
Available equipment
↓
Program adaptation
↓
Starting a workout
↓
Recording the result
↓
Replay and track progressThe exercise catalog in this scenario is no longer a separate reference section. It becomes the foundation that links the program, equipment, and training diary.
And it all grew out of a question that at first seemed almost formal: what is an exercise in a product model?
The main conclusion
Sometimes an architectural problem does not start with an incorrect table or an unsuccessfully chosen type of relationship. It starts with an inaccurate definition of the entity.
While exercise was an element of a particular sport for me, the catalog inevitably turned into a set of closed lists. When the exercise became an independent movement with its own properties and conditions, it became possible to use it much more widely.
At first, this freed the user from the hard filter. Then it allowed us to arrange the programs in a different way. And then we connected the program with the available equipment and turned a static set of exercises into an adaptive training scenario.
I was planning to fix a local architectural error. As a result, we had to redefine one of the central Potrenim entities.
It took longer than I expected. But it was this reinvention that gave the product mechanics that could not normally be built on top of the old model.
В прошлой статье я рассказывал, как простая идея фильтровать упражнения по виду спорта превратилась в архитектурную проблему Potrenim.
Изначально всё выглядело логично: человек выбирает спортивное направление, а приложение показывает ему только подходящие упражнения. Но со временем стало понятно, что упражнение не принадлежит какому-то одному спорту. Подтягивания встречаются в воркауте, скалолазании, гимнастике и обычной силовой подготовке. Планка может быть частью беговой программы, домашней тренировки или реабилитации.
Связь упражнения со спортом должна была помогать сортировать каталог, а я превратил её в ограничение.
Тогда мне казалось, что исправление будет достаточно локальным: убрать жёсткий фильтр, открыть глобальный поиск и изменить несколько запросов. В реальности переработка заняла намного больше времени, чем я планировал.
Потому что стоило убрать неправильное ограничение, как появился следующий вопрос: если упражнение не определяется видом спорта, то чем оно вообще определяется?
Ответ на этот вопрос изменил не только каталог. Из него постепенно вырос механизм, который позволяет адаптировать тренировочную программу под оборудование, доступное конкретному человеку.
Я думал, что переделываю фильтр
На первом этапе задача выглядела технической.
Нужно было перестать воспринимать связь Exercise ↔ Sport как разрешение. Спортивное направление должно было стать одним из признаков релевантности: упражнения, связанные с выбранным спортом, показываются выше, но человек всё равно может найти и добавить любое другое.
Но быстро выяснилось, что проблема находится глубже.
Если убрать спорт из центра модели, каталог перестаёт быть просто списком названий, разложенных по разделам. Системе нужно понимать, чем упражнения отличаются друг от друга и в каких условиях каждое из них можно выполнить.
Например, подтягивания — это не только упражнение «для воркаута». У него есть более конкретные свойства:
- паттерн движения;
- задействованные мышцы;
- необходимое оборудование;
- уровень сложности;
- варианты выполнения;
- способ учёта результата: повторения, время, вес или дистанция.
Вид спорта тоже остаётся полезным свойством, но уже не главным. Он отвечает на вопрос «в каком контексте это упражнение часто используется», а не «разрешено ли его здесь выполнять».
Это различие кажется небольшим, пока каталог нужен только для ручного добавления упражнения в тренировку. Но оно становится принципиальным, когда появляются готовые программы.
Программа не должна ломаться из-за отсутствия турника
Представим простую программу для человека, который хочет начать тренироваться с собственным весом.
В ней есть подтягивания, отжимания на брусьях и приседания. На площадке с турником и брусьями всё работает. Но дома у человека есть только стул. У другого есть турник, но нет брусьев. Третий тренируется в зале, где доступен верхний блок.
Если программа хранит только жёсткий список упражнений, она подходит лишь человеку с заранее известным набором оборудования.
Можно создать несколько версий одной программы:
- программа для площадки;
- программа для дома;
- программа для зала;
- программа с резиной;
- программа без оборудования.
Но тогда мы снова получаем ручную классификацию, только на другом уровне. Каждое изменение методики приходится переносить в несколько почти одинаковых программ. Чем больше оборудования, уровней подготовки и ограничений мы поддерживаем, тем быстрее растёт количество комбинаций.
Здесь переосмысление упражнения дало другой вариант.
Программа может описывать не только конкретное движение, но и его задачу внутри тренировки. Например: выполнить вертикальную тягу подходящего уровня сложности. А уже каталог помогает найти упражнение, которое решает эту задачу с доступным человеку оборудованием.
Если есть турник, это могут быть подтягивания. Если пока не хватает силы — подтягивания с резиной или облегчённый вариант. Если человек находится в зале — тяга верхнего блока. Если подходящей замены нет, система должна честно сообщить об этом, а не подставлять случайное упражнение на те же мышцы.
То есть речь не о примитивной замене одного названия другим. Упражнения должны быть сопоставимы по смыслу движения, цели, сложности и условиям выполнения.
Каталог стал частью логики программы
Раньше каталог отвечал на простой вопрос: «Какое упражнение пользователь хочет добавить?»
После переработки он должен отвечать сразу на несколько вопросов:
- можно ли выполнить упражнение с доступным оборудованием;
- соответствует ли оно задаче конкретного этапа программы;
- подходит ли оно человеку по уровню сложности;
- каким вариантом можно заменить его без потери смысла;
- как записывать результат и отслеживать прогресс.
Из обычного справочника каталог постепенно превращается в слой знаний о тренировках.
А программа перестаёт быть статичным массивом идентификаторов упражнений.
Условно старая модель выглядела так:
Программа → День → Подтягивания → 3 × 5Новая логика ближе к следующей:
Программа → День → Вертикальная тяга → подходящее упражнение → 3 × 5Конкретное упражнение всё ещё можно закрепить, если методика требует именно его. Но там, где допустима адаптация, программа получает возможность учитывать реальные условия человека.
Это важный момент. Я не хочу, чтобы Potrenim генерировал случайные тренировки из набора похожих движений. Программа должна сохранять замысел автора. Адаптация допустима только в тех границах, которые заранее определены структурой программы и свойствами упражнений.
Иначе вместо полезного движка получится генератор правдоподобного, но методически бессмысленного набора упражнений.
Почему переработка заняла больше времени
Изначально я оценивал задачу как исправление неправильной связи между спортом и упражнением. Но изменение смысла одной сущности потянуло за собой почти все места, где она используется.
Пришлось пересматривать:
- структуру самого каталога;
- характеристики упражнений;
- модель оборудования;
- создание и редактирование программ;
- подбор упражнений внутри тренировки;
- поиск и рекомендации;
- пользовательские упражнения;
- способы учёта результата.
Главная сложность была даже не в объёме кода. Код можно переписать. Намного больше времени заняло определение границ модели.
Считать ли подтягивания с резиной отдельным упражнением или вариантом подтягиваний? Является ли стул оборудованием? Можно ли считать тягу верхнего блока полноценной заменой подтягиваниям? Где заканчивается адаптация программы и начинается уже другая программа? Что делать, если два упражнения используют одинаковые мышцы, но имеют совершенно разный паттерн движения?
На такие вопросы нельзя ответить одной новой таблицей в базе данных. Сначала нужно определить, как система понимает тренировку, и только потом переносить это понимание в типы, связи и интерфейсы.
Поэтому переосмысление заняло больше времени, чем первоначальная реализация фильтра. Я исправлял не отдельную функцию. Я убирал неверное предположение, на котором уже успели вырасти другие части продукта.
Ошибка не стала хорошим решением сама по себе
Здесь легко рассказать красивую историю: допустил архитектурную ошибку, исправил её и случайно создал новую сильную функцию.
Но в реальности процесс выглядел не так аккуратно.
Ошибка действительно забрала время. Часть логики пришлось переписывать. Некоторые решения, которые казались почти готовыми, снова вернулись на этап проектирования. Сроки выросли, а видимого для пользователя результата долго не появлялось.
Ценность возникла не из самой ошибки. Она появилась из попытки понять, почему первоначальная модель перестала работать.
Можно было ограничиться косметическим исправлением: оставить текущий каталог, убрать запрет и разрешить поиск по всем упражнениям. Это решило бы непосредственную проблему. Но не ответило бы на вопрос, как создавать программы для людей с разными условиями тренировок.
Более глубокий разбор показал, что спорт, оборудование, сложность и паттерн движения — это не конкурирующие способы разложить упражнения по папкам. Это разные признаки, каждый из которых нужен системе для своей задачи.
Спорт помогает определить контекст. Оборудование — возможность выполнения. Сложность — доступность для человека. Паттерн движения и цель — допустимость замены. Способ учёта результата — логику дневника и прогресса.
Когда эти понятия перестали быть одним общим фильтром, из них стало возможно собрать более гибкую систему.
Что в итоге изменилось в продукте
Изначальная идея Potrenim была простой: человек выбирает спорт, видит подходящие упражнения и записывает тренировку.
Теперь я смотрю на этот сценарий иначе.
Человек может указать, где он собирается тренироваться и какое оборудование у него есть. Программа сохраняет свою цель и структуру, а система подбирает допустимый вариант выполнения. Результат записывается в тот же дневник, поэтому при следующей тренировке можно учитывать историю и прогресс.
Получается связанный путь:
Доступное оборудование
↓
Адаптация программы
↓
Запуск тренировки
↓
Запись результата
↓
Повтор и отслеживание прогрессаКаталог упражнений в этом сценарии уже не отдельный справочный раздел. Он становится основой, которая связывает программу, оборудование и дневник тренировок.
И всё это выросло из вопроса, который сначала казался почти формальным: что такое упражнение в модели продукта?
Главный вывод
Иногда архитектурная проблема начинается не с неправильной таблицы или неудачно выбранного типа связи. Она начинается с неточного определения сущности.
Пока упражнение было для меня элементом конкретного вида спорта, каталог неизбежно превращался в набор закрытых списков. Когда упражнение стало самостоятельным движением со своими свойствами и условиями выполнения, появилась возможность использовать его намного шире.
Сначала это освободило пользователя от жёсткого фильтра. Затем позволило по-другому устроить программы. А после связало программу с доступным оборудованием и превратило статичный набор упражнений в адаптивный сценарий тренировки.
Я планировал исправить локальную архитектурную ошибку. В итоге пришлось заново определить одну из центральных сущностей Potrenim.
Это заняло больше времени, чем я ожидал. Но именно это переосмысление дало продукту механику, которую невозможно было бы нормально построить поверх старой модели.