Статьи

Игра жизни Конвея и образец мухи

легкий вес Игра жизни Конвея увлекательна как с функциональной, так и с технической точки зрения.

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

Удивительно, как работа с новыми парами дает вам новые идеи практически каждый раз.

На последнем ретрите кода, который я посетил, одна из моих пар предложила использовать шаблон Flyweight для ячеек:

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

Когда вышла книга Design Patterns (которая содержит приведенную выше цитату), я помню, что у меня было много моментов ага. Было так круто увидеть все эти шаблоны, которые я использовал раньше, и, наконец, назвать их, чтобы я мог обсудить их с коллегами более эффективно!

У меня не было ага, когда я читал о весе в полете, однако. Пример, приведенный в книге о совместном использовании символьных объектов в текстовом редакторе, в то время казался немного надуманным. Однако этот пример мало чем отличается от ячеек в сетке Game of Life, поэтому я с радостью согласился с идеей моей пары исследовать применимость шаблона в этом контексте.

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

На самом деле мы все время используем потенциальный вес в полете: логические значения. Логический класс — это класс только с двумя экземплярами, и эти экземпляры могут легко использоваться совместно. В Java это не так: new Boolean(true) != new Boolean(true) . Однако Boolean класс предоставляет две константы, TRUE и FALSE , для экземпляров, которые вы можете использовать для совместного использования.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Cell {
 
    ALIVE(true), DEAD(false);
 
    private final boolean alive;
 
    private Cell(boolean alive) {
      this.alive = alive;
    }
 
    public boolean isAlive() {
      return alive;
    }
 
    public Cell evolve(int numLiveNeighbors) {
      boolean aliveInNextGeneration = alive
          ? 2 <= numLiveNeighbors && numLiveNeighbors <= 3
          : numLiveNeighbors == 3;
      return aliveInNextGeneration ? ALIVE : DEAD;
    }
 
  }

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

Одно ограничение, которое интересно в этом контексте, состоит в том, чтобы не использовать какие-либо условия, такие как операторы if или switch или троичные операторы. Идея этого ограничения состоит в том, чтобы заставить вас заменить условные выражения полиморфизмом , делая вашу программу более объектно-ориентированной.

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

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
41
42
43
44
45
46
47
48
public enum Cell {
 
    ALIVE(true), DEAD(false);
 
    private final boolean alive;
    private static final Map<Boolean, Map<Integer, Cell>>
        NEXT = new HashMap<>();
 
    static {
      Map<Integer, Cell> dead = new HashMap<>();
      dead.put(0, DEAD);
      dead.put(1, DEAD);
      dead.put(2, DEAD);
      dead.put(3, ALIVE);
      dead.put(4, DEAD);
      dead.put(5, DEAD);
      dead.put(6, DEAD);
      dead.put(7, DEAD);
      dead.put(8, DEAD);
      dead.put(9, DEAD);
      NEXT.put(false, dead);
      Map<Integer, Cell> alive = new HashMap<>();
      alive.put(0, DEAD);
      alive.put(1, DEAD);
      alive.put(2, ALIVE);
      alive.put(3, ALIVE);
      alive.put(4, DEAD);
      alive.put(5, DEAD);
      alive.put(6, DEAD);
      alive.put(7, DEAD);
      alive.put(8, DEAD);
      alive.put(9, DEAD);
      NEXT.put(true, alive);
    }
 
    private Cell(boolean alive) {
      this.alive = alive;
    }
 
    public boolean isAlive() {
      return alive;
    }
 
    public Cell evolve(int numLiveNeighbors) {
      return NEXT.get(alive).get(numLiveNeighbors);
    }
 
  }

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

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

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public interface Cell {
 
    boolean isAlive();
    Cell evolve(int numLiveNeighbors);
 
  }
 
  public class CellFactory {
 
    private static final Map<Boolean, Cell> CELLS
        = new HashMap<>();
 
    static {
      CELLS.put(false, new DeadCell());
      CELLS.put(true, new AliveCell());
    }
 
    public static Cell dead() {
      return cell(false);
    }
 
    public static Cell alive() {
      return cell(true);
    }
 
    static Cell cell(boolean alive) {
      return CELLS.get(alive);
    }
 
  }
 
  class DeadCell implements Cell {
 
    @Override
    public boolean isAlive() {
      return false;
    }
 
    @Override
    public Cell evolve(int numLiveNeighbors) {
      return CellFactory.cell(numLiveNeighbors == 3);
    }
 
  }
 
  class AliveCell implements Cell {
 
    @Override
    public boolean isAlive() {
      return true;
    }
 
    @Override
    public Cell evolve(int numLiveNeighbors) {
      return CellFactory.cell(numLiveNeighbors == 2
          || numLiveNeighbors == 3);
    }
 
  }

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

Благодаря отступлению от кода и моему партнеру я теперь знаю почему.

Ссылка: Игра жизни Конвея и шаблон Flyweight от нашего партнера JCG Ремона Синнема в блоге по разработке безопасного программного обеспечения .