Статьи

Менеджеры и индивидуальные вкладчики в коде

менеджер-individualcontributor
Мне повезло работать в компании, которая делает возможную хорошую техническую карьеру, так что мне не нужно становиться менеджером, чтобы получить больше денег.

Мне нравится быть индивидуальным спонсором, потому что это дает мне чувство выполненного долга, когда я сам что-то делаю.

В любом случае, я считаю, что различие между менеджером и отдельным автором полезно и при написании кода.

Позвольте мне показать это на примере игры жизни Конвея .

Шаги малыша

На ретритах кода вы найдете людей, которые начинают работать над проблемой снизу (ячейка), и тех, кто начинает сверху (вселенная, мир, сетка, игра или как их там называют).

Интересно, что обе группы имеют тенденцию сталкиваться с похожей проблемой.

шаги малыша Начиная снизу, вы сможете быстро внедрить правила в ячейке. Но тогда возникает проблема, как соединить клетки во вселенную.

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

Если вы начнете сверху, убедитесь, что мы не делаем ничего лишнего или строим какие-либо части, которые не будут правильно соединяться. Но и здесь трудно отодвинуться в маленьких шагах.

Принцип единой ответственности

Чтобы развить целую вселенную, по крайней мере эти вещи должны произойти:

  1. Итерировать по всем ячейкам
  2. Определить соседей каждой клетки
  3. Подсчитайте количество живых соседей
  4. Определите следующее состояние ячейки на основе ее текущего состояния и количества живых соседей

В зависимости от некоторых вариантов реализации (например, сохранение неизменяемости юниверса) может потребоваться больше работы.

Тем не менее, принцип единой ответственности говорит нам, что у класса должна быть только одна причина для изменения. Вы можете считать пункты № 2 и № 3 одной обязанностью (или № 3 и № 4), но очевидно, что в этом участвует более одного участника.

Классы менеджера и классы индивидуальных участников и как их проверить

Теперь запомните разницу между менеджером и индивидуальным вкладчиком (IC). Менеджеры управляют IC; это вся их работа. Мы должны также считать управление другими классами ответственностью .

Это будет означать, что класс либо реализует некоторую логику (IC), либо координирует с другими классами (менеджером), но не обоими.

издеваться Я думаю, что это также связано с дебатами по тестированию на основе состояния по сравнению с взаимодействием : мы должны использовать тестирование на основе состояния для классов IC и тестирование на основе взаимодействия ( с использованием имитаций ) для классов менеджера.

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

Классы Test-Driving Manager

Итак, давайте попробуем основанное на взаимодействии тестирование класса менеджера. Я буду придерживаться примера из игры жизни.

Я хотел бы начать с архитектурного выбора: я хочу, чтобы вселенная была неизменной, чтобы позже было легко внедрить многопоточность. По мере развития игры мне придется создавать новую вселенную для каждого поколения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class GameTest {
  @Test
  public void clonesImmutableUniverseWhenEvolving() {
    Universe universe = mock(Universe.class, "old");
    Universe nextGeneration = mock(Universe.class, "new");
    when(universe.clone()).thenReturn(nextGeneration);
 
    Game game = new Game(universe);
    assertSame("Initial", universe, game.universe());
 
    game.evolve();
    assertSame("Next generation", nextGeneration,
      game.universe());
  }
}

Я использую Mockito, чтобы определить, как Game взаимодействует со Universe . Mocking лучше всего работает с интерфейсами, поэтому Universe — это интерфейс.

Это означает, что игра не знает тип вселенной и поэтому не может создать ее новый экземпляр. Для этой цели юниверс предоставляет фабричный метод clone() .

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class GameTest {
  @SuppressWarnings("unchecked")
  @Test
  public void clonesImmutableUniverseWhenEvolving() {
    Universe universe = mock(Universe.class, "old");
    Universe nextGeneration = mock(Universe.class, "new");
    when(universe.clone(any(Iterable.class)))
        .thenReturn(nextGeneration);
 
    Game game = new Game(universe);
    assertSame("Initial", universe, game.universe());
 
    game.evolve();
    assertSame("Next generation", nextGeneration,
        game.universe());
  }
}

Хорошо, так кто же предоставит эти клетки clone() ? Игра является менеджером, поэтому она должна только управлять, а не обеспечивать логику. Вселенная — это IC, которая отвечает за клетки и их взаимосвязи, поэтому она уже несет ответственность.

Похоже, нам нужен новый тип. Новый тип должен управляться игрой, так как это единственный класс менеджера, который у нас есть. Таким образом, мы должны добавить новый тест в GameTest для этого взаимодействия.

Новый тип должен отвечать за определение нового состояния вселенной из старого. Здесь вступают в игру правила игры, поэтому давайте назовем новый тип Rules .

Начнем с тестирования того, что игра управляет правилами:

1
2
3
4
5
6
7
8
9
public class GameTest {
  @Test
  public void consultsRulesWhenEvolving() {
    Rules rules = mock(Rules.class);
 
    Game game = new Game(null, rules);
    assertSame("Rules", rules, game.rules());
  }
}

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

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

Вселенная, несомненно, является правильным местом для определения соседей клетки. Стоит ли подсчитывать количество живых?

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

Это означает, что входом в правила является ячейка и ее соседи. Поскольку Java не позволяет возвращать две части информации, нам нужно объединить их. Давайте назовем комбинацию окрестностью:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class NeighborhoodTest {
  @Test
  public void holdsACellAndItsNeighbors() {
    Cell cell = mock(Cell.class, "cell");
    List<Cell> neighbors = Arrays.asList(
        mock(Cell.class, "neighbor1"),
        mock(Cell.class, "neighbor2"));
 
    Neighborhood neighborhood = new Neighborhood(cell,
        neighbors);
 
    assertSame("Cell", cell, neighborhood.cell());
    assertEquals("Neighbors", neighbors,
        neighborhood.neighbors());
  }
}

Теперь мы можем заставить юниверс возвращать соседство для каждой ячейки, которую он содержит, и проверять, что эти окрестности используются в качестве входных данных для правил:

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
31
32
33
34
35
36
37
38
39
40
public class GameTest {
  @SuppressWarnings("unchecked")
  @Test
  public void clonesImmutableUniverseWhenEvolving() {
    Universe universe = mock(Universe.class, "old");
    Universe nextGeneration = mock(Universe.class, "new");
    when(universe.clone(any(Iterable.class)))
        .thenReturn(nextGeneration);
    when(universe.neighborhoods()).thenReturn(
        new ArrayList<Neighborhood>());
 
    Game game = new Game(universe, mock(Rules.class));
    assertSame("Initial", universe, game.universe());
 
    game.evolve();
    assertSame("Next generation", nextGeneration,
        game.universe());
  }
 
  @Test
  public void consultsRulesWhenEvolving() {
    Universe universe = mock(Universe.class);
    Neighborhood neighborhood1 = new Neighborhood(
        mock(Cell.class),
        Arrays.asList(mock(Cell.class)));
    Neighborhood neighborhood2 = new Neighborhood(
        mock(Cell.class),
        Arrays.asList(mock(Cell.class)));
    when(universe.neighborhoods()).thenReturn(
        Arrays.asList(neighborhood1, neighborhood2));
    Rules rules = mock(Rules.class);
 
    Game game = new Game(universe, rules);
    assertSame("Rules", rules, game.rules());
 
    game.evolve();
    verify(rules).nextGeneration(neighborhood1);
    verify(rules).nextGeneration(neighborhood2);
  }
}

Следующий шаг — убедиться, что выходные данные из правил используются для создания нового юниверса:

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 GameTest {
  @Test
  public void consultsRulesWhenEvolving() {
    Universe universe = mock(Universe.class);
    Neighborhood neighborhood1 = new Neighborhood(
        mock(Cell.class),
        Arrays.asList(mock(Cell.class)));
    Neighborhood neighborhood2 = new Neighborhood(
        mock(Cell.class),
        Arrays.asList(mock(Cell.class)));
    when(universe.neighborhoods()).thenReturn(
        Arrays.asList(neighborhood1, neighborhood2));
    Rules rules = mock(Rules.class);
    Cell cell1 = mock(Cell.class, "cell1");
    Cell cell2 = mock(Cell.class, "cell2");
    when(rules.nextGeneration(neighborhood1))
        .thenReturn(cell1);
    when(rules.nextGeneration(neighborhood2))
        .thenReturn(cell2);
 
    Game game = new Game(universe, rules);
    assertSame("Rules", rules, game.rules());
 
    game.evolve();
    verify(universe).clone(eq(
        Arrays.asList(cell1, cell2)));
  }
}

На этом мы закончили с классом Game . Все, что осталось сделать, — это создать реализации для трех интерфейсов, которые мы представили: Universe , Cell и Rules . Каждый из них является классом IC, и, таким образом, довольно прост для тест-драйва с использованием тестирования на основе состояния.

Вывод

Я считаю, что различие между классами менеджера и отдельных участников помогает мне решить, каким должен быть следующий тест.

Что вы думаете? Может ли это быть частью недостающей части правильной теории разработки через тестирование ?