Перейти к содержанию

Правильная выгрузка DLL


Antonshka

Рекомендуемые сообщения

Привет всем!

Я решил отвлечься на время от написания библиотеки и попробовать себя в написании небольшой DLL. Я давно планировал это сделать. На С++.

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

 

На просторах интернета я нашел исходники одного приложения. В нем имеется такой код

Спойлер
#include "Main.h"

DWORD WINAPI RunCT(LPVOID lpArg)
{
  g_mainHandle = new Main();
  if (g_mainHandle->Initialize())
    g_mainHandle->Run();
  delete g_mainHandle;
  FreeLibraryAndExitThread(g_dllHandle, 0);
  return 0;
}

DWORD WINAPI DllMain(_In_ HINSTANCE hInstance, _In_ DWORD fdwReason, _In_ LPVOID lpvReserved)
{
  if (fdwReason == DLL_PROCESS_ATTACH)
  {
    g_dllHandle = hInstance;
    CreateThread(NULL, NULL, RunCT, NULL, NULL, NULL);
  }
  return 1;
}

 

 

Какие ошибки в этом коде вы видите?

1 - по совету Джефри Рихтера, нужно стараться всегда использовать _beginthreadex, а не CreateThread

выписка из книги Джефри Рихтера

Спойлер

CreateThread — это Windows-функция, создающая поток. Но никогда не вы-
зывайте ее, если Вы пишете код на C/C++. Вместо нее Вы должны использо-
вать функцию _beginthreadex из библиотеки Visual C++. (Если Вы работаете с
другим компилятором, он должен поддерживать свой эквивалент функции
CreateThread.) Что именно делает _beginthreadex и почему это так важно, я
объясню потом.

2 - в коде из интернета, вызывается FreeLibraryAndExitThread

выписка из книги Джефри Рихтера

Спойлер

DLL можно выгрузить и с помощью другой функции:
VOID FreeLibraryAndExitThread(
HINSTANCE hinstDll,
DWORD dwExitCode);
Она реализована в Kernel32.dll так:
VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode) {
FreeLibrary(hinstDll);
ExitThread(dwExitCode);
}

Код из интернета, вызывая FreeLibraryAndExitThread, по сути вначале вызывает FreeLibrary, и весь модуль DLL выгружается, а затем, вызывает ExitThread, и поток завершается, и уже не возвращается в процедуру, из которой была вызвана FreeLibraryAndExitThread. А так как поток уже не возвращается, то -

выписка из книги Джефри Рихтера

Спойлер

Поток можно завершить принудительно, вызвав:

VOID ExitThread(DWORD dwExitCode);
При этом освобождаются все ресурсы операционной системы, выделенные дан-
ному потоку, но C/C++-ресурсы (например, объекты, созданные из C++-классов) не
очищаются. Именно поэтому лучше возвращать управление из функции потока, чем
самому вызывать функцию ExitThread. (Подробнее на эту тему см. раздел «Функция
ExitProcess» в главе 4.)

В коде из интернета объект класса Main конечно хоть и удаляется через delete, но есть же еще и другие вещи -

выписка из книги Джефри Рихтера

Спойлер

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

 

 

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

Так как же мне тогда произвести выгрузку DLL? Оставив процесс запущенным.

 

Еще один интересный факт, обнаруженный в ходе теста, - если поток создан через CreateThread, то вызов FreeLibraryAndExitThread отрабатывает нормально, в том плане что модуль DLL сразу же выгружается. Но если поток создан через _beginthreadex, то вызов FreeLibraryAndExitThread не выгружает модуль DLL вообще. Да и было бы неправильно, создать поток через _beginthreadex, а звершить его через ExitThread (который в FreeLibraryAndExitThread), вместо _endthreadex.

Ссылка на комментарий
Поделиться на другие сайты

Может попробовать создать вторую DLL, вспомогательную. Назовем ее B. Основную же DLL, назовем А

В как и А, загружается в процесс. Но В загружается на постоянную основу, - она пребудет в процессе до его завершения. Да и ладно, он же маленькая.

Задача В, выгружать любую указанную DLL.

Когда хотим выгрузить А, то А вызывает единственную функцию в B, функцию Unload(HANDLE A, HINSTANCE A). Эта функция принимает хендл потока А, и инстанс DLL А.

В этой функции создается поток P (причем сразу же вызывается для P CloseHandle).

А, после возврата из Unload(), выходит через свой return 0.

Поток P, в цикле, проверяет состояние объекта потока A, на STILL_ACTIVE. Как только этот флаг будет сброшен, P вызывает FreeLibrary для DLL А. Далее P закрывает хендл объекта потока А, и наконец, сам завершается.

Изменено пользователем Antonshka
Ссылка на комментарий
Поделиться на другие сайты

Придумал вариант получше. Никаких дополнительных DLL.

Ехе файл инжектора вначале инжектит основную DLL, и записывает в специальную её переменную свой путь на диске. Далее Exe файл завершает свою работу.

Затем, когда нужно выгрузить DLL, эта DLL находит по переданному ей пути Exe файл инжектора, и запускает его с параметром Unload. Далее Exe делает все то же что и ранее делала дополнительная DLL. То есть Exe выгружает DLL, а затем и сам завершается.

 

Если DLL не найдет Exe по указанному пути, или если процесс в который была заинжектена DLL будет намерен завершиться, то Exe не будет запускаться.

 

Ссылка на комментарий
Поделиться на другие сайты

Потому что так пишет Рихтер.

Как я написал в начале поста, Рихтер настоятельно не рекомендует самостоятельное завершение потока.

Спойлер

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

 

Поток можно завершить четырьмя способами:
1 функция потока возвращает управление (рекомендуемый способ);
2 поток самоуничтожается вызовом функции ExitThread (нежелательный способ);
3 один из потоков данного или стороннего процесса вызывает функцию Termi-
nateThread (нежелательный способ);
4 завершается процесс, содержащий данный поток (тоже нежелательно).

 

Далее, по логике Рихтера, так как API функция FreeLibraryAndExitThread, вызывает ExitThread, - значит этот вариант выгрузки DLL отпадает. Тем более этот вариант отпадает, что согласно моему тесту, FreeLibraryAndExitThread, вызванная из потока запущенного через _beginthreadex, не выгружает библиотеку! Даже не вызывается DLL_PROCESS_DETACH.

А вот если FreeLibraryAndExitThread вызвана из потока запущенного через CreateThread, тогда библиотека выгружается.

Но по совету того же Рихтера, нужно использовать всегда _beginthreadex.

Спойлер

CreateThread — это Windows-функция, создающая поток. Но никогда не вы-
зывайте ее, если Вы пишете код на C/C++. Вместо нее Вы должны использо-
вать функцию _beginthreadex из библиотеки Visual C++. (Если Вы работаете с
другим компилятором, он должен поддерживать свой эквивалент функции
CreateThread.) Что именно делает _beginthreadex и почему это так важно, я
объясню потом.

 

Значит, нужно использовать только _beginthreadex и FreeLibrary. Но на MSDN пишут

Спойлер

A thread that must unload the DLL in which it is executing and then terminate itself should call FreeLibraryAndExitThread instead of calling FreeLibrary and ExitThread separately. Otherwise, a race condition can occur. For details, see the Remarks section of FreeLibraryAndExitThread.

Получается расхождение.

 

1 час назад, Xipho сказал:

Почему библиотека не может сама убить свой поток в секции DLL_PROCESS_DETACH?

Потому что Рихтер так пишет. Поток должен выйти через свой return 0. Если завершить поток в DLL_PROCESS_DETACH, он уже не вернется в свою процедуру, и не вызовет деструкторы и прочие подобные вещи, который компилятор встраивает в конце процедуры.

 

Итог, нужно соблюсти следующие условия.

1 - поток запускается только через _beginthreadex

2 - DLL выгружается только через FreeLibrary

3 - поток завершается только сам, возвращая управление через return 0.

Ссылка на комментарий
Поделиться на другие сайты

19 минут назад, Xipho сказал:

Ну ок. Что мешает в потоке сделать вечный цикл типа while(isRunning), а в DLL_PROCESS_DETACH выставить isRunning в false?

Мешает race condition. Ведь -

 

- я вызываю FreeLibrary из потока, reference count для DLL уменьшается, становится 0. Система готова выгрузить DLL, но, вначале идет вызов DllMain

- вызвается DllMain с DLL_PROCESS_DETACH. Всё, система решается выгрузить DLL. И выгружает, допустим. Но у нас же там еще поток не завершен, и плюс мы туда еще должны вернуться, из FreeLibrary(). А куда возвращаться, кода DLL уже нет в памяти.

 

Это из MSDN Remak для FreeLibraryAndExitThread

Спойлер

The FreeLibraryAndExitThread function allows threads that are executing within a DLL to safely free the DLL in which they are executing and terminate themselves. If they were to call FreeLibrary and ExitThread separately, a race condition would exist. The library could be unloaded before ExitThread is called.

 

А это из книги Рихтера

Спойлер

Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьез-
ная проблема: FreeLibrary тут же отключит DLL от адресного пространства процесса.
После возврата из FreeLibrary код, содержащий вызов ExitThread, окажется недосту-
пен, и поток попытается выполнить не известно что. Это приведет к нарушению до-
ступа и завершению всего процесса!
С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет
FreeLibrary, и та сразу же отключит DLL. Но следующая исполняемая инструкция на-
ходится в Kernel32.dll, а не в только что отключенной DLL. Значит, поток сможет про-
должить выполнение и вызвать ExitThread, которая корректно завершит его, не воз-
вращая управления.

 

Не зря придумана FreeLibraryAndExitThread. Но, она нам не подходит, как я написал уже.

Она вызывает ExitThread, это плохо, по Рихтеру. Плюс она вызывает ExitThread а не _endthreadex. Ведь мы должны запустить поток с _beginthreadex, и завершить его мы должны соответственно с _endthreadex, чтобы очистить в потоке блок памяти tiddata.

 

Ссылка на комментарий
Поделиться на другие сайты

Давно я не кодил на плюсах, тем более, под винду. И уж точно сейчас не буду сидеть и вспоминать, как оно там. Как мне кажется, ты сильно подвержен явлению под названием "давление авторитета". Почитай, что это такое, и почему это плохо.

Ссылка на комментарий
Поделиться на другие сайты

53 минуты назад, Antonshka сказал:

Далее, по логике Рихтера, так как API функция FreeLibraryAndExitThread, вызывает ExitThread, - значит этот вариант выгрузки DLL отпадает.

А может, Рихтер как раз имел в виду не прямой вызов ExitThread, а вызов через FreeeLibraryAndExitThread?

Ссылка на комментарий
Поделиться на другие сайты

23 минуты назад, Xipho сказал:

Как мне кажется, ты сильно подвержен явлению под названием "давление авторитета". Почитай, что это такое, и почему это плохо.

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

 

15 минут назад, Xipho сказал:

А может, Рихтер как раз имел в виду не прямой вызов ExitThread, а вызов через FreeeLibraryAndExitThread?

Не понял этого предложения.

Прямой или не прямой вызов ExitThread, - в любом случае, при ExitThread, поток уже не выйдет через return своей процедуры.

Ссылка на комментарий
Поделиться на другие сайты

Да какая разница, он в любом случае выходит в ExitThread в итоге, возвращается в одно место, что через return, что так, что разводить полемику на пустом месте

Ссылка на комментарий
Поделиться на другие сайты

24 минуты назад, imaginary сказал:

Да какая разница, он в любом случае выходит в ExitThread в итоге, возвращается в одно место, что через return, что так, что разводить полемику на пустом месте

Разница есть, в утечке памяти.

 

Сравни

Спойлер
#include <windows.h>
#include <stdio.h>

class CSomeObj {
public:
	CSomeObj() { printf("Constructor\r\n"); }
	~CSomeObj() { printf("Destructor\r\n"); }
};

CSomeObj g_GlobalObj;

int main() {
	CSomeObj LocalObj;
	ExitThread(0);
	return 0;
}

 

с этим.

Спойлер
#include <windows.h>
#include <stdio.h>

class CSomeObj {
public:
	CSomeObj() { printf("Constructor\r\n"); }
	~CSomeObj() { printf("Destructor\r\n"); }
};

CSomeObj g_GlobalObj;

int main() {
	CSomeObj LocalObj;
	//ExitThread(0);
	return 0;
}

 

 

Первый код выведет,

Спойлер

Constructor
Constructor

второй код выведет.

Спойлер

Constructor
Constructor
Destructor
Destructor

 

Но это мы рассмотрели только объекты нами созданных классов. А помимо них есть еще и другие объекты классов, в библиотеках С++. Да и кто еще знает, что там еще есть, что еще очищается при выходе через естественный return.

Так что давай, не разводи полемику, а помоги мне разобраться. Так ли все есть на самом деле, как я описал в этом топике.

Изменено пользователем Antonshka
Ссылка на комментарий
Поделиться на другие сайты

Никаких утечек не будет после выгрузки dll

И вообще, когда dll создаётся, создаётся поток через CreateThread и после этого - он проходит через функцию активации, а дальше возвращается с результатом, потом тебе если нужен поток, создаёшь через CreateThread, а далее если он не требуется, пусть возвращается через return. А если надо выгрузить библиотеку то как раз используется функция FreeLibraryAndExitThread, которая выгружает библиотеку, а потом сам поток, не позволяя ему возвращаться в пустое место. Всё придумано давно,  в майкрософте указано как это всё использовать, но ты прочитал книжку и всё, появилась какая то проблема.

Ссылка на комментарий
Поделиться на другие сайты

Не понял некоторых твоих слов.

По поводу CreateThread, вот выписка с MSDN Remark CreateThread

Спойлер

A thread in an executable that calls the C run-time library (CRT) should use the _beginthreadex and _endthreadex functions for thread management rather than CreateThread and ExitThread; this requires the use of the multithreaded version of the CRT. If a thread created using CreateThread calls the CRT, the CRT may terminate the process in low-memory conditions.

Так что никаких CreateThread. Только _beginthreadex. Более того, даже std::thread использует _beginthreadex.

 

Дальше, по логике, так как поток мы создаем через функцию _beginthreadex,то и выход должен быть через соответствующую ей функцию _endthreadex, которая вызывается неявно когда поток возвращает управление через свой return. Но при вызове FreeLibraryAndExitThread вызывается ExitThread, а не _endthreadex, соответственно блок памяти потока tiddata не освобождается, это первая утечка. Вторая утечка, произойдет если поток не выйдет через свой return, а выйдет раньше, примеры кода я тебе написал выше.

 

Также, DLL не выгрузится, если поток который впоследствии вызовет FreeLibraryAndExitThread, был создан через _beginthreadex. Да это и неправильно, как я уже написал.

 

Я загружаю DLL через свою EXE, через CreateRemoteThread

Спойлер
VOID injectDll(DWORD processID, CONST std::wstring& filePath) {
	BOOL isDllInjected{ FALSE };
	HMODULE kernell32 = GetModuleHandleW(L"kernel32.dll");
	if (kernell32) {
		LPTHREAD_START_ROUTINE loadLibWAddress = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(kernell32, "LoadLibraryW"));
		HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
		if (loadLibWAddress && process) {
			DWORD pathSize = filePath.size() * sizeof(WCHAR);
			LPVOID allocForDLLPath = VirtualAllocEx(process, NULL, pathSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
			if (allocForDLLPath) {
				WriteProcessMemory(process, allocForDLLPath, filePath.c_str(), pathSize, NULL);
				HANDLE remoteThread = CreateRemoteThread(process, NULL, 0, loadLibWAddress, allocForDLLPath, 0, NULL);
				if (remoteThread) {
					WaitForSingleObject(remoteThread, INFINITE);
					CloseHandle(remoteThread);
				}
				isDllInjected = TRUE;
				VirtualFreeEx(process, allocForDLLPath, 0, MEM_RELEASE);
			}
		}
		if (process) {
			CloseHandle(process);
		}
	}
	if (!isDllInjected) {
		std::wstring errorNotify = L"Inject DLL failed";
		mainWindow->setNotifyMessage(errorNotify);
		mainWindow->invalidateFullRectWithErase();
	}
	else {
		std::wstring errorNotify = L"DLL injected \n You can close this window";
		mainWindow->setNotifyMessage(errorNotify);
		mainWindow->invalidateFullRectWithErase();
	}
}

 

В DLL

Спойлер
BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        createThreadGUI(hinstDLL);
        break;
    }
    return TRUE;
}

 

createThreadGUI()

Спойлер
HINSTANCE instanceDll{ NULL };
HANDLE dllGUIThreadID{ NULL };


VOID createThreadGUI(HINSTANCE hinstDLL) {
	instanceDll = hinstDLL;
	dllGUIThreadID = reinterpret_cast<HANDLE>(_beginthreadex(NULL, 0, threadProcedure, NULL, 0, NULL));
	if (dllGUIThreadID) {
		CloseHandle(dllGUIThreadID);
	}
}

 

threadProcedure

Спойлер
unsigned WINAPI threadProcedure(LPVOID param) {
	//Разный рабочий код.
	//Выход должен быть через return 0;
	return 0;
}

 

 

 

Ссылка на комментарий
Поделиться на другие сайты

1 час назад, Antonshka сказал:

Так что никаких CreateThread

А ты в курсе что в итоге всё равно вызывается CreateThread?

Если уж так трясёшься не использовать CreateThread что бы ты его не видел у себя в коде, значит не надо выгружать библиотеку, один раз загрузить и будет находиться в процессе, пока не закроют его.

Ссылка на комментарий
Поделиться на другие сайты

18 минут назад, Xipho сказал:

В твоем потоке есть вызовы CRT?

Я ждал этот вопрос.

Пока нет, но когда они могут появиться, я не хочу возвращаться и переписывать с CreateThread на _beginthreadex. Плюс, я не могу знать, вызывают ли те WinAPI функции или не WinAPI функции, которые я могу использовать, внутренне CRT функции. Или вообще, чья-то сторонняя библиотеке, которую я могу подгрузить с Гитхаба, или еще откуда. Зачем нужно беспокойство об этом.

 

20 минут назад, imaginary сказал:

А ты в курсе что в итоге всё равно вызывается CreateThread?

Да, об этом я читал. _beginthreadex, создает структуру tiddata для CRT, затем вызывает CreateThread. Структура нужна для работы с CRT.

Но что нам дает это знание, что все равно вызывается CreateThread?

Я почему против CreateThread то, - потому что по словам Рихтера, поток, созданный с ее помощью, не работает с CRT. Вот и вся причина. А так, это единственная функция в Windows, которая способна запустить поток.

 

28 минут назад, imaginary сказал:

Если уж так трясёшься не использовать CreateThread что бы ты его не видел у себя в коде, значит не надо выгружать библиотеку, один раз загрузить и будет находиться в процессе, пока не закроют его.

Нет, так не пойдет. Моя задача докопаться до истины, как бы это не звучало странно.

Этот вопрос должен быть закрыт раз и навсегда. Раз я уж взялся за него. Я должен уметь выгружать библиотеку правильно.

 

Для того я и поднял этот вопрос. Обсудить его, прояснить его для себя и нас.

Ссылка на комментарий
Поделиться на другие сайты

Извините меня пожалуйста. Может не в своё дело вмешиваюсь. Я всегда уважал вас imaginari и Xiho и буду уважать в дальнейшем, вы были и будете для меня авторитетами. Но в этой теме человек ясно описал ситуацию. Что хочет это сделать правильно, написал чего он хочет избежать и каких функций опасается. Может он и не прав ссылаясь на источник Рихтера, но не поверю что то что он хочет и как хочет неразрешимая проблема. Вы задаёте наводящие вопросы и даёте советы там где он чего то опасается, наверное ваши выводы правильные на все сто процентов и вы для себя используете это как закон. Но в законах есть исключения. Так почему бы не найти такое решение чтоб было без ошибок но без тех вещей которые пользователь не хочет? Ещё раз извините нагрубить или обидеть не хотел.

Ссылка на комментарий
Поделиться на другие сайты

22 минуты назад, Pitronic сказал:

Так почему бы не найти такое решение чтоб было без ошибок но без тех вещей которые пользователь не хочет?

Начинай поиск

Ссылка на комментарий
Поделиться на другие сайты

Если суммировать, то получается, самая лучшая формула создания потока, его завершения, а также выгрузки DLL, на С++, это

 

- поток всегда создается через _beginthreadex

- поток всегда завершается сам, выходя через свой return

- FreeLibraryAndExitThread никогда не вызывается

- выгрузкой библиотеки, через FreeLibrary, занимается стороннее приложение/процесс. Как вариант, тот же, который и загрузил её.

 

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

 

Когда же процесс сам лично загружает DLL, то там немного другая история, и ее, по крайней мере я, здесь не рассматривал.

Ссылка на комментарий
Поделиться на другие сайты

16 часов назад, Antonshka сказал:

Пока нет, но когда они могут появиться, я не хочу возвращаться и переписывать с CreateThread на _beginthreadex.

Почитай про принципы KISS и YAGNI


 

Ссылка на комментарий
Поделиться на другие сайты

3 часа назад, Xipho сказал:

Почитай про принципы KISS и YAGNI

 

Спасибо за советы. Почитал, для тех кому тоже интересно, вот выписка -

Спойлер

KISS – keep it short simple – делайте вещи проще. Порой наиболее правильное решение – это наиболее простая реализация задачи, в которой нет ничего лишнего.

Эта аббревиатура (англ. kiss — поцелуй, целовать) мне самому всегда нравилась — как по форме, так и по значению: Keep it simple, stupid («Сделай это проще, тупица») или, если кому-то не нравится называться тупицей, есть вариант Keep it stupid simple («Пусть всё будет простым до безобразия»), который ещё лучше передаёт смысл аббревиатуры.

Решая какую-нибудь проблему, можно так увлечься, что сам не заметишь, как уже занялся оверинжинирингом или, как я люблю говорить, вовсю палишь из пушки по воробьям. Задача в итоге, конечно, будет решена — но её можно было бы выполнить куда проще и изящнее.

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

Проверяйте, достаточно ли понятны ваши логические цепочки. Хватит ли знаний вашим коллегам, чтобы в них разобраться? Простой код и простой дизайн уменьшают риск ошибок, да и читать такой код проще. В общем, не забывайте про KISS!

 

 

YAGNI — You ain’t gonna need it – вам это не понадобится. Все что не предусмотрено техническим заданием проекта, не должно быть в нем.

Принцип, иначе известный как You ain’t gonna need it («Вам это не понадобится»), пришёл из экстремального программирования. Согласно ему создавать какую-то функциональность следует только тогда, когда она действительно нужна.

Дело в том, что в рамках Agile-методологий нужно фокусироваться только на текущей итерации проекта. Работать на опережение, добавляя в проект больше функциональности, чем требуется в данный момент, — не очень хорошая идея, учитывая, как быстро могут меняться планы.

Что я имею в виду, говоря про смену планов? Итерации в Agile довольно короткие — то есть вы будете получать какой-то фидбэк уже на ранних этапах разработки, и потенциально он может изменить направление всей работы над проектом. Так зачем вам тратить время на функцию, которая в итоге окажется совершенно ненужной?

Однако замечу напоследок, что если вы используете методы каскадной разработки, где вся работа планируется заранее и все стараются чётко придерживаться плана, принцип YAGNI неприменим

Ссылка на комментарий
Поделиться на другие сайты

×
×
  • Создать...

Важная информация

Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.