Comments 30
При этом каждая буква может расшифровываться как очень длинное предложение, если дать нормальное имя, то оно будет состоять из нескольких слов, пяти, шести. Так хотя бы видны дизъюнкции, конъюнкции, отрицания и скобки. Расшифровка букв всегда рядом.
По именованию переменных жесть конечно. Такое чувство как будто в один момент стало лень придумывать названия.
boolean isPossibleToCopyRewrite = isPossibleToCopy & L;
Возможно есть более простой способ решить вашу задачу. Я отвечу только по поводу того, что сейчас уже написано.
Думаю, что в первую очередь нужно избавиться от обозначения предикатов буквами.
Чтобы каждый из предикатов не раздулся до нечитаемых размеров, нужно их уменьшить.
Первый шаг, который приходит в голову — избавиться от "или".
boolean copyCase = (М & !К) | (А & !B & !C & !D & !K);
boolean copyRewriteCase
= (M & K) | (А & !В & !C & !D & К);
boolean cloneCase = N | (A & B & !D);
boolean moveCase = (O & !K) | (А & !B & C & !D & !К);
boolean moveRewriteCase = (O & К) | (A & !В & C & !D & K);
Получится вот такое:
boolean copyCase = false;
boolean copyRewriteCase = false;
boolean cloneCase = false;
boolean moveCase = false;
boolean moveRewriteCase = false;
boolean createLinkCase = false;
if (A & D){
Actions userChoice = askUserForAction(event);
if (userChoise == Actions.CANCEL) return;
boolean M = (userChoise == Actions.COPY);
boolean N = (userChoise == Actions.CLONE);
boolean O = (userChoise == Actions.MOVE);
boolean P = (userChoise == Actions.LINK);
copyCase = М & !K;
copyRewriteCase = M & К;
cloneCase = N;
moveCase = O & !К;
moveRewriteCase = O & K;
createLinkCase = P;
} else {
copyCase = (A & !В & !C & !D & !K);
copyRewriteCase = (А & !В & !C & !D & К);
cloneCase = (А & B & !D);
moveCase = (A & !B & C & !D & !К);
moveRewriteCase = (А & !B & C & !D & K);
}
Далее можно заменить M,N,O,P на выражения вроде userChoise == Actions.COPY
.
Становится видно, что:
- вообще любой из кейсов требует истинности A. Кажется можно проверить эту переменную где-нибудь в начале и больше ничего не проверять.
- Любой из кейсов в блоке
else
требует ложности D. Поскольку выполнение в else, можно убрать предикат!D
.
В блоке else
останется вот такое выражение
else {
copyCase = !В & !C & !K;
copyRewriteCase = (!B & !C & К);
cloneCase = (В);
moveCase = (!B & C & !К);
moveRewriteCase = (!B & C & K);
}
которое можно развернуть в дерево if'ов
else if (B) {
cloneCase = true;
} else {
if (C){
moveRewriteCase = K;
moveCase = !К;
}
copyCase = !В & !C & !K;
copyRewriteCase = (!B & !C & К);
}
copyCase и copyRewriteCase можно написать по аналогии с moveCase и moveRewriteCase.
После этого можно будет просто заменить буквы на выражения.
Чтобы было не скучно, я поменял в своем комментарии кое-где в коде английские буквы на аналогично выглядящие русские.
Вы пришли к тому, от чего я убежал. Раньше я делал либо 10-уровневые деревья, либо сочетания 5-уровневых деревьев с вынесенными отдельными поддеревьями с досрочным return. Во время изменения требований я путался и делал ошибки, так как приходилось держать в уме ситуации досрочных выходов и при необходимости возвращать вынесенные отдельные поддеревья в единое общее дерево. Каждый раз приходиться пересматривать все ветки и все вынесенные поддеревья, это было утомительно. Ведь когда-нибудь появится действие на предикат! А с сочетанием с другим предикатом и т.д.
Пользователь сначала перенес файл, что вызвало окно с прогрессом копирования. Далее пользователь переносит другой файл, но опускает не в окне папки, а в окне с монитором прогресса копирования. В этом случае нужно проверить, что можно скопировать файл, что данный файл в данный момент не копируется, после чего поставить файл в очередь на копирование.
Изменения в коде плоские и линейные:
//------------------------------------------------------
// Исходные предикаты
// ...
boolean Z = isTargetPlaceIsCopyingMonitor(event);
boolean Y = isQueueContainsFile(event);
boolean copyQueueCase = Z & !K;
boolean copyQueueRewriteCase = Z & K;
// ...
if (errorMessage.isEmpty()) {
// ...
if (copyQueueCase) addToCopyQueue(event);
if (copyQueueRewriteCase) addToCopyQueueRewrite(event);
} else {
showMessage(errorMessage + reasons);
}
Проверку на невозможность я не сделал, но там тоже изменения плоские и линейные
Лучше бы не убегали. В предикатах вроде (M & !K) | (A & !B & !C & !D & !K);
запутаться так же просто, как и в "лесу" из if'ов.
Если предлагаемый вариант не подходит, то попробуйте каждую ситуацию вынести в свой класс с методами "canBeExecuted(event)" и "execute(event)". Экземпляры ситуаций положите в коллекцию и при каждом событии вызывайте execute первой стратегии, у которой canBeExecuted вернет true. Внутри execute можно будет написать соответствующие обработчики ошибок.
Вот не надо так делать:
А) лишняя проверка, всё равно при копировании будет сделано тоже самое системой.
Б) бессмысленная проверка, перед копированием, но после проверки возможность может исчезнуть (кончилось место, например)
Поэтому надо копировать и разбирать код возврата. Реймон Чен (ЕМНИП) об этом говорил.
Будьте проще. Нет, намного проще.
- Сначала надо определить совершаемое действие, и только его: либо это перенос, либо это копирование, либо дублирование (дублирование — это, в данном случае, копирование файла в файл с именем "… копия x"). Если перенос был левой кнопкой, действие определяется автоматически, если правой — по выбору пользователя (а "автоматическое" используется как дефолт).
- Запустить соответствующую операцию, если произошла одна из перечисленных ошибок, разбираем ее.
- Ошибки делятся на те, где от пользователя нужен дальнейший ввод, и все остальные (retry-cancel мы за ввод не считаем, это общая стратегия); у дублирования ошибок-со-вводом не бывает, у остальных операций это ситуация "файл с таким именем уже существует".
- Если пользователь задал новое имя, запускаем ту же операцию, но с явным указанием целевого имени, и возвращаемся на шаг 2.
PS Если речь идет о Java или любом другом ОО-языке, каждая операция — это объект, а повторного использования кода можно добиться наследованием или композицией, в зависимости от ваших архитектурных предпочтений и языка.
Пример достаточно объемный чтобы его переписать полностью, но я бы с удовольсвие видело код вида следующего псевдокода
switch (action, someBoolFlag, secondBoolFlaga) {
case Actions.COPY, namedBoolFlagA, namedBoolFlagB: action.IfTargetDirNotEqualsSourceDir().IfTargetVolumeHasFreeSpace.Then(
askUserWantToRewrite().Then(action.Execute())
).Error(.....)
}
Error должен получать имя и некий объект от того промиса который его вызвал.
И тогда это было бы читаемо и понятно. Понятно что можно еще разделить на отдельные подпросимы и использховать их не один раз.
Заодно это бы решило вопросы с вещами вида сполнения askUserWantToRewrite и askUserForAction — так как промисы бы выполнялись в отдельном «потоке», то это бы не блокировало выполнение программы как таковой.
Идем дальше, как уже справедливо заметили выше, ранние проверки в лучшем случае бесполезны а по факту даже вредны (так как ухудшают читаемость и плодят немеряную сложность на ровном месте). Меньшая часть логики должна сидеть в блоке обработки ошибок, большая часть должна быть просто удалена. Никого не волнует причина по которой не может быть выполнено копирование (дисковое пространство, права доступа или водолей в овне).
Смелое заявление.
Проблема в читаемости. Код пишется один раз, правится несколько раз, а читается иногда десятки и сотни раз. Прочитать написанное очень сложно: приходится постоянно прыгать в начало или конец метода, приходится постоянно проверять, что означают непонятные переменные, а также прослеживать все цепочки до конца кода. Возьмём, например, строку: «Actions userChoise = (A & D)? askUserForAction(event): null;». По каким причинам userAction может стать null? Потому что не (A & D). А что такое A? А что такое D? Возвращаемся в самое начало, чтобы выяснить. А может ли метод askUserForAction вернуть null, при том что (A &D)? Лезем смотреть реализацию askUserForAction. А если userAction реально станет null? Случится ли что-нибудь плохое? Надо проследить всё до самого конца, разобраться в каждой проверке, чтобы понять, что ничего страшного не произойдёт, хотя можно было бы сразу выйти.
А так конечно хорошо быть эльфом в стране эльфов и всегда писать идеальный код, но задач много, времени мало и иногда чуток говнокода, здоровый цинизм и юнит-тесты позволяют сворачивать горы гораздо меньшими усилиями.
В данном случае можно было бы потратить столько же времени и написать более читаемый код, который было бы гораздо проще и приятнее покрывать тестами. И автор совершенно правильно делает, что хочет научиться делать лучше, вынося свой код на ревью общественности. Не у каждого смелости хватит.
— Имена переменных. Выше обсуждалось, код должен быть читабельным без комментариев.
— Большая функция. Выходит больше строк кода, что значит больше переменных, а значит длиннее их имена. Я бы выносил логические блоки в другие методы, даже если метод будет вызываться всего один раз.
— Много переменных. Состояния мыши в можно объединить в структуру и передавать её между методами. Можно будет написать проверку не так:
boolean copyCase = (M & !K) | (A & !B & !C & !D & !K);
а, хотя бы так:
boolean copyCase = isCopyCase(mouseState);
где в isCopyCase я бы скомбинровал проверку правильным образом.
— Код будет редактироваться другими членами команды, и чтобы оставить его «читабельным», они должны писать так-же неправильно. И скорее всего они не будут следовать твоим внутренним правилам, и через 3-4 сторонние правки вряд-ли сам поймешь, что происходит в коде.
— Для постройки строк оптимальнее использовать StringBuilder.
— Правильное разделение кода на пакеты, классы, методы дает возможность покрывать код тестами. Код например можно разделить на блоки: «вычитка состояния», «проверка», «выполнения действия». И нет ни одной причины писать всё это в одном вместе.
В общем, есть интересная книжка — «Боб Мартин — Чистый код», она перевернёт твой взгляд, как однажды перевернула и мой. Советую почитать.
Подключите к сборке checkstyle & pmd и попробуйте некоторое время обходиться без suppress-ов или отключения правил.
Code Review case 1