Автор книг 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 позволяет вам двигаться небольшими шагами с запасными вариантами. Используйте это в своих интересах.
А мы поможем прокачать скиллы или с самого начала освоить профессию, востребованную в любое время:
Выбрать другую востребованную профессию.
Краткий каталог курсов и профессий
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также
Комментарии (6)
Matshishkapeu
12.04.2022 00:36+8Это хороший фильм, но, если вы его не видели, свободное одиночное восхождение — это когда вы взбираетесь на скалу без защитного снаряжения, ремней безопасности и верёвок.
На каждом восхождении он использует верёвки, жгут и различные крепления верёвок.
В статье отсылка к выдающемуся восхождению Алекса Хоннолда. Поистине выдающемуся, и фильм Джимми Чина хороший. Но качество перевода всего что к этой теме относится - очень Гугл транслейт образца 2004 года. В двух приведенных фрагментах harness переведено как 'ремень безопасности' или 'жгут'. Ни одно из которых к нему отношения не имеет. Ибо в русских скалолазных терминах оно 'обвязка', 'страховочная система' или более олдскульное 'страховочная беседка' (здесь немного отдает изгибом жёлтой гитары, Визбором, и разрядной книжкой альпиниста). Такие узкоспециальные вещи надо переводить с уважением к специфике.
OlegIva
12.04.2022 22:23Такие узкоспециальные вещи надо переводить с уважением к специфике
Конечно, надо. Вот только если перевод делает не специалист, а обычный человек, не въедливый переводчик, то ему что правильный вариант, что неправильный будут одинаково непонятны.
Пример из личного опыта: дали девушке перевести микро-словарик по женской анатомии. И она для одного названия нашла на Мультитране вариант благозвучный и научный. Правда, означает он проход, который есть исключительно у мужчин, но она-то этого даже не поняла.
zloddey
12.04.2022 07:14+3На заре своей работы с
git
примерно так и пытался работать. Спустя десяток лет микрокоммиты остались, а вот всеstash
,reset
и десятки экспериментальных веток канули в Лету. Слишком легко в них случайно ошибиться и запороть изменения. Зато отлично работаетrevert
и интерактивныйrebase
. Возможно, это просто два разных flow.zueve
14.04.2022 20:10Согласен, что stash для этих целей использовать странно, нет чувства безопасности
tenzink
12.04.2022 17:48Техника отличная. Однако без умения группировать-редактировать коммиты и их названия приведёт к мусорной истории. В общем умение в `interactive rebase` крайне желательно
MonkAlex
Слишком сложный подход.
Я для похожих целей использую stage - если какой то код уже работает и несет ценность, то в stage выношу. И если продолжение разработки не очень удачное - сношу то что не попало в stage и повторяю заход.
Всё это делается в каждом коммите отдельно, коммиты средних размеров (~100 строк).