Вот что такое тактический Git

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

Автор книг Dependency Injection in .NET («Внедрение зависимостей на платформе .NET») и Code That Fits in Your Head рассказывает о своём подходе к Git и git stash, позволяющем добиться большой гибкости в работе с кодом. Опытом Марка Симана делимся к старту курса по разработке на С#.


В фильме Фри-соло скалолаз Алекс Хоннольд тренировался в свободном восхождении, чтобы покорить гору Эль-Капитан, в Йосемите. Это хороший фильм, но, если вы его не видели, свободное одиночное восхождение — это когда вы взбираетесь на скалу без защитного снаряжения, ремней безопасности и верёвок. Вот Эль-Капитан, просто чтобы вы представляли его, — это 914 метров отвесной скалы:

Потеряв хватку и упав, вы умрёте. Свободное восхождение — это невероятное усилие, но Хоннольд покоряет скалу, делая одно движение за один раз; в конце концов, эта статья о работе с Git.

Сохранение

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

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

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

Нестандартное редактирование

Редактируя код, вы переходите от одного рабочего состояния к другому, но во время редактирования код не всегда выполняется или компилируется. Рассмотрим такой интерфейс:

public interface IReservationsRepository
{
    Task Create(Reservation reservation);
 
    Task<IReadOnlyCollection<Reservation>> ReadReservations(
        DateTime dateTime);
 
    Task<Reservation?> ReadReservation(Guid id);
 
    Task Update(Reservation reservation);
 
    Task Delete(Guid id);
}

Этот код, как и большая часть кода в статье, из моей книги Code That Fits in Your Head. Как я рассказываю в разделе о паттерне Strangler Fig, в какой-то момент мне пришлось добавить новый метод в интерфейс. Он должен был перегружаться методом ReadReservations с такой сигнатурой:

Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max);

Однако, как только вы начнёте вводить это определение метода, код перестанет работать:

Task<IReadOnlyCollection<Reservation>> ReadReservations(
    DateTime dateTime);
 
T
 
Task<Reservation?> ReadReservation(Guid id);

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

Чтобы все волнистые линии исчезли, необходимо ввести все объявления метода но даже тогда код не скомпилируется. Определение интерфейса может быть синтаксически корректным, но добавление нового метода сломает другой код. Кодовая база содержит классы, реализующие интерфейс IReservationsRepository, но ни один из этих классов не определяет только что добавленный метод. Компилятор знает об этом и жалуется:

Error CS0535 'SqlReservationsRepository' does not implement interface member 'IReservationsRepository.ReadReservations(DateTime, DateTime)'

В этом нет ничего плохого. Я просто подчёркиваю, что редактирование кода включает в себя переход между двумя рабочими состояниями:

В фильме подъём опасен, но есть особенно опасный манёвр, который Алекс Хоннольд должен сделать из-за того, что не может найти безопасный маршрут. 

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

Микрокоммиты

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

Тим Оттингер [работавший в консалтинговой компании Роберта С. Мартина Object Mentor] называет это микрокоммитами. Вы не только должны фиксировать все изменения всякий раз, когда проходят тесты и компиляция, но вы сознательно должны продвигаться так, чтобы расстояние между двумя коммитами было наименьшим. 

Если можно придумать альтернативные изменения кода, выберите путь с наименьшими шагами: зачем делать опасные прыжки, когда можно продвигаться небольшими управляемыми движениями?

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

Добавление метода к интерфейсу

Как я сказал выше, мне хотелось добавить перегрузку ReadReservations в интерфейс IReservationsRepository. Причина желания раскрывается в Code That Fits Your Head, но суть не в ней, а в том, чтобы использовать Git, продвигаясь малыми приращениями.

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

Полагайтесь на компилятор, как сказано в Working Effectively with Legacy Code. Ошибки компилятора укажут на классы, в которых нет нового метода; в кодовой базе примера это SqlReservationRepository и FakeDatabase. 

Откройте эти файлы, скопируйте объявление метода ReadReservations в буфер обмена и скройте изменения в stash:

$ git stash

Saved working directory and index state WIP on tactical-git: [...]

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

Реализация SQL

Я начну с класса SqlReservationsRepository. Перейдя к строке, в которую хочется добавить новый метод, я вставляю его объявление:

Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max);

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

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{

}

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

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    throw new NotImplementedException();
}

Вы можете не захотеть фиксировать в Git код, бросающий NoImplementedException, но у нового метода нет вызывающего кода. Все тесты проходят, и код компилируются, ведь он не изменялся. Зафиксируем изменения:

$ git add . && git commit
[tactical-git 085e3ea] Add ReadReservations overload to SQL repo
 1 file changed, 5 insertions(+)

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

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

Я не буду делать это здесь, потому что пытаюсь сохранить SqlReservationsRepository как Humble Object. Реализация будет иметь цикломатическую сложность, равную 2. Учитывая сложность написания и поддержки теста интеграции с базой данных, я считаю, что это достаточно низкий уровень, чтобы отказаться от теста. Но, если вы не согласны, ничто не мешает добавить тесты на этом этапе.

public async Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    const string readByRangeSql = @"
        SELECT [PublicId], [Date], [Name], [Email], [Quantity]
        FROM [dbo].[Reservations]
        WHERE @Min <= [Date] AND [Date] <= @Max";
 
    var result = new List<Reservation>();
 
    using var conn = new SqlConnection(ConnectionString);
    using var cmd = new SqlCommand(readByRangeSql, conn);
    cmd.Parameters.AddWithValue("@Min", min);
    cmd.Parameters.AddWithValue("@Max", max);
 
    await conn.OpenAsync().ConfigureAwait(false);
    using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
    while (await rdr.ReadAsync().ConfigureAwait(false))
        result.Add(
            new Reservation(
                (Guid)rdr["PublicId"],
                (DateTime)rdr["Date"],
                new Email((string)rdr["Email"]),
                new Name((string)rdr["Name"]),
                (int)rdr["Quantity"]));
 
    return result.AsReadOnly();
}

Конечно, это занимает больше минуты, но если вы делали подобные вещи раньше, то, вероятно, займёт меньше времени, особенно если вы предварительно получили результат SELECT, возможно, поэкспериментировав с редактором запросов. Код снова компилируется, все тесты проходят. Фиксируем изменения:

$ git add . && git commit
[tactical-git 6f1e07e] Implement ReadReservations overload in SQL repo
 1 file changed, 25 insertions(+), 2 deletions(-)

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

Реализация заглушки

Другой класс, реализующий IReservationsRepository, называется FakeDatabase. Это заглушка, своего рода дублёры, только для поддержки автоматизированного тестирования. Новый метод реализуется точно так же, как в SqlReservationsRepository. 

Сначала добавьте этот метод:

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    throw new NotImplementedException();
}

Код компилируется, все тесты проходят. Фиксируем изменения:

$ git add . && git commit
[tactical-git c5d3fba] Add ReadReservations overload to FakeDatabase
 1 file changed, 5 insertions(+)

Добавим реализацию:

public Task<IReadOnlyCollection<Reservation>> ReadReservations(DateTime min, DateTime max)
{
    return Task.FromResult<IReadOnlyCollection<Reservation>>(
        this.Where(r => min <= r.At && r.At <= max).ToList());
}

И снова код компилируется, тесты проходят:

$ git add . && git commit
[tactical-git e258575] Implement FakeDatabase.ReadReservations overload
 1 file changed, 2 insertions(+), 1 deletion(-)

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

Изменим интерфейс

Имейте в виду, что методы добавляются в ожидании изменения интерфейса IReservationsRepository, но сам интерфейс ещё не изменился: я скрыл его изменение. Теперь новый метод используется везде, где он должен быть, то есть в SqlReservationsRepository и в FakeDatabase.

Вернём скрытые изменения:

$ git stash pop
On branch tactical-git
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   Restaurant.RestApi/IReservationsRepository.cs

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (4703ba9e2bca72aeafa11f859577b478ff406ff9)

Код повторно добавляет в интерфейс перегрузку метода ReadReservations. Когда я впервые попытался добавить её, код не скомпилировался: реализующие интерфейс классы не имели этого метода. Иными словами, код компилируется сразу, и все тесты проходят. 

Фиксируем изменения:

$ git add . && git commit
[tactical-git de440df] Add ReadReservations overload to repo interface
 1 file changed, 2 insertions(+)

Вот и всё. Применяя git stash тактически, мы разделили длинный манёвр на пять более безопасных шагов.

Тактический Git

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

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

Возможно, он компилируется; или нет. Всё нормально: всегда можно вернуться к точке последнего сохранения. Часто я сбрасываю работу, скрывая результаты сырых экспериментов. Так я не выбрасываю ничего, что может стать ценным, но начинаю с чистого листа. Вероятно, командой git stash для повышения маневренности я пользуюсь чаще всего; во вторую очередь полезна возможность локального перемещения между ветками. 

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

Возможность спрятать изменения хороша, когда вы обнаружите, что код, который вы пишете прямо сейчас, нуждается в чём-то ещё, например во вспомогательном методе, которого ещё нет.  Спрячьте изменения в stash, добавьте то, о чём вы только что узнали, зафиксируйте это и верните скрытые изменения. Пример есть в подразделе 11.1.3 Раздельный рефакторинг тестового и производственного кода книги Code That Fits in Your Head.

Часто я использую git rebase. Я не сторонник объединения коммитов, но не испытываю угрызений совести по поводу изменения порядка коммитов в моих локальных ветках Git. Пока я не делюсь коммитами со всем миром, переписать историю коммитов может быть полезным.

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

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

Заключение 

Git — это инструмент с большим потенциалом, чем думают многие. Обычно программисты, чтобы синхронизировать работу с другими людьми, только когда чувствуют необходимость в git push и git pull. Хотя это полезная и важная функция Git, если это всё, что вы делаете, то можно использовать централизованную систему управления версиями.

Ценность Git — в тактическом преимуществе. Можно экспериментировать, ошибаться, метаться и напрягаться на своём компьютере, и сбросить работу через git reset в любой момент, если всё станет слишком сложно.

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

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

А мы поможем прокачать скиллы или с самого начала освоить профессию, востребованную в любое время:

  • Профессия C#-разработчик

  • Профессия Fullstack-разработчик на Python

Выбрать другую востребованную профессию.

Краткий каталог курсов и профессий

Data Science и Machine Learning

  • Профессия Data Scientist

  • Профессия Data Analyst

  • Курс «Математика для Data Science»

  • Курс «Математика и Machine Learning для Data Science»

  • Курс по Data Engineering

  • Курс «Machine Learning и Deep Learning»

  • Курс по Machine Learning

Python, веб-разработка

  • Профессия Fullstack-разработчик на Python

  • Курс «Python для веб-разработки»

  • Профессия Frontend-разработчик

  • Профессия Веб-разработчик

Мобильная разработка

  • Профессия iOS-разработчик

  • Профессия Android-разработчик

Java и C#

  • Профессия Java-разработчик

  • Профессия QA-инженер на JAVA

  • Профессия C#-разработчик

  • Профессия Разработчик игр на Unity

От основ — в глубину

  • Курс «Алгоритмы и структуры данных»

  • Профессия C++ разработчик

  • Профессия Этичный хакер

А также

  • Курс по DevOps

  • Все курсы

Источник: https://habr.com/ru/company/skillfactory/blog/660335/


Интересные статьи

Интересные статьи

Хочу поделиться с вами, как мы проводим продукт-сессии - процесс в течении которого команда разработчиков, отталкиваясь от исходных данных, выходит на идеи и концепции новых продуктов.
Многие компании успешно внедрили практики DevOps в свой инженеринг. Мы в SHARE NOW сделали также. Команды в компании ответственны не только за разработку программ, но и за то как эти программы попадут...
Всем привет! Думаю, у многих сразу возник другой вопрос — а зачем вообще нужна ещё одна статья про LLVM, ведь на хабре их и так больше сотни? Моей задачей было написать "введение...
Это пост удивления. Я сходил на собеседования в 3 минских стартапа и в одну небольшую компанию, и вот, что из этого вышло. Читать дальше →
Мы публикуем видео с прошедшего мероприятия. Приятного просмотра.