Статьи

Запахи кода — часть II

В последнем посте « Запахи кода — часть I» я говорил о раздувах: они представляют собой запахи кода, которые можно идентифицировать как длинные методы, большие классы, примитивные навязчивые идеи, длинный список параметров и скопления данных. В этом я хотел бы углубиться в объектно-ориентированных нарушителей и предотвращающих изменения .

Нарушители объектно-ориентированных

Этот тип запаха кода обычно возникает, когда объектно-ориентированные принципы являются неполными или неправильно применяются.

Переключение операторов

Этот случай легко идентифицировать: у нас есть случай переключения. Но вы также должны считать это запахом, если найдете последовательность ifs. (это скрытый случай переключения). Почему заявления о переключении плохо? Потому что, когда добавляется новое условие, вы должны найти каждое вхождение этого случая переключения. Поэтому, разговаривая с Дэвидом , он спросил меня: а что произойдет, если я включу коммутатор в метод, тогда это приемлемо? Это действительно хороший вопрос… Если ваш случай переключения используется только для «заботы» об одном поведении и все, тогда это может быть хорошо. Помните, что идентификация запаха кода не означает, что вы всегда должны его использовать: это компромисс. Если вы обнаружите, что ваш оператор switch реплицирован, и каждая репликация имеет свое поведение, вы не можете просто изолировать оператор switch в методе. Вам нужно найти подходящий «дом», в котором он будет находиться. Как правило, вы должны думать о полиморфизме, когда окажетесь в этой ситуации. Здесь мы можем применить два метода рефакторинга:

  • Замена кода типа на подклассы Этот метод состоит в создании подклассов для каждого случая переключения и применении соответствующего поведения к этим подклассам.
  • Замените код типа стратегией Как и в предыдущем примере, в этом случае вы должны использовать один из шаблонов: состояние или стратегия .

Так, когда использовать один или другой? Если Код Типа не меняет поведение класса, вы можете использовать технику Подклассов . Разделение каждого поведения на соответствующий подкласс обеспечит соблюдение принципа единой ответственности и сделает код в целом более читабельным. Если вам нужно добавить еще один случай, вы просто добавляете новый класс в свой код без необходимости изменения какого-либо другого кода. Таким образом, вы применяете принцип открытия / закрытия . Вы должны использовать стратегический подход, когда код типа влияет на поведение ваших классов. Если вы изменяете состояние класса, полей и многих других действий, вам следует использовать шаблон состояния . если это влияет только на то, как вы выбираете поведение класса, тогда Стратегия лучше подходит.

Хм … Это немного сбивает с толку, нет? Итак, давайте попробуем с примером.

У вас есть перечисление EmployeeType:

1
2
3
4
5
6
public enum EmployeeType
{
       
    Worker,
     
    Supervisor,
     
    Manager
 
}

И класс Сотрудник:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Employee

{
   
    private float salary;
   
    private float bonusPercentage;
   
    private EmployeeType employeeType;

   
 
    public Employee(float salary, float bonusPercentage, EmployeeType employeeType)
   
    {
       
        this.salary = salary;
       
        this.bonusPercentage = bonusPercentage;
       
        this.employeeType = employeeType;
   
    }

   
 
    public float CalculateSalary() 
   
    {
       
        switch (employeeType) 
       
        {
           
            case EmployeeType.Worker:
               
                return salary; 
           
            case EmployeeType.Supervisor:
               
                return salary + (bonusPercentage * 0.5F);
           
            case EmployeeType.Manager:
               
                return salary + (bonusPercentage * 0.7F);
       
        }
 
        return 0.0F;
   
    }
}

Все выглядит хорошо. Но что произойдет, если вам нужно рассчитать годовой бонус? Вы добавите другой метод, подобный этому:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public float CalculateYearBonus() 

    {
   
        switch (employeeType) 
   
        {
       
            case EmployeeType.Worker:
           
                return 0; 
       
            case EmployeeType.Supervisor:
           
                return salary + salary * 0.7F;
       
            case EmployeeType.Manager:
           
                return salary + salary * 1.0F;  
   
        }
 
        return 0.0F;
    }

Видите повторение выключателя? Итак, давайте сначала попробуем подход подкласса: Вот суперкласс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
abstract public class Employee

{
 
    

protected float salary;
   
    protected float bonusPercentage;

   
 
    public EmployeeFinal(float salary, float bonusPercentage)
   
    {
       
        this.salary = salary;
       
        this.bonusPercentage = bonusPercentage;
   
    }

   
 
    abstract public float CalculateSalary();
 
    

virtual public float CalculateYearBonus() 
   
    {

        return 0.0F;
   
    }

}

И здесь у нас есть подклассы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Worker: Employee

{
   two
 
    public Worker(float salary, float bonusPercentage)
 
        : base(salary, bonusPercentage)

    {}

 
 
     override public float CalculateSalary() 
   
     {
       
        return salary; 
   
     }

}
 
public class Supervisor : Employee

{
   
 
    public Supervisor(float salary, float bonusPercentage)

            : base(salary, bonusPercentage)
   
    {}

   
 
    override public float CalculateSalary() 
   
    {
       
        return salary + (bonusPercentage * 0.5F);
   
    }

   
 
    public override float CalculateYearBonus()
   
    {
       
        return salary + salary * 0.7F;
   
    }

}

При использовании Стратегического подхода мы создали бы интерфейс для расчета возмездия:

1
2
3
4
5
public interface IRetributionCalculator 

{
       
    float CalculateSalary(float salary);
    
    float CalculateYearBonus(float salary);
 
}

Имея интерфейс, мы можем теперь передать сотруднику любой класс, соответствующий этому протоколу, и рассчитать правильную зарплату / бонус.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class Employee
{
   
    private float salary;
   
    private IRetributionCalculator retributionCalculator;

   
 
    public Employee(float salary, IRetributionCalculator retributionCalculator)
   
    {
        this.salary = salary;
       
        this.retributionCalculator = retributionCalculator;
   
    }

   
 
    public float CalculateSalary()
   
    {
       
        return retributionCalculator.CalculateSalary(salary);
   
    }
            
   
 
    public float CalculateYearBonus() 
   
    {
       
        return retributionCalculator.CalculateYearBonus(salary);
   
    }
}

Временное поле

Этот случай возникает, когда мы вычисляем какой-то большой алгоритм, которому нужно несколько входных переменных. Создание этих полей в классе в большинстве случаев не имеет значения, поскольку они просто используются для этого конкретного вычисления. И это тоже может быть опасно, потому что вы должны быть уверены, что повторно инициализируете их, прежде чем начинать следующее вычисление. Здесь лучший метод рефакторинга — использовать метод Replace Method with Method Object , который извлечет метод в отдельный класс. Затем вы можете разделить метод на несколько методов в одном классе.

Отказался от завещания

Этот запах кода немного сложно обнаружить, потому что это происходит, когда подкласс не использует все поведения своего родительского класса. Так что, как будто подкласс «отказывается» от некоторых поведений («завещать») своего родительского класса.

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

Когда наследование будет правильным, тогда переместите все ненужные поля и методы из подкласса. Извлеките все методы и поля из подкласса и родительского класса и поместите их в новый класс. Сделайте этот новый класс суперклассом, от которого должен наследоваться подкласс и родительский класс. Эта методика называется Extract Superclass .

Альтернативные классы с разными интерфейсами

Хм, этот случай заставляет меня думать об «отсутствии связи» между членами одной команды, потому что это происходит, когда у нас есть два класса, которые делают одно и то же, но имеют разные имена для своих методов. Начните с переименования методов или метода перемещения , чтобы оба класса могли реализовывать один и тот же интерфейс. В некоторых случаях только часть поведения дублируется в обоих классах. Если это так, попробуйте извлечь суперкласс и сделать исходные классы подклассами.

Изменить Профилактики

О, парень! Такого рода запахи кода вы действительно хотите избежать. Это те, которые, когда вы вносите изменения в одном месте, вы должны проходить в основном по всей своей кодовой базе, внося изменения и в других местах. Так что это кошмар, которого все мы хотим избежать!

Дивергентное изменение

Это тот случай, когда вы меняете один и тот же класс по нескольким причинам. Это означает, что вы нарушаете принцип единой ответственности ) (что связано с разделением интересов). Применяемая здесь техника рефакторинга — это Extract Class, поскольку вы хотите извлечь различные варианты поведения в разные классы.

Дробовик Хирургия

Это означает, что когда вы вносите небольшое изменение в класс, вы должны пойти и изменить несколько классов одновременно. Несмотря на то, что они кажутся такими же, как запах расходящихся изменений , в действительности они противоположны друг другу: Дивергентные изменения — это когда много изменений вносятся в один класс. Хирургия дробовика относится к тому, когда одно изменение сделано к нескольким классам одновременно.

Здесь применяется метод рефакторинга — метод перемещения и / или поле перемещения . Это позволит вам переместить дублированные методы или поля в общий класс. Если этот класс не существует, создайте новый. В случае, если оригинальный класс остается почти пустым, возможно, вам следует подумать, является ли этот класс избыточным, и если это так, избавьтесь от него с помощью встроенного класса : переместите оставшиеся методы / поля в один из созданных новых классов. Это все зависит от того, не несет ли первоначальный класс ответственности больше.

Параллельное наследование иерархий

В этом случае вы создаете новый подкласс для класса B, потому что вы добавляете подкласс в класс A. Здесь вы можете: во-первых, заставить одну из иерархий ссылаться на экземпляры другой иерархии. После этого первого шага вы можете использовать метод Move Method и Move Field, чтобы удалить иерархию в указанном классе. Вы можете применить здесь шаблон посетителя тоже.

Вывод

В случае объектно-ориентированных нарушителей и средств предотвращения изменений , я думаю, что их проще избежать, если вы знаете, как применить хороший дизайн к своему коду. И это приходит с большой практикой. Сегодня я рассказал о нескольких методах рефакторинга, но их гораздо больше. Вы можете найти хорошую ссылку на все это в Refactoring.com . И, как я сказал в первой части этой серии , запахи кода не всегда могут быть удалены. Изучите каждый случай и решите: помните, что это всегда компромисс.

Ссылка: Запахи кода — часть II от нашего партнера JCG Ана Ногал в блоге Crafted Software .