ООП в примерах. Часть 4. Интерфейсы, повторное использование

от
Совершенный код   ооп, java me, java

Первая часть
Вторая часть. Наследование
Третья часть. Переопределение методов, уровни абстракции

Продолжим наше изучение ООП. Давайте создадим сущность Меню и обернём её вокруг наших пунктов, то есть перенесём всё, что касается меню в отдельный класс Menu.
Позиция курсора (int cursor), массив с пунктами меню (MenuItem[] items), отрисовка, обработка клавиш теперь будет в классе Menu. Menu.java
Вместо массива я добавил Vector, чтобы можно было динамически добавлять элементы.

Теперь класс Canvas будет ещё проще:
  1. public class OopMenu5 extends Canvas {
  2.  
  3.     private final int width, height;
  4.     private Menu menu;
  5.  
  6.     public OopMenu5() {
  7.         setFullScreenMode(true);
  8.         width = getWidth();
  9.         height = getHeight();
  10.  
  11.         menu = new Menu();
  12.         menu.addMenuItem("Start", "start.png");
  13.         menu.addMenuItem("Options", 0xFFA06028);
  14.         menu.addMenuItem("Help", 0xFF3C3A83);
  15.         menu.addMenuItem("About");
  16.         menu.addMenuItem("Exit", "exit.png");
  17.     }
  18.  
  19.     public void paint(Graphics g) {
  20.         g.setColor(0xFFE1FFE1);
  21.         g.fillRect(0, 0, width, height);
  22.         menu.paint(g);
  23.     }
  24.  
  25.     protected void keyPressed(int keyCode) {
  26.         final int ga = getGameAction(keyCode);
  27.         menu.handleKeys(ga);
  28.         repaint();
  29.     }
  30. }

Посмотрим ещё раз на класс Menu. Мы ведь можем создать ещё одну сущность - курсор.
  1. public class MenuCursor {
  2.  
  3.     private int cursor;
  4.  
  5.     public MenuCursor() {
  6.         cursor = 0;
  7.     }
  8.  
  9.     public int getCursor() {
  10.         return cursor;
  11.     }
  12.  
  13.     public void paint(Graphics g, int startY) {
  14.         g.setColor(0xFF11C62A);
  15.         g.drawRect(0, startY + Menu.ITEM_HEIGHT * cursor, g.getClipWidth(), Menu.ITEM_HEIGHT);
  16.     }
  17.  
  18.     public void cursorUp(int itemsCount) {
  19.         cursor--;
  20.         if (cursor < 0) cursor = itemsCount - 1;
  21.     }
  22.  
  23.     public void cursorDown(int itemsCount) {
  24.         cursor++;
  25.         if (cursor >= itemsCount) cursor = 0;
  26.     }
  27. }
И тогда вот эти строки класса Menu:
  1. case Canvas.UP:
  2.     cursor--;
  3.     if (cursor < 0) cursor = items.size() - 1;
  4.     break;
  5. case Canvas.DOWN:
  6.     cursor++;
  7.     if (cursor >= items.size()) cursor = 0;
  8.     break;
  9. }
превратятся в:
  1. case Canvas.UP:
  2.     cursor.cursorUp(items.size());
  3.     break;
  4. case Canvas.DOWN:
  5.     cursor.cursorDown(items.size());
  6.     break;

Теперь посмотрим в метод paint класса Canvas.
  1. g.setColor(0xFFE1FFE1);
  2. g.fillRect(0, 0, width, height);
Это не что иное, как фон. Сейчас он простенький, но если вдруг понадобится усложнить его и использовать в других экранах, например в экране помощи, рекордов, настроек? Каждый раз писать один и тот же код, как мы уже выяснили - нехорошо, поэтому создадим ещё одну сущность - фон, класс Background.
  1. public class Background {
  2.  
  3.     public void paint(Graphics g) {
  4.         g.setColor(0xFFE1FFE1);
  5.         g.fillRect(0, 0, g.getClipWidth(), g.getClipHeight());
  6.     }
  7. }

Теперь можно усложнить его как душе угодно, добавить фоновую картинку или градиент, а то и вовсе какую-нибудь анимацию космических боёв добавить.
И потом, где нужно будет этот фон использовать, будем вызывать background.paint(g); и всё.


Теперь рассмотрим работу обработчиков выбора пункта меню.
В данный момент у нас обработка каждого пункта находится в классе Menu в методе handleKeys в блоке switch/case Canvas.FIRE.
Это не хорошо, так как одна из возможностей ООП - возможность повторного использования. То есть нам надо сделать так, чтобы в любом проекте можно было просто скопировать нужные классы и вызвать необходимый код для нужного действия. Вот как мы с классом фона сделали - если надо использовать в другом проекте, просто копируем класс, создаём поле Background bg и вызываем bg.paint(g) в методе отрисовки. Также нужно сделать и с меню.

И тут на помощь нам приходят интерфейсы.
Интерфейс позволяет расширить некоторую сущность, задать дополнительные свойства.
Например, есть класс машина, трактор и велосипед. В каждом из них можно создать метод drive(). Но тогда мы не сможем одновременно управлять всеми этими объектами. Нам нужно по отдельности вызывать car.drive(), tractor.drive() и bicycle.drive(). Но если мы повесим каждому из этих классов одинаковый интерфейс, тогда мы сможем с лёгкостью всеми этими объектами управлять. Потому он и зовётся интерфейсом, чтобы предоставлять интерфейс управления.
В нашем конкретном случае мы можем создать интерфейс рисования, так как у класса Background и Menu есть одинаковый метод paint(Graphics g).

Также интерфейс используется для создания обработчиков.
Давайте создадим интерфейс MenuItemSelectListener:
  1. public interface MenuItemSelectListener {
  2.  
  3.     public void onSelect(int index, MenuItem item);
  4. }

Обратите внимание, что в интерфейсах мы не пишем тело метода. Мы просто указываем имя метода, параметры (если есть) и возвращаемое значение.

Теперь повесим этот интерфейс классу канваса: public class OopMenu5 extends Canvas implements MenuItemSelectListener
Для интерфейсов используется ключевое слово implements, в переводе - реализует.
Те классы, которые реализуют интерфейс, должны иметь тело метода, указанного в интерфейсе. То есть в класс OopMenu5 нужно добавить метод public void onSelect(int index, MenuItem item) { }
Так как этот метод будет вызываться после выбора пункта меню, то перенесём код из case Canvas.FIRE в него:
  1. public void onSelect(int index, MenuItem item) {
  2.     Alert al = new Alert("Info");
  3.     al.setString(item.getName() + "\n"
  4.             + item.getClass().getName() + "\nPosition: " + index);
  5.     Demo.midlet.dsp.setCurrent(al);
  6. }

В классе Menu добавим поле и метод добавления обработчика:
  1. private MenuItemSelectListener itemSelectListener;
  2.  
  3. public void addItemSelectListener(MenuItemSelectListener itemSelectListener) {
  4.     this.itemSelectListener = itemSelectListener;
  5. }

И в case Canvas.FIRE вот это:
  1. switch (gameActionKey) {
  2.     case Canvas.UP:
  3.         cursor.cursorUp(items.size());
  4.         break;
  5.     case Canvas.DOWN:
  6.         cursor.cursorDown(items.size());
  7.         break;
  8.     case Canvas.FIRE:
  9.         int index = cursor.getCursor();
  10.         itemSelectListener.onSelect(index, getMenuItem(index));
  11.         break;
  12. }

Осталось только "повесить" обработчик, то есть связать интерфейс класса OopMenu5 с соответствующим полем в классе Menu.
  1. public OopMenu5() {
  2.     setFullScreenMode(true);
  3.     background = new Background();
  4.     menu = new Menu();
  5.     menu.addItemSelectListener(OopMenu5.this);
  6.     menu.addMenuItem("Start", "start.png");
  7.     menu.addMenuItem("Options", 0xFFA06028);
  8.     menu.addMenuItem("Help", 0xFF3C3A83);
  9.     menu.addMenuItem("About");
  10.     menu.addMenuItem("Exit", "exit.png");
  11. }

Теперь надо внимательно разобраться с вызовами операторов.
Мы запустили программу, открылось меню, то есть сейчас управление в классе канваса - OopMenu5. Если нажмём вниз, то выполнится:
keyPressed(int keyCode) - класса OopMenu5
--final int ga = getGameAction(keyCode); - класса OopMenu5
--menu.handleKeys(ga); - класса OopMenu5
----switch (gameActionKey) - класса Menu
----case Canvas.DOWN - класса Menu
----cursor.cursorDown(items.size()); - класса Menu
------cursor++; - класса MenuCursor
------if (cursor >= itemsCount) cursor = 0; - класса MenuCursor

Вот такая вот иерархия. Это вполне нормально.
Теперь выберем какой-нибудь пункт меню, нажав кн. 5:
keyPressed(int keyCode) - класса OopMenu5
--final int ga = getGameAction(keyCode); - класса OopMenu5
--menu.handleKeys(ga); - класса OopMenu5
----switch (gameActionKey) - класса Menu
----case Canvas.FIRE - класса Menu
----int index = cursor.getCursor(); - класса Menu
----itemSelectListener.onSelect(index, getMenuItem(index)); - класса Menu
Теперь itemSelectListener посмотрит, какой интерфейс ему соответствует, то есть в каком классе было написано implements MenuItemSelectListener и addItemSelectListener(..). Таким классом у нас является OopMenu5. Поэтому вызовется метод onSelect именно этого класса.
------public void onSelect(int index, MenuItem item) - класса OopMenu5
--------Alert al = new Alert("Info"); - класса OopMenu5
--------al.setString(...) - класса OopMenu5
--------Demo.midlet.dsp.setCurrent(al); - класса OopMenu5
Надеюсь теперь более понятно, как работают интерфейсы?
Если бы мы использовали MenuItemSelectListener в классе Background, тогда нам:
1. Пришлось бы дописать implements MenuItemSelectListener
2. Добавить (реализовать) все методы интерфейса, то есть public void onSelect(int index, MenuItem item) { }
3. Добавить обработчик (то есть связать класс с интерфейсом):
  1. public Background() {
  2.   menu.addItemSelectListener(Background.this);
}

Что это нам даёт? Теперь мы можем брать классы Menu и MenuCursor и добавлять в свои проекты без изменения кода. То есть мы сделали эти классы для повторного использования.
Вспомните теперь наш первый вариант без ООП. Для того, чтобы перекинуть меню в другой проект, нужно было искать необходимые переменные, операторы, копировать всё это в другой класс Canvas'a. А теперь нам достаточно просто скопировать классы и вызвать их с соответствующими параметрами.

  Скриншот
  Готовый проект
+6   7   1
3388