Рефакторинг кода — укрощение спагетти
Многие из нас сталкивались со сложным и запутанным фрагментом кода, напоминающим тарелку спагетти. Однако не стоит расстраиваться! В этом блоге делается попытка исследовать область рефакторинга, с помощью которого мы распутываем наш хаотичный код и преобразовываем его в элегантное, модульное и понятное решение.
Прежде чем мы начнем, что такое спагетти-код?
Спагетти-код относится к исходному коду, который является запутанным и трудным для понимания. Он имеет запутанную структуру, что затрудняет его обслуживание и, вероятно, приводит к ошибкам.
Давайте рассмотрим пример спагетти-кода:
using System;
class Program {
static void Main(string[] args) {
var random = new Random();
var flag = random.Next(1, 100) > 50;
while (true) {
Console.Write("Enter a number: ");
var input = Console.ReadLine();
if (input == "exit") break;
if (!int.TryParse(input, out int number)) {
Console.WriteLine("Input is not a number. Try again.");
continue;
}
if (flag) {
if (number % 2 == 0) {
Console.WriteLine("The number is even.");
for (var i = 0; i <= 10; i++) {
if (i == 5) break;
Console.Write(i + " ");
}
} else {
Console.WriteLine("The number is odd.");
for (var i = 0; i <= 10; i++) {
if (i == 7) break;
Console.Write(i + " ");
}
}
} else {
if (number % 3 == 0) {
Console.WriteLine("The number is even.");
for (var i = 0; i <= 10; i++) {
if (i == 5) break;
Console.Write(i + " ");
}
} else {
Console.WriteLine("The number is odd.");
for (var i = 0; i <= 10; i++) {
if (i == 7) break;
Console.Write(i + " ");
}
}
}
Console.WriteLine();
}
}
}
Давайте разберемся в ситуации шаг за шагом и попытаемся распутать эту путаницу.
Определите проблему
Одной из первых проблем, с которой мы сталкиваемся, является отсутствие осмысленных имен переменных. Содержательные и описательные имена значительно улучшают читаемость кода. Наш первый шаг, чтобы распутать код — это переименовать переменные так, чтобы они точно отражали свое назначение, делая код понятным. Эта передовая практика улучшает собственное понимание кода и помогает будущим разработчикам, работающим над кодом.
Отсутствие модульности в коде проявляется в слишком большой функции Main
, которая выполняет несколько задач. Чтобы решить эту проблему, мы разделим функцию Main
и выделим отдельные функции. Они будут извлечены в свои собственные функции или классы. Такой подход позволяет нам сгруппировать связанную логику вместе, способствует повторному использованию кода и улучшает общую структуру кодовой базы.
Другим важным аспектом, который нуждается в улучшении, является ограниченная обработка ошибок кода. Эффективная обработка ошибок имеет решающее значение для выявления неожиданных ситуаций и корректного выхода из них. Чтобы решить эту проблему, мы рассмотрим возможные исключения, которые могут возникнуть во время выполнения кода. Затем мы внедрим соответствующие механизмы обработки ошибок, такие как блоки try-catch, чтобы убедиться, что код плавно обрабатывает исключения и выдает полезные сообщения об ошибках.
Код содержит множество сложных условий if-else, что затрудняет его чтение и понимание. Чтобы упростить это, мы будем использовать такие методы, как операторы switch, полиморфизм или шаблоны проектирования, такие как шаблон стратегии. Эти подходы помогают оптимизировать условную логику и уменьшить ее сложность. Тем самым мы улучшаем читабельность кода, обслуживаемость и упрощаем его модификацию в будущем.
Разберите его на части
Чтобы начать распутывать код, наш первый шаг — разбить его на более мелкие управляемые фрагменты. Это позволяет нам сосредоточиться на конкретных функциях и улучшить модульность. Разбивая код на более мелкие части, мы можем более эффективно изолировать и понимать отдельные компоненты, что упрощает обслуживание и расширяет кодовую базу.
if (flag) {
if (number % 2 == 0) {
Console.WriteLine("The number is even.");
for (var i = 0; i <= 10; i++) {
if (i == 5) break;
Console.Write(i + " ");
}
} else {
// Other similar codes...
}
}
Чтобы улучшить модульность кода, мы можем начать с разделения логики, отвечающей за определение типа числа, и логики, отвечающей за вывод чисел, в отдельные функции или классы.
- Создайте функцию или класс с именем
DefinitionNumberType
, которые принимают число в качестве входных данных и обрабатывают логику для определения типа числа. Эта функция/класс должны анализировать число и возвращать его тип, например «четное», «нечетное», «простое» или любые другие соответствующие категории. - Затем создайте отдельную функцию или класс с именем
PrintNumbersUpTo
, который обрабатывает логику вывода чисел до заданного предела. Эта функция/ класс должны принимать ограничение в качестве входных данных и выполнять итерацию по числам, вызывая функциюDefinitionNumberType
для каждого числа и выводя результаты.
static string DetermineNumberType(int number) { /* logic here */ }
static void PrintNumbersUpTo(int terminationNumber) { /* logic here */ }
Используйте описательные имена
Для повышения читаемости и удобства обслуживания кода важно давать осмысленные имена переменным и методам. Давайте применим эту практику к коду, назначив соответствующие имена соответствующим элементам.
До:
var random = new Random();
var flag = random.Next(1, 100) > 50;
После:
private static readonly Random randomNumberGenerator = new Random();
bool isStrategyForEvenNumbers = randomNumberGenerator.Next(1, 100) > 50;
Используйте шаблоны и принципы проектирования
Чтобы улучшить читаемость кода и уменьшить сложность условных выражений, мы можем использовать Strategy Pattern. Strategy Pattern позволяет нам инкапсулировать различные алгоритмы или стратегии и динамически выбирать одну из них во время выполнения. Вот как мы можем применить шаблон стратегии для замены сложных условий:
До:
if (flag) {
if (number % 2 == 0) {
// some code
} else {
// some code
}
} else {
// more code
}
После:
private delegate bool NumberClassificationStrategy(int number);
private static readonly Dictionary<bool, NumberClassificationStrategy> numberTypeDeterminationStrategies =
new Dictionary<bool, NumberClassificationStrategy>
{
{ true, IsEven },
{ false, IsDivisibleByThree }
};
Используя Strategy Pattern, мы получаем более модульную и удобную в сопровождении структуру кода. Это упрощает сложную условную логику, улучшает читаемость кода и упрощает расширение или изменение поведения в будущем.
Улучшите обработку ошибок
Исходный код имел минимальную обработку ошибок. Давайте улучшим это.
До:
if (!int.TryParse(input, out int number)) {
Console.WriteLine("Input is not a number. Try again.");
continue;
}
После:
try
{
if (!int.TryParse(input, out int number)) {
Console.WriteLine("Input is not a number. Try again.");
continue;
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
Последние штрихи
Мы можем улучшить читаемость, используя интерполяцию строк.
До:
Console.Write(i + " ");
После:
Console.Write($"{i} ");
И вот оно! Наш окончательный код после рефакторинга стал четким, модульным и «легким» для понимания.
using System;
using System.Collections.Generic;
class Program
{
private const int MaxPrintLimit = 10;
private const int ExitCommandCode = -1;
private const int RandomThresholdForNumberType = 50;
private const string EvenNumberIndicator = "even";
private const string OddNumberIndicator = "odd";
private static readonly Random randomNumberGenerator = new Random();
private delegate bool NumberClassificationStrategy(int number);
private static readonly Dictionary<bool, NumberClassificationStrategy> numberTypeDeterminationStrategies =
new Dictionary<bool, NumberClassificationStrategy>
{
{ true, IsEven },
{ false, IsDivisibleByThree }
};
static void Main()
{
try
{
while (true)
{
int userInput = RetrieveUserInput();
if (userInput == ExitCommandCode) break;
string determinedNumberType = DetermineNumberType(userInput);
Console.WriteLine($"The number is {determinedNumberType}.");
PrintNumberSequence(determinedNumberType);
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
static int RetrieveUserInput()
{
while (true)
{
try
{
Console.Write("Enter a number: ");
string input = Console.ReadLine();
if (input == "exit") return ExitCommandCode;
if (int.TryParse(input, out int parsedNumber)) return parsedNumber;
throw new FormatException("Input is not a number. Try again.");
}
catch (FormatException fe)
{
Console.WriteLine(fe.Message);
}
}
}
static string DetermineNumberType(int number)
{
bool randomFlag = randomNumberGenerator.Next(1, 100) > RandomThresholdForNumberType;
if (numberTypeDeterminationStrategies.TryGetValue(randomFlag, out NumberClassificationStrategy numberClassificationMethod))
{
return numberClassificationMethod(number) ? EvenNumberIndicator : OddNumberIndicator;
}
else
{
throw new KeyNotFoundException("The strategy for number type determination could not be found.");
}
}
static bool IsEven(int number)
{
return number % 2 == 0;
}
static bool IsDivisibleByThree(int number)
{
return number % 3 == 0;
}
static void PrintNumberSequence(string numberType)
{
int terminationNumber = numberType == EvenNumberIndicator ? 5 : 7;
PrintNumbersUpTo(terminationNumber);
Console.WriteLine();
}
static void PrintNumbersUpTo(int terminationNumber)
{
for (int i = 0; i <= MaxPrintLimit; i++)
{
if (i == terminationNumber)
{
return;
}
Console.Write($"{i} ");
}
}
}
Цикломатическая сложность
Цикломатическая сложность (CC) — это показатель, который измеряет сложность программы. Он определяет количество независимых путей через исходный код программы. Для простых конструкций if-else вы можете оценить цикломатическую сложность, подсчитав точки принятия решения (например, if, while для операторов) и добавив единицу.
В старом коде:
- 5
if
блоков - 1
while
цикл - 2
for
цикла
Суммируя их и прибавляя единицу, цикломатическая сложность равна 9.
В новом коде:
- 2
if
блока - 1
while
цикл - Нет циклов
for
(инкапсулированы в метод)
Цикломатическая сложность нового кода равна 4.
Уменьшение цикломатической сложности упрощает код, снижает вероятность ошибок и улучшает удобство обслуживания.
Напоследок
В заключение, благодаря рефакторингу мы сделали наш код более удобным для обслуживания, простым для понимания и снизили цикломатическую сложность с 9 до 4. Это солидная победа для любого разработчика.
Помните, что хороший код зависит не от того, насколько сложным вы можете его создать, а от того, насколько простым вы можете его сделать. Как сказал Альберт Эйнштейн: “Все должно быть сделано настолько простым, насколько это возможно, но не проще”. Счастливого кодирования!