ООП в примерах. Часть 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
  • views 7804