Рефакторингът на кода в наследения софтуер е възможност за преструктуриране и подобряване на източника на код, като същевременно се запазват същите функции. Овладяването на техники за рефакторинг може да спести време, отколкото да изхвърлите кода и да го изградите от нулата. Рефакторингът на код става стратегически, когато трябва да извършите модификации на наследена кодова база, особено когато липсват модулни тестове, за да сте сигурни, че няма да въведете регресии. Тази публикация е кратко пътуване към 8 техники за рефакторинг, които биха улеснили работата ви като разработчик.

Те произтичат от уеб семинар за рефакторинг, организиран с нашия партньор Arolla, експерт от френска компания в практиките на Изработка на софтуер (повторението е „тук“, но само на френски).

Съвет #1 за рефакторинг: Дайте по-смислени имена на елементите на кода

Нека започнем с една проста техника за рефакторинг, преименуването. Обща насока в софтуерното инженерство е да наименувате вашите променливи, функции и класове, така че да можем да разберем тяхното намерение и тяхната бизнес стойност. Доброто именуване ще подобри драстично разбираемостта на част от кода. Да предположим, че сега се справяте със следния код в клас, наречен TennisGame:

public String getScore() {
    String s;
    if (p1 < 4 && p2 < 4 && !(p1 + p2 == 6)) {
        String[] p = new String[]{"Love", "Fifteen", "Thirty", "Forty"};
        s = p[p1];
        return (p1 == p2) ? s + "-All" : s + "-" + p[p2];
    } else {
        if (p1 == p2)
            return "Deuce";
        s = p1 > p2 ? p1N : p2N;
        return ((p1-p2)*(p1-p2) == 1) ? "Advantage " + s : "Win for " + s;
    }
}

Не е лесно, нали? Ако се потрудите малко, в крайна сметка ще разберете, че променливата s, която има ключова роля в тази функция, се използва за съхраняване на показания резултат от тенис играта. Отново, с вашата IDE бърза победа е да извършите операция за преименуване:

public String getScore() {
    String displayScore;
    if (p1 < 4 && p2 < 4 && !(p1 + p2 == 6)) {
        String[] p = new String[]{"Love", "Fifteen", "Thirty", "Forty"};
        displayScore = p[p1];
        return (p1 == p2) ? displayScore + "-All" : displayScore + "-" + p[p2];
    } else {
        if (p1 == p2)
            return "Deuce";
        displayScore = p1 > p2 ? p1N : p2N;
        return ((p1-p2)*(p1-p2) == 1) ? "Advantage " + displayScore : "Win for " + displayScore;
    }
}

С един поглед бързо разбираме намерението на тази променлива.

Съвет за рефакторинг #2: Извлечете съдържанието на цикъла в отделен метод

Помислете за следния код (от ката Gilded Rose):

public void updateQuality() {
    for (Item item : items) {
        if (!item.name.equals("Aged Brie")
                && !item.name.equals("Backstage passes to a TAFKAL80ETC concert")) {
            if (item.quality > 0) {
                if (!item.name.equals("Sulfuras, Hand of Ragnaros")) {
                    item.quality = item.quality - 1;
                }
            }
        } else {
            if (item.quality < 50) {
                item.quality = item.quality + 1;
                // ... lots of code
            }
        }
        // ... more code
   }
}

Този код има няколко нива на отстъп, което го прави по-труден за четене и разбиране. Една бърза операция, поддържана от всички основни IDE, е функцията Метод за извличане. Дайте смислено име и ето го!

public void updateQuality() {
    for (Item item : items) {
        //The content has been extracted into a separate method
        updateQuality(item);
    }
}

private void updateQuality(Item item) {
    if (!item.name.equals("Aged Brie")
            && !item.name.equals("Backstage passes to a TAFKAL80ETC concert")) {
        if (item.quality > 0) {
            if (!item.name.equals("Sulfuras, Hand of Ragnaros")) {
                item.quality = item.quality - 1;
            }
        }
    } else {
        if (item.quality < 50) {
            item.quality = item.quality + 1;

            // ... lots of code
        }
    }

    // ... more code
}

Сега имаме два метода с отделни отговорности (преглед на колекцията и изчисляване на всеки елемент) и премахнато ниво на отстъп. Операциите на метода за извличане са често срещани при рефакторинг на кода.

Съвет за рефакторинг #3: Отделете булеви условия във вложено if

Това не е непременно валидно във всички контексти, но в някои случаи може да помогне да направите кода по-четлив и да помогне за разбирането на дървото на решенията на кода. Трябва да гледате на тази практика като на междинна стъпка по време на вашия рефакторинг. Разгледайте следния код:

public void updateQuality() {
    for (Item item : items) {
        if (!item.name.equals("Aged Brie")
                && !item.name.equals("Backstage passes to a TAFKAL80ETC concert")) {
            if (item.quality > 0) {
                if (!item.name.equals("Sulfuras, Hand of Ragnaros")) {
                    item.quality = item.quality - 1;
                }
            }
        } else {
            // ... else branch
        }

       //more code...
    }
}

Ако разрушим условията в оператора if, получаваме този резултат:

public void updateQuality() {
    for (Item item : items) {
      if (!item.name.equals("Aged Brie")) {
          if (!item.name.equals("Backstage passes to a TAFKAL80ETC concert")) {
              if (item.quality > 0) {
                  if (!item.name.equals("Sulfuras, Hand of Ragnaros")) {
                      item.quality = item.quality - 1;
                  }
              }
          } else {
              // ... else branch
          }
      } else {
          // ... else branch
      }
    //more code...
    }
}

Вижте намерението? Можете да видите, че кодът в … else клон наистина е дублиран, но може да се съгласите, че условията са по-лесни за четене сега. След като сте по-уверени с този код, например, ако успеете да напишете модулни тестове, които покриват различните случаи, ще можете да правите нови промени в кода си.

Съвет за рефакторинг #4: Пренапишете отрицателните условия

Подобно на Съвет #3, това е междинна стъпка. Твърде много условия могат да увеличат умственото натоварване, необходимо за разбиране на код. Един съвет може да бъде да премахнете отрицанията, което означава да обърнете следния код:

if (!item.name.equals("Sulfuras, Hand of Ragnaros")) {
    item.sellIn = item.sellIn - 1;
}

В този:

if (item.name.equals("Sulfuras, Hand of Ragnaros")) {
} else {
    item.sellIn = item.sellIn - 1;
}

Въпреки че първото условие няма инструкция, то все пак подобрява четенето на този код.

Съвет за рефакторинг #5: Капсулирайте статична зависимост

Този случай обикновено се случва при използването на модела Singleton. Има 2 проблема с singleton: 1) не можете да го замените 2) споделеното му състояние може да се промени от изпълнение на различни модулни тестове, което ги прави невъзпроизводими в зависимост от техния ред на изпълнение. Идеята тук е да извлечем извикването на зависимостта в метод, така че да можем да отменим поведението по подразбиране в подклас по време на нашите модулни тестове.

Като пример тук:

public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
		List<Trip> tripList = new ArrayList<Trip>();
    //The following line calls the static dependency
		User loggedUser = UserSession.getInstance().getLoggedUser();
		boolean isFriend = false;
		if (loggedUser != null) {
			for (User friend : user.getFriends()) {
				if (friend.equals(loggedUser)) {
					isFriend = true;
					break;
				}
			}
			if (isFriend) {
				tripList = TripDAO.findTripsByUser(user);
			}
			return tripList;
		} else {
			throw new UserNotLoggedInException();
    }
}

Това може да се превърне в следното:

public List<Trip> getTripsByUser(User user) throws UserNotLoggedInException {
		List<Trip> tripList = new ArrayList<Trip>();
    //We don't directly depend upon a static dependency now.
		User loggedUser = getLoggedUser();
		boolean isFriend = false;
		if (loggedUser != null) {
			for (User friend : user.getFriends()) {
				if (friend.equals(loggedUser)) {
					isFriend = true;
					break;
				}
			}
			if (isFriend) {
				tripList = TripDAO.findTripsByUser(user);
			}
			return tripList;
		} else {
			throw new UserNotLoggedInException();
		}
}

User getLoggedUser() {
	return UserSession.getInstance().getLoggedUser();
}

След това методът getLoggedUser ще бъде заменен, ако е необходимо, в нашия тестов пакет.

Съвет #6 за рефакторинг: Върнете се по-рано, за да намалите сложността

Метод, съдържащ множество условия if и изходи (връщане или хвърлени изключения), може да има сложно дърво на решенията с няколко нива на отстъп. Предложението за преработване на кода тук е да превърнете условията в тяхната отрицателна форма, за да напуснете метода възможно най-скоро. Мислете като подход за Бърз провал. Ето по-долу илюстрация с метод, съдържащ четири изходни точки:

public decimal Convert(decimal amount, string sourceCurrency, string targetCurrency) {
    if (currencyVerifier.Verify(sourceCurrency))
    {
        if (currencyVerifier.Verify(targetCurrency))
        {
            if(amount < 0)
            {
                throw new InvalidOperationException(); //Exit 1
            }

            decimal conversionRate = _rates.GetRateOf(sourceCurrency, targetCurrency);
            if (sourceCurrency.Equals(targetCurrency))
            {
                return amount; //Exit 2
            }

            logger.Log(DateTime.Now, sourceCurrency, targetCurrency, conversionRate);
            var convertedValue = amount * conversionRate;
            return convertedValue; //Exit 3
        }
    }

    throw new InvalidOperationException(); //Exit 4
}

Можем да го превърнем в следния код, който съдържа четири сплескани условия if, намалявайки когнитивната сложност на разбирането на този код:

public decimal Convert(decimal amount, string sourceCurrency, string targetCurrency) {
        if (!currencyVerifier.Verify(sourceCurrency))
        {
            throw new InvalidOperationException();
        }

        if (!currencyVerifier.Verify(targetCurrency))
        {
            throw new InvalidOperationException();
        }

        if (amount < 0)
        {
            throw new InvalidOperationException();
        }

        decimal conversionRate = _rates.GetRateOf(sourceCurrency, targetCurrency);
        if (sourceCurrency.Equals(targetCurrency))
        {
            return amount;
        }

        logger.Log(DateTime.Now, sourceCurrency, targetCurrency, conversionRate);
        var convertedValue = amount * conversionRate;
        return convertedValue;
    }
}

Може да видите, че това е контраинтуитивно, ако разгледаме Съвет №4. Не забравяйте, че най-важното е да останете прагматични, в зависимост от контекста на вашия код. Рефакторингът е за подобряване на поддръжката на изходния код, така че е вероятно да преминете през различни междинни стъпки; доверете се на себе си, за да изберете този, който ви помага най-много.

Съвет #7 за рефакторинг: Декларирайте променливите на най-близките места на тяхното използване

Лесен трик за подобряване на четливостта на изходния код е да се уверите, че не декларирате променлива и не я използвате няколко реда след това. Четенето на код, който не следва тази практика, ни позволява да мислим, че давате неуместна информация, след това правите нещо друго и накрая използвате предишната информация, тъй като имате нужда от нея сега.

В този пример няма нужда да декларирате conversionRate толкова рано:

decimal conversionRate = _rates.GetRateOf(sourceCurrency, targetCurrency);
if (sourceCurrency.Equals(targetCurrency))
{
    return amount;
}

logger.Log(DateTime.Now, sourceCurrency, targetCurrency, conversionRate);
var convertedValue = amount * conversionRate;

И така, докато чета първото условие, съм объркан относно ролята на тази променлива. Вместо това го декларирайте точно преди да го използвате:

if (sourceCurrency.Equals(targetCurrency))
{
    return amount;
}

decimal conversionRate = _rates.GetRateOf(sourceCurrency, targetCurrency);
logger.Log(DateTime.Now, sourceCurrency, targetCurrency, conversionRate);
var convertedValue = amount * conversionRate;
return convertedValue;

Пълната история е по-лесна за четене и можете да продължите да преработвате кода.

Съвет за рефакторинг #8: Капсулирайте примитивните типове в бизнес типове

Типовете бизнес улесняват изразяването на намерение и логика, като същевременно осигуряват валидиране. В следния код трябва да направим предположения за това кои са трите входни параметъра (въпреки ясното наименуване):

public decimal Convert(decimal amount, string sourceCurrency, string targetCurrency)
{
    if (amount < 0)
    {
       throw new InvalidOperationException();
    }
    //...
}

Но можете да видите, че бизнес логиката с условието сума ‹ 0 е разпръсната в кода и не трябва да е там. Вместо това можем да предложим следния код, въвеждащ класовете Amount и Currency, който позволява капсулиране на предишното условие в метод isNegative().

public decimal Convert(Amount amount, Currency sourceCurrency, Currency targetCurrency)
{
    if (amount.isNegative())
    {
       throw new InvalidOperationException();
    }
    //...
}

Искате ли да популяризирате вашите практики за рефакторинг?

Във вашия контекст със сигурност има още най-добри практики за кодиране, които бихте искали да споделите и съберете с екипа си, не само относно преработването, но и относно архитектурата, сигурността, езика за програмиране и дори повече.

Promyze се интегрира с вашето IDE, за да ви помогне да заснемете модификациите на изходния код, които илюстрират най-добрата практика, която да следвате или не. След това можете да споделите своя опит с вашия Promyze и да го прегледате по време на специален семинар с вашия екип.

Когато преработвате кода си, можете да оцените операциите си с изходния код и да предоставите своя опит на разработчиците във вашия проект и организация.

Готови ли сте да преработите кода, върху който работите?

Обединихме тези практики в каталог, достъпен в обществения център за най-добри практики за кодиране, поддържан от Promyze.

И накрая, ако търсите повече техники за рефакторинг, можете да проверите специалния раздел на отличния уебсайт Refactoring.guru; повече от 50 бяха налични, когато написахме тази публикация.