jackbauer писал(а):
Представим, что у нас последовательность действий не такая короткая, как в этом примере, а немного подлиннее, но все равно линейная:
1. получить ресурс1
2. создать ресурс2 с использованием ресурс1
3. получить ресурс3
4. создать ресурс4 с использованием ресурс2 и ресурс3
5. создать ресурс5 с использованием ресурс1
6. проделать некоторые операции с использованием ресурс3, 4 и 5.
Честно говоря, предыдущий пример был сложнее из-за выпрыгивания из цикла. Здесь всё гораздо проще.
Код:
PROCEDURE GetResourcesForXXX (VAR res3, res4, res5: Resource): BOOLEAN;
VAR
res1, res2: Resource;
success: BOOLEAN;
BEGIN
InitRes1 (res1);
InitRes2 (res2);
success := GetRes1 (res1)
& GetRes2 (res2, res1)
& GetRes3 (res3)
& GetRes4 (res4, res2, res3)
& GetRes5 (res5, res1);
FreeRes1 (res1);
FreeRes2 (res2);
RETURN success
END GetResourcesForXXX;
PROCEDURE XXX;
VAR res3, res4, res5: Resource;
BEGIN
InitRes3 (res3);
InitRes4 (res4);
InitRes5 (res5);
IF GetResourcesForXXX (res3, res4, res5) THEN
DoXXX (res3, res4, res5); (* это не обязательно отдельная процедура, просто некий код, использующий ресурсы *)
END;
FreeRes3 (res3);
FreeRes4 (res4);
FreeRes5 (res5);
END XXX;
Процедура GetResourcesForXXX добавлена исключительно ради локализации ресурсов res1 и res2 - таким образом подчёркивается, что они не используются в DoXXX, да и освобождаются пораньше. Если сильно хочется, всё можно слепить в одну процедуру тривиальным рефакторингом.
Предполагается, что не является ошибкой освобождение ресурса, для которого выполнен Init, но не выполнен Get (или Get завершился с ошибкой). Таково подавляющее большинство ресурсов, в том числе в вашем примере: если buf == NULL, то delete [] buf ничего не делает и не выбрасывает ошибку. В Delphi если объект obj: TObject = nil, то obj.Free не вызывает деструктор и не выбрасывает исключение (в отличие от obj.Destroy, например). В WinApi CloseHandle (NULL) ничего не делает, и так далее. Другими словами, InitRes может заключаться просто в присвоении NULL сразу же при объявлении идентификатора (в языке Си), и всё.
Если же для некоторый ресурс следует освобождать только после успешного Get, то добавляется простейшая охрана, как в вашем примере: if (fh) fclose (fh).
Наконец, помните, что это не обязательно код, это прежде всего схема. Читабельность кода - это во многом легкость узнавания схемы. Поэтому не обязательно должны присутствовать дополнительные процедуры, часто достаточно поделить код на блоки краткими комментариями.
jackbauer писал(а):
Теперь допустим, что требования немного поменялись. На шаге 3 нам не нужно больше создавать ресурс3. Получается, что мы не только должны изменить реализацию, но и интерфейсы всех функций, которые следуют за шагом 3.
Выяснилось, что функция только одна, да и то не обязательно её создавать. Если же эту процедуру сделать локальной (т.е. поместить внутрь PROCEDURE XXX), то изменения в её интерфейсе никого не коснутся в принципе. Если же процедуру GetResourcesForXXX использует кто-то ещё помимо XXX, то её интерфейс необходимо изменить, так как интерфейс отражает требования к задаче XXX, и эти требования изменились.
Допустим, res3 стал не нужен. Удаляем переменную res3 из процедуры XXX, компилируем. По ошибкам компилятора легко вычищается всё остальное.
jackbauer писал(а):
Так как ты упомянул "грамотную" декомпозицию, то значит ли это, что упомянутый здесь ранее метод вложенных if-ов ты считаешь неприемлимым?
Я предпочитаю обращение на "вы". Глубокий уровень вложенности затрудняет чтение, поскольку становится трудно сопоставить начало блока и его конец, тем более если блок занимает более одного экрана по вертикали. Метод считаю приемлемым, но начиная с определённой глубины вложенности лучше перейти в отдельную процедуру (опять же, если не тянется слишком много параметров, и если процедура может пригодиться кому-то ещё для повторного использования). Это путь компромиссов, как и всё искусство дизайна.
jackbauer писал(а):
Поэтому с практической точки зрения, твой подход не работает, или как ещё можно сказать, не масштабируется: с увеличением количества шагов резко возрастает количество действий, которые программист должен проделать, чтобы написать или внести изменение в код.
К сожалению, не вижу данного вывода в свете моего ответа. Более того, этот вывод представляется мне несколько странным по сути, ведь на этом же основании следует отказаться от документирования ПО и написания тестов: слишком много работы при изменении интерфейсов или постановки задачи.
Как практикующий программист, добавлю: у меня этот подход работает.