Условные предложения иногда приводят к появлению кода, которым сложнее управлять. Это включает не только оператор if
, но даже чаще оператор switch
, который обычно включает больше ветвей, чем соответствующий if
.
В некоторых случаях вполне разумно использовать if
Когда вы пишете служебные методы, расширения или специальные библиотечные функции, вполне вероятно, что вы не сможете избежать if
s (и не должны). Нет лучшего способа закодировать эту маленькую функцию и сделать ее более документированной, чем она есть:
// this is a good "if" use-case
int Min(int a, int b)
{
if (a < b)
return a;
else
return b;
}
// or, if you prefer the ternary operator
int Min(int a, int b)
{
return (a < b) ? a : b;
}
Разветвление над "кодом типа" - это запах кода
С другой стороны, если вы сталкиваетесь с кодом, который проверяет какой-либо код типа или проверяет, является ли переменная определенного типа, то это, скорее всего, хороший кандидат на рефакторинг, а именно замена условный с полиморфизмом .
Причина этого заключается в том, что, позволяя вашим вызывающим абонентам переходить по определенному коду типа, вы создаете возможность в конечном итоге выполнять многочисленные проверки, разбросанные по всему коду, что значительно усложняет расширение и обслуживание. С другой стороны, полиморфизм позволяет максимально приблизить это решение о ветвлении к корню вашей программы.
Рассмотрим:
// this is called branching on a "type code",
// and screams for refactoring
void RunVehicle(Vehicle vehicle)
{
// how the hell do I even test this?
if (vehicle.Type == CAR)
Drive(vehicle);
else if (vehicle.Type == PLANE)
Fly(vehicle);
else
Sail(vehicle);
}
Размещая общие, но специфичные для типа (то есть специфичные для класса) функциональные возможности в отдельных классах и выставляя их через виртуальный метод (или интерфейс), вы позволяете внутренним частям вашей программы делегировать это решение кому-то, кто выше в иерархия вызовов (потенциально в одном месте в коде), позволяющая значительно упростить тестирование (макетирование), расширяемость и обслуживание:
// adding a new vehicle is gonna be a piece of cake
interface IVehicle
{
void Run();
}
// your method now doesn't care about which vehicle
// it got as a parameter
void RunVehicle(IVehicle vehicle)
{
vehicle.Run();
}
И теперь вы можете легко проверить, работает ли ваш RunVehicle
метод должным образом:
// you can now create test (mock) implementations
// since you're passing it as an interface
var mock = new Mock<IVehicle>();
// run the client method
something.RunVehicle(mock.Object);
// check if Run() was invoked
mock.Verify(m => m.Run(), Times.Once());
Шаблоны, отличающиеся только условиями if
, могут быть использованы повторно
Относительно аргумента о замене if
"предикатом" в вашем вопросе, Хейнс, вероятно, хотел упомянуть, что иногда в вашем коде существуют похожие шаблоны, которые отличаются только своими условными выражениями. Условные выражения появляются вместе с if
s, но вся идея состоит в том, чтобы выделить повторяющийся шаблон в отдельный метод, оставив выражение в качестве параметра. Это то, что LINQ уже делает, обычно , что приводит к более чистому коду по сравнению с альтернативным foreach
:
Рассмотрим два очень похожих метода:
// average male age
public double AverageMaleAge(List<Person> people)
{
double sum = 0.0;
int count = 0;
foreach (var person in people)
{
if (person.Gender == Gender.Male)
{
sum += person.Age;
count++;
}
}
return sum / count; // not checking for zero div. for simplicity
}
// average female age
public double AverageFemaleAge(List<Person> people)
{
double sum = 0.0;
int count = 0;
foreach (var person in people)
{
if (person.Gender == Gender.Female) // <-- only the expression
{ // is different
sum += person.Age;
count++;
}
}
return sum / count;
}
Это означает, что вы можете извлечь условие в предикат, оставив вам один метод для этих двух случаев (и многих других будущих случаев):
// average age for all people matched by the predicate
public double AverageAge(List<Person> people, Predicate<Person> match)
{
double sum = 0.0;
int count = 0;
foreach (var person in people)
{
if (match(person)) // <-- the decision to match
{ // is now delegated to callers
sum += person.Age;
count++;
}
}
return sum / count;
}
var males = AverageAge(people, p => p.Gender == Gender.Male);
var females = AverageAge(people, p => p.Gender == Gender.Female);
А поскольку в LINQ уже есть множество таких удобных методов расширения, вам даже не нужно писать свои собственные методы:
// replace everything we've written above with these two lines
var males = list.Where(p => p.Gender == Gender.Male).Average(p => p.Age);
var females = list.Where(p => p.Gender == Gender.Female).Average(p => p.Age);
В этой последней версии LINQ оператор if
полностью "исчез", хотя:
- Если честно, проблема была не в
if
сама по себе, а во всем шаблоне кода (просто потому, что он был дублирован), и
-
if
все еще фактически существует, но он написан внутри метода расширения LINQ Where
, который был протестирован и закрыт для модификации. Полезно иметь меньше собственного кода: меньше вещей для тестирования, меньше ошибок, а код проще отслеживать, анализировать и поддерживать.
Рефакторинг, когда вы чувствуете, что это запах кода, но не перегружайте
Сказав все это, вы не должны проводить бессонные ночи из-за того, что сейчас и там есть пара условий. Хотя эти ответы могут предоставить некоторые общие практические правила, лучший способ уметь обнаруживать конструкции, требующие рефакторинга, - это через опыт. Со временем появляются некоторые шаблоны, которые приводят к изменению одних и тех же предложений снова и снова.