Упражнение: разхвърлян клас, изчистен стъпка по стъпка
Накратко: Упражнение по refactoring: клас за наем на тротинетки се изчиства на пет малки стъпки (имена, магически числа, пари, въпрос отделно от командата, планове), а тестовете остават зелени.
Проблем
Тротинетките, които никой не смее да пипне
Фирма за тротинетки под наем таксува всяко возене с класа RideMgr. Работи от три години. Методът Calc смята цената по плана на клиента (на минута, студент или месечна карта), маха таксата за отключване от четвъртото возене за деня, слага нощна добавка, трупа оборота и праща бележка:
public class RideMgr{ private readonly Dictionary<string, int> c = new(); public double S { get; private set; } public List<string> Out { get; } = new();
// Цената: 1 € за отключване и 0,15 € на минута. // Нощем цената се вдига с 50%. public double Calc(string to, string t, int m, bool f) { c[to] = c.GetValueOrDefault(to) + 1; double p = 0; if (t == "payg") { p = 1.0 + m * 0.2; } else if (t == "student") { p = (1.0 + m * 0.2) * 0.8; } else if (t == "monthly") { if (m > 45) { p = (m - 45) * 0.2; } } if (c[to] > 3 && t != "monthly") { p = p - 1.0; } if (f) { p = p + 0.5; } S = S + p; Out.Add($"{to}: {m} мин, {p:F2} €"); return p; }}Следващия месец идват две промени:
- фирмен план: без такса за отключване, 0,18 € на минута, без отстъпка за четвъртото возене;
- цената преди края: приложението показва колко ще струва возенето, докато то още тече.
Втората не става с този клас. Ето какво се случва, ако приложението извика Calc, за да покаже цената, и после пак, за да вземе парите:
var rides = new RideMgr();double shown = rides.Calc("ana@example.com", "payg", 10, false);Console.WriteLine($"Цена преди края: {shown:F2} €");double charged = rides.Calc("ana@example.com", "payg", 10, false);Console.WriteLine($"Взето: {charged:F2} €");Console.WriteLine($"Бележки: {rides.Out.Count}");Console.WriteLine($"Оборот: {rides.S:F2} €");Цена преди края: 3.00 €Взето: 3.00 €Бележки: 2Оборот: 6.00 €Едно возене, две бележки и 6 € оборот. Calc звучи като въпрос, а брои, трупа и праща.
В урока няма нова идея. Той е упражнение: класът минава през инструментите от модула един по един, на малки стъпки, а след всяка стъпка пускаш тестовете.
Виж го
Стъпка 0: мрежата
Първо мрежата, както в урока за тестовете. Девет теста заковават какво прави класът днес: цената по всеки план, нощта, четвъртото возене, бележката и оборота.
using Xunit;
public class RideMgrTest{ private const string Email = "ana@example.com"; private readonly RideMgr rides = new();
private string Ride(string plan, int minutes, bool night) { return Money(rides.Calc(Email, plan, minutes, night)); }
private static string Money(double amount) { return amount.ToString("F2"); }
[Fact] public void PayAsYouGoRide() { Assert.Equal("3.00", Ride("payg", 10, false)); }
[Fact] public void StudentRide() { Assert.Equal("2.40", Ride("student", 10, false)); }
[Fact] public void MonthlyWithinFreeMinutes() { Assert.Equal("0.00", Ride("monthly", 30, false)); }
[Fact] public void MonthlyOverFreeMinutes() { Assert.Equal("3.00", Ride("monthly", 60, false)); }
[Fact] public void NightSurcharge() { Assert.Equal("3.50", Ride("payg", 10, true)); }
[Fact] public void FourthRideUnlockIsFree() { Ride("payg", 10, false); Ride("payg", 10, false); Ride("payg", 10, false); Assert.Equal("2.00", Ride("payg", 10, false)); }
[Fact] public void StudentFourthRide() { // Така е днес: отстъпката е целият 1 €, а не 0,80 €. Ride("student", 10, false); Ride("student", 10, false); Ride("student", 10, false); Assert.Equal("1.40", Ride("student", 10, false)); }
[Fact] public void Receipt() { Ride("payg", 10, false); Assert.Equal(new[] { "ana@example.com: 10 мин, 3.00 €" }, rides.Out); }
[Fact] public void Revenue() { Ride("payg", 10, false); Ride("student", 10, false); Assert.Equal("5.40", Money(rides.S)); }}ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Тестовете викат класа само през помощника Ride. Когато в следващите стъпки формата на класа се смени, ще се сменят той и извикванията, а очакваните стойности никога.
Единият тест е странен. Студентът плаща 80% от цената, значи и отключването му струва 0,80 €. На четвъртото возене обаче класът маха целия 1 €, и студентът плаща 1,40 € вместо 1,60 €. Грешка ли е, или подарък? Не решаваш ти и не решаваш сега. Тестът заковава какво е днес, с коментар, а въпросът отива при собственика на продукта. Докато чистиш, поведението не се сменя, дори когато изглежда грешно.
По-точно, за любопитните
Сумите се сравняват като текст с две цифри ("2.40"), както ги вижда клиентът. Сравнение с число щеше да се препъне в малките двоични грешки на double:
Console.WriteLine((1.0 + 10 * 0.2) * 0.8);2.4000000000000004А в стъпка 3 типът на сумите ще се смени. Текстът с две цифри е един и същ преди и след.
Виж го
Стъпка 1: имената и флагът
t, m, f, p, c и s стават думи (имената). Най-важното име е на метода: той не смята, а приключва возенето. Честното име е FinishRide, и страничният ефект вече се вижда от името. Класът става RideBilling.
public class RideBilling{ private readonly Dictionary<string, int> ridesToday = new(); public double Revenue { get; private set; } public List<string> Receipts { get; } = new();
// Цената: 1 € за отключване и 0,15 € на минута. // Нощем цената се вдига с 50%. public double FinishRide(string email, string plan, int minutes, bool night = false) { ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; double price = 0; if (plan == "payg") { price = 1.0 + minutes * 0.2; } else if (plan == "student") { price = (1.0 + minutes * 0.2) * 0.8; } else if (plan == "monthly") { if (minutes > 45) { price = (minutes - 45) * 0.2; } } if (ridesToday[email] > 3 && plan != "monthly") { price = price - 1.0; } if (night) { price = price + 0.5; } Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price; }}Остава флагът (функциите). true при извикването не казва нищо. Нощта е данни за возенето, а не избор на друга работа, затова не става две функции. Извикването трябва да казва какво значи:
night: true.private string Ride(string plan, int minutes, bool night){ return Money(rides.Calc(Email, plan, minutes, night));}
[Fact]public void NightSurcharge(){ Assert.Equal("3.50", Ride("payg", 10, true));}private string Ride(string plan, int minutes, bool night = false){ return Money(billing.FinishRide(Email, plan, minutes, night));}
[Fact]public void NightSurcharge(){ Assert.Equal("3.50", Ride("payg", 10, night: true));}ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Виж го
Стъпка 2: числата и коментарът, който лъже
1.0, 0.2, 0.8, 45, 3 и 0.5 стават константи с имена (магическо число → константа).
Коментарът над метода казва 0,15 € на минута и 50% нощна надценка. Кодът взима 0,20 € и добавя 0,50 €. Кое е вярно? Тестовете заковаха какво прави кодът, а клиентите плащат по него от години. Коментарът лъже (коментарите) и се трие. Константите казват същото и не могат да остареят отделно от кода.
public class RideBilling{ private const double UnlockFee = 1.00; private const double PerMinute = 0.20; private const double StudentShare = 0.80; private const int MonthlyFreeMinutes = 45; private const int PaidUnlocksPerDay = 3; private const double NightSurcharge = 0.50;
private readonly Dictionary<string, int> ridesToday = new(); public double Revenue { get; private set; } public List<string> Receipts { get; } = new();
public double FinishRide(string email, string plan, int minutes, bool night = false) { ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; double price = 0; if (plan == "payg") { price = UnlockFee + minutes * PerMinute; } else if (plan == "student") { price = (UnlockFee + minutes * PerMinute) * StudentShare; } else if (plan == "monthly") { if (minutes > MonthlyFreeMinutes) { price = (minutes - MonthlyFreeMinutes) * PerMinute; } } if (ridesToday[email] > PaidUnlocksPerDay && plan != "monthly") { price = price - UnlockFee; } if (night) { price = price + NightSurcharge; } Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price; }}ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Тестовете не се пипат: нито името, нито извикванията са се сменили.
Пробвай
Намери грешката
Колега прави същата стъпка, но вярва на коментара и го оставя:
Избери реда с грешката.
Цената на минута е взета от коментара: 0,15 € вместо 0,20 €. Така refactoring тихо сваля цената на всяко возене. Осем от деветте теста падат, минава само месечната карта до 45 минути, където минутите не се плащат. Коментарът трябваше да се изтрие, а числото да се вземе от кода, който тестовете вече пазят.
По-точно, за любопитните
ОК MonthlyWithinFreeMinutesПАДА FourthRideUnlockIsFreeПАДА MonthlyOverFreeMinutesПАДА NightSurchargeПАДА PayAsYouGoRideПАДА ReceiptПАДА RevenueПАДА StudentFourthRideПАДА StudentRideТестове: 9, минали: 1, паднали: 8Виж го
Стъпка 3: парите
Парите в double са примитиви вместо типове (миризмите). Днес това не дава грешен цент в нито един тест, защото всяка сума се закръгля чак при печатане. Затова стъпката е refactoring: изходът е същият. Но оборотът е сбор от хиляди такива суми, и там малките грешки се трупат.
double става decimal, а литералите получават m. Формулите не се пипат, защото decimal има същите оператори. В курса парите в C# бяха decimal от началото, но този клас е стар и е писан с double.public class RideBilling{ private const decimal UnlockFee = 1.00m; private const decimal PerMinute = 0.20m; private const decimal StudentShare = 0.80m; private const int MonthlyFreeMinutes = 45; private const int PaidUnlocksPerDay = 3; private const decimal NightSurcharge = 0.50m;
private readonly Dictionary<string, int> ridesToday = new(); public decimal Revenue { get; private set; } public List<string> Receipts { get; } = new();
public decimal FinishRide(string email, string plan, int minutes, bool night = false) { ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; decimal price = 0; if (plan == "payg") { price = UnlockFee + minutes * PerMinute; } else if (plan == "student") { price = (UnlockFee + minutes * PerMinute) * StudentShare; } else if (plan == "monthly") { if (minutes > MonthlyFreeMinutes) { price = (minutes - MonthlyFreeMinutes) * PerMinute; } } if (ridesToday[email] > PaidUnlocksPerDay && plan != "monthly") { price = price - UnlockFee; } if (night) { price = price + NightSurcharge; } Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price; }}private static string Money(decimal amount){ return amount.ToString("F2");}ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Пробвай
Попълни
Цената на минута в типа за пари на езика:
private const decimal PerMinute = ;m прави литерала decimal. Без него 0.20 е double и не се присвоява на decimal без изрично преобразуване.Виж го
Стъпка 4: въпросът отделно от командата
Сега промяната, която чакаше: цената преди края трябва да е въпрос, който не сменя нищо (команда или въпрос). Изчисляването се изважда в метод Price (Extract Method от урока за refactoring). FinishRide остава команда: пита за цената, после брои, трупа и праща.
Колега го прави така. Броячът отива след цената, защото возенето се брои, когато е приключило:
Какво казват деветте теста?
public decimal Price(string email, string plan, int minutes, bool night = false){ decimal price = 0; if (plan == "payg") { price = UnlockFee + minutes * PerMinute; } else if (plan == "student") { price = (UnlockFee + minutes * PerMinute) * StudentShare; } else if (plan == "monthly") { if (minutes > MonthlyFreeMinutes) { price = (minutes - MonthlyFreeMinutes) * PerMinute; } } if (ridesToday.GetValueOrDefault(email) > PaidUnlocksPerDay && plan != "monthly") { price = price - UnlockFee; } if (night) { price = price + NightSurcharge; } return price;}
public decimal FinishRide(string email, string plan, int minutes, bool night = false){ decimal price = Price(email, plan, minutes, night); ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price;}В стария Calc броячът растеше преди проверката, затова „повече от 3“ значеше „това е четвъртото возене“. Сега Price пита преди броенето и броячът е с едно по-малко. Четвъртото возене минава за трето и плаща отключването. Другите тестове минават, защото не стигат до четвърто возене.
По-точно, за любопитните
ОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentRideПАДА FourthRideUnlockIsFree Assert.Equal() Failure: Strings differ ↓ (pos 0) Expected: "2.00" Actual: "3.00" ↑ (pos 0)ПАДА StudentFourthRide Assert.Equal() Failure: Strings differ ↓ (pos 0) Expected: "1.40" Actual: "2.40" ↑ (pos 0)Тестове: 9, минали: 7, паднали: 2Виж го
Стъпка 4, втори опит
Червеното сочи последната стъпка. Не търсиш грешката в целия клас: връщаш стъпката и я правиш пак. Редът на страничните ефекти беше скрита част от поведението, и знаеха за нея само тестовете.
Price смята кое поред ще е возенето:
public decimal Price(string email, string plan, int minutes, bool night = false){ int rideOfDay = ridesToday.GetValueOrDefault(email) + 1; decimal price = 0; if (plan == "payg") { price = UnlockFee + minutes * PerMinute; } else if (plan == "student") { price = (UnlockFee + minutes * PerMinute) * StudentShare; } else if (plan == "monthly") { if (minutes > MonthlyFreeMinutes) { price = (minutes - MonthlyFreeMinutes) * PerMinute; } } if (rideOfDay > PaidUnlocksPerDay && plan != "monthly") { price = price - UnlockFee; } if (night) { price = price + NightSurcharge; } return price;}
public decimal FinishRide(string email, string plan, int minutes, bool night = false){ decimal price = Price(email, plan, minutes, night); ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price;}ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Тестовете пак не се пипат: те викат FinishRide, а то прави същото като преди.
Целият клас след стъпка 4
public class RideBilling{ private const decimal UnlockFee = 1.00m; private const decimal PerMinute = 0.20m; private const decimal StudentShare = 0.80m; private const int MonthlyFreeMinutes = 45; private const int PaidUnlocksPerDay = 3; private const decimal NightSurcharge = 0.50m;
private readonly Dictionary<string, int> ridesToday = new(); public decimal Revenue { get; private set; } public List<string> Receipts { get; } = new();
public decimal Price(string email, string plan, int minutes, bool night = false) { int rideOfDay = ridesToday.GetValueOrDefault(email) + 1; decimal price = 0; if (plan == "payg") { price = UnlockFee + minutes * PerMinute; } else if (plan == "student") { price = (UnlockFee + minutes * PerMinute) * StudentShare; } else if (plan == "monthly") { if (minutes > MonthlyFreeMinutes) { price = (minutes - MonthlyFreeMinutes) * PerMinute; } } if (rideOfDay > PaidUnlocksPerDay && plan != "monthly") { price = price - UnlockFee; } if (night) { price = price + NightSurcharge; } return price; }
public decimal FinishRide(string email, string plan, int minutes, bool night = false) { decimal price = Price(email, plan, minutes, night); ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price; }}Виж го
Стъпка 5: плановете
Остава веригата от if по текста на плана (дълга верига от if). Всеки нов план е нов клон, а "monthly" се проверява на две места. Всеки план става клас, който знае цената си и отстъпката за четвъртото возене. Това е Replace Conditional with Polymorphism от урока за refactoring:
public interface IPlan{ decimal Price(int minutes);
decimal LoyaltyDiscount();}
public class PayAsYouGo : IPlan{ public decimal Price(int minutes) { return RideBilling.UnlockFee + minutes * RideBilling.PerMinute; }
public decimal LoyaltyDiscount() { return RideBilling.UnlockFee; }}
public class Student : IPlan{ private const decimal StudentShare = 0.80m;
public decimal Price(int minutes) { return new PayAsYouGo().Price(minutes) * StudentShare; }
public decimal LoyaltyDiscount() { // Така е днес: целият 1 €, макар че отключването на студента струва 0,80 €. return RideBilling.UnlockFee; }}
public class Monthly : IPlan{ private const int FreeMinutes = 45;
public decimal Price(int minutes) { return Math.Max(0, minutes - FreeMinutes) * RideBilling.PerMinute; }
public decimal LoyaltyDiscount() { return 0; }}Проверката за месечната карта изчезва: тя просто връща 0 отстъпка. Странното поведение на студента вече е на един ред, с коментар, който казва защо е така (добрият коментар от урока за коментарите).
public class RideBilling{ public const decimal UnlockFee = 1.00m; public const decimal PerMinute = 0.20m; private const int PaidUnlocksPerDay = 3; private const decimal NightSurcharge = 0.50m;
private readonly Dictionary<string, int> ridesToday = new(); public decimal Revenue { get; private set; } public List<string> Receipts { get; } = new();
public decimal Price(string email, IPlan plan, int minutes, bool night = false) { int rideOfDay = ridesToday.GetValueOrDefault(email) + 1; decimal price = plan.Price(minutes); if (rideOfDay > PaidUnlocksPerDay) { price = price - plan.LoyaltyDiscount(); } if (night) { price = price + NightSurcharge; } return price; }
public decimal FinishRide(string email, IPlan plan, int minutes, bool night = false) { decimal price = Price(email, plan, minutes, night); ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; Revenue = Revenue + price; Receipts.Add($"{email}: {minutes} мин, {price:F2} €"); return price; }}private.В тестовете извикванията подават обект вместо текст:
Assert.Equal("3.50", Ride("payg", 10, night: true));Assert.Equal("3.50", Ride(new PayAsYouGo(), 10, night: true));ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Това е Strategy от модула за шаблоните: правилото е отделен обект, а таксуването само пита.
Виж го
Двете промени сега
Фирменият план е нов клас. Класът за таксуването не се пипа (отворено за разширение):
public class Business : IPlan{ private const decimal PerMinute = 0.18m;
public decimal Price(int minutes) { return minutes * PerMinute; }
public decimal LoyaltyDiscount() { return 0; }}Цената преди края е въпросът Price. Колкото пъти и да го викаш, нищо не се брои и не тръгва бележка:
var billing = new RideBilling();var payg = new PayAsYouGo();billing.FinishRide("ana@example.com", payg, 10);billing.FinishRide("ana@example.com", payg, 5);billing.FinishRide("ana@example.com", payg, 12, night: true);Console.WriteLine($"Цена преди края: {billing.Price("ana@example.com", payg, 8):F2} €");Console.WriteLine($"Цена преди края: {billing.Price("ana@example.com", payg, 8):F2} €");Console.WriteLine($"Взето: {billing.FinishRide("ana@example.com", payg, 8):F2} €");Console.WriteLine($"Фирмата, 12 мин: {billing.FinishRide("office@example.com", new Business(), 12):F2} €");Console.WriteLine($"Бележки: {billing.Receipts.Count}");Цена преди края: 1.60 €Цена преди края: 1.60 €Взето: 1.60 €Фирмата, 12 мин: 2.16 €Бележки: 5Пет бележки: три возения преди прегледа, четвъртото и това на фирмата. Двата прегледа не оставят следа, а показват точно колкото после се взима.
Идеята
Преди и след
| Миризма | Стъпка | Урок |
|---|---|---|
| нищо не пази поведението | 0: тестове, които заковават днешното | мрежата |
имена от една буква, calc, който не казва, че праща |
1: Rename | имената |
true при извикването |
1: аргумент, който казва какво значи | функциите |
| магически числа | 2: константи с имена | имената |
| коментар, който лъже | 2: изтрит | коментарите |
| пари в double | 3: тип за пари | миризмите |
| въпрос със скрит страничен ефект, дълъг метод | 4: Extract Method, команда и въпрос | функциите, refactoring |
верига от if по плана като текст |
5: Replace Conditional with Polymorphism | миризмите, refactoring, SOLID |
Класът стана по-дълъг, не по-къс. Но всяко правило е на едно място и с име, а промените от началото са по един клас.
Денят на возенията, пуснат с класа отпреди стъпка 1 и с класа след стъпка 5, печата едно и също ред по ред. Тук и двете програми се пускат при всяка проверка на сайта.
var rides = new RideMgr();rides.Calc("ana@example.com", "payg", 10, false);rides.Calc("ivan@example.com", "student", 25, false);rides.Calc("ana@example.com", "payg", 5, false);rides.Calc("maria@example.com", "monthly", 60, true);rides.Calc("ana@example.com", "payg", 12, true);rides.Calc("ana@example.com", "payg", 8, false);
foreach (string receipt in rides.Out){ Console.WriteLine(receipt);}Console.WriteLine($"Оборот: {rides.S:F2} €");var billing = new RideBilling();var payg = new PayAsYouGo();billing.FinishRide("ana@example.com", payg, 10);billing.FinishRide("ivan@example.com", new Student(), 25);billing.FinishRide("ana@example.com", payg, 5);billing.FinishRide("maria@example.com", new Monthly(), 60, night: true);billing.FinishRide("ana@example.com", payg, 12, night: true);billing.FinishRide("ana@example.com", payg, 8);
foreach (string receipt in billing.Receipts){ Console.WriteLine(receipt);}Console.WriteLine($"Оборот: {billing.Revenue:F2} €");ana@example.com: 10 мин, 3.00 €ivan@example.com: 25 мин, 4.80 €ana@example.com: 5 мин, 2.00 €maria@example.com: 60 мин, 3.50 €ana@example.com: 12 мин, 3.90 €ana@example.com: 8 мин, 1.60 €Оборот: 18.80 €Идеята
Къде да спреш
Спираш, когато промените, които идват, станат лесни. Тук двете промени са един клас и един ред. Затова някои стъпки съзнателно не са направени:
- бележката в свой клас. Текстът на бележката е втора причина класът да се сменя (една причина за промяна). Днес той не се сменя. Щом маркетингът започне да го пипа всеки месец, става отделен клас (задача 1);
- изпращането зад интерфейс. Бележките са списък, а не истински имейл. Когато дойде истинският имейл, тестът не бива да го праща, и тогава идва абстракцията (зависимост от абстракция);
- поведението на студента. То е промяна в поведението, не изчистване. Прави се отделно, със свой тест, когато продуктът реши.
И правилото от урока за refactoring: или сменяш формата, или поведението, никога и двете в една стъпка.
Упражнение
Продължи сам
Реши задачите в редактора си, като тръгнеш от класа след стъпка 5. След всяка стъпка пусни деветте теста.
1. Бележката в свой клас. Изнеси текста на бележката и списъка с изпратените в клас ReceiptBook. Класът за таксуването само му казва „изпрати“. Тестовете не се пипат.
Едно възможно решение
public class ReceiptBook{ public List<string> Sent { get; } = new();
public void Send(string email, int minutes, decimal price) { Sent.Add($"{email}: {minutes} мин, {price:F2} €"); }}public class RideBilling{ public const decimal UnlockFee = 1.00m; public const decimal PerMinute = 0.20m; private const int PaidUnlocksPerDay = 3; private const decimal NightSurcharge = 0.50m;
private readonly Dictionary<string, int> ridesToday = new(); public decimal Revenue { get; private set; } private readonly ReceiptBook receipts = new(); public List<string> Receipts => receipts.Sent;
public decimal Price(string email, IPlan plan, int minutes, bool night = false) { int rideOfDay = ridesToday.GetValueOrDefault(email) + 1; decimal price = plan.Price(minutes); if (rideOfDay > PaidUnlocksPerDay) { price = price - plan.LoyaltyDiscount(); } if (night) { price = price + NightSurcharge; } return price; }
public decimal FinishRide(string email, IPlan plan, int minutes, bool night = false) { decimal price = Price(email, plan, minutes, night); ridesToday[email] = ridesToday.GetValueOrDefault(email) + 1; Revenue = Revenue + price; receipts.Send(email, minutes, price); return price; }}ОК FourthRideUnlockIsFreeОК MonthlyOverFreeMinutesОК MonthlyWithinFreeMinutesОК NightSurchargeОК PayAsYouGoRideОК ReceiptОК RevenueОК StudentFourthRideОК StudentRideТестове: 9, минали: 9, паднали: 0Програмата за деня от картата „Преди и след“ печата с този клас същия изход.
2. Планът от текста. Планът идва от базата като текст ("payg"). Напиши таблицата, която го превръща в обект. Какво правеше старият клас с непознат план? Закови го с тест върху стария клас, преди да решиш какво да прави новият.
3. Нощта от часа. Приложението вече не знае кога е нощ, а подава времето на тръгване. Правилото „от 22:00 до 6:00“ трябва да живее на едно място. Напиши първо тестовете за 21:59, 22:00, 5:59 и 6:00.
4. Студентът. Продуктът реши: на четвъртото возене студентът получава 0,80 €, а не 1 €. Това е промяна в поведението: първо смени очакваното в теста и го виж червен, после смени един ред в плана.
Тест