ООП в примерах. Часть 4. Интерфейсы, повторное использование
от aNNiMON
Продолжим наше изучение ООП. Давайте создадим сущность Меню и обернём её вокруг наших пунктов, то есть перенесём всё, что касается меню в отдельный класс Menu.
Позиция курсора (int cursor), массив с пунктами меню (MenuItem[] items), отрисовка, обработка клавиш теперь будет в классе Menu. Menu.java
Вместо массива я добавил Vector, чтобы можно было динамически добавлять элементы.
Теперь класс Canvas будет ещё проще:
Посмотрим ещё раз на класс Menu. Мы ведь можем создать ещё одну сущность - курсор.
И тогда вот эти строки класса Menu:
превратятся в:
Теперь посмотрим в метод paint класса Canvas.
Это не что иное, как фон. Сейчас он простенький, но если вдруг понадобится усложнить его и использовать в других экранах, например в экране помощи, рекордов, настроек? Каждый раз писать один и тот же код, как мы уже выяснили - нехорошо, поэтому создадим ещё одну сущность - фон, класс Background.
Теперь можно усложнить его как душе угодно, добавить фоновую картинку или градиент, а то и вовсе какую-нибудь анимацию космических боёв добавить.
И потом, где нужно будет этот фон использовать, будем вызывать 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:
Обратите внимание, что в интерфейсах мы не пишем тело метода. Мы просто указываем имя метода, параметры (если есть) и возвращаемое значение.
Теперь повесим этот интерфейс классу канваса: public class OopMenu5 extends Canvas implements MenuItemSelectListener
Для интерфейсов используется ключевое слово implements, в переводе - реализует.
Те классы, которые реализуют интерфейс, должны иметь тело метода, указанного в интерфейсе. То есть в класс OopMenu5 нужно добавить метод public void onSelect(int index, MenuItem item) { }
Так как этот метод будет вызываться после выбора пункта меню, то перенесём код из case Canvas.FIRE в него:
В классе Menu добавим поле и метод добавления обработчика:
И в case Canvas.FIRE вот это:
Осталось только "повесить" обработчик, то есть связать интерфейс класса OopMenu5 с соответствующим полем в классе Menu.
Теперь надо внимательно разобраться с вызовами операторов.
Мы запустили программу, открылось меню, то есть сейчас управление в классе канваса - 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. Добавить обработчик (то есть связать класс с интерфейсом):
}
Что это нам даёт? Теперь мы можем брать классы Menu и MenuCursor и добавлять в свои проекты без изменения кода. То есть мы сделали эти классы для повторного использования.
Вспомните теперь наш первый вариант без ООП. Для того, чтобы перекинуть меню в другой проект, нужно было искать необходимые переменные, операторы, копировать всё это в другой класс Canvas'a. А теперь нам достаточно просто скопировать классы и вызвать их с соответствующими параметрами.
Готовый проект
Позиция курсора (int cursor), массив с пунктами меню (MenuItem[] items), отрисовка, обработка клавиш теперь будет в классе Menu. Menu.java
Вместо массива я добавил Vector, чтобы можно было динамически добавлять элементы.
Теперь класс Canvas будет ещё проще:
- public class OopMenu5 extends Canvas {
- private final int width, height;
- private Menu menu;
- public OopMenu5() {
- setFullScreenMode(true);
- width = getWidth();
- height = getHeight();
- menu = new Menu();
- menu.addMenuItem("Start", "start.png");
- menu.addMenuItem("Options", 0xFFA06028);
- menu.addMenuItem("Help", 0xFF3C3A83);
- menu.addMenuItem("About");
- menu.addMenuItem("Exit", "exit.png");
- }
- public void paint(Graphics g) {
- g.setColor(0xFFE1FFE1);
- g.fillRect(0, 0, width, height);
- menu.paint(g);
- }
- protected void keyPressed(int keyCode) {
- final int ga = getGameAction(keyCode);
- menu.handleKeys(ga);
- repaint();
- }
- }
Посмотрим ещё раз на класс Menu. Мы ведь можем создать ещё одну сущность - курсор.
- public class MenuCursor {
- private int cursor;
- public MenuCursor() {
- cursor = 0;
- }
- public int getCursor() {
- return cursor;
- }
- public void paint(Graphics g, int startY) {
- g.setColor(0xFF11C62A);
- g.drawRect(0, startY + Menu.ITEM_HEIGHT * cursor, g.getClipWidth(), Menu.ITEM_HEIGHT);
- }
- public void cursorUp(int itemsCount) {
- cursor--;
- if (cursor < 0) cursor = itemsCount - 1;
- }
- public void cursorDown(int itemsCount) {
- cursor++;
- if (cursor >= itemsCount) cursor = 0;
- }
- }
- case Canvas.UP:
- cursor--;
- if (cursor < 0) cursor = items.size() - 1;
- break;
- case Canvas.DOWN:
- cursor++;
- if (cursor >= items.size()) cursor = 0;
- break;
- }
- case Canvas.UP:
- cursor.cursorUp(items.size());
- break;
- case Canvas.DOWN:
- cursor.cursorDown(items.size());
- break;
Теперь посмотрим в метод paint класса Canvas.
- g.setColor(0xFFE1FFE1);
- g.fillRect(0, 0, width, height);
- public class Background {
- public void paint(Graphics g) {
- g.setColor(0xFFE1FFE1);
- g.fillRect(0, 0, g.getClipWidth(), g.getClipHeight());
- }
- }
Теперь можно усложнить его как душе угодно, добавить фоновую картинку или градиент, а то и вовсе какую-нибудь анимацию космических боёв добавить.
И потом, где нужно будет этот фон использовать, будем вызывать 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:
- public interface MenuItemSelectListener {
- public void onSelect(int index, MenuItem item);
- }
Обратите внимание, что в интерфейсах мы не пишем тело метода. Мы просто указываем имя метода, параметры (если есть) и возвращаемое значение.
Теперь повесим этот интерфейс классу канваса: public class OopMenu5 extends Canvas implements MenuItemSelectListener
Для интерфейсов используется ключевое слово implements, в переводе - реализует.
Те классы, которые реализуют интерфейс, должны иметь тело метода, указанного в интерфейсе. То есть в класс OopMenu5 нужно добавить метод public void onSelect(int index, MenuItem item) { }
Так как этот метод будет вызываться после выбора пункта меню, то перенесём код из case Canvas.FIRE в него:
- public void onSelect(int index, MenuItem item) {
- Alert al = new Alert("Info");
- al.setString(item.getName() + "\n"
- + item.getClass().getName() + "\nPosition: " + index);
- Demo.midlet.dsp.setCurrent(al);
- }
В классе Menu добавим поле и метод добавления обработчика:
- private MenuItemSelectListener itemSelectListener;
- public void addItemSelectListener(MenuItemSelectListener itemSelectListener) {
- this.itemSelectListener = itemSelectListener;
- }
И в case Canvas.FIRE вот это:
- switch (gameActionKey) {
- case Canvas.UP:
- cursor.cursorUp(items.size());
- break;
- case Canvas.DOWN:
- cursor.cursorDown(items.size());
- break;
- case Canvas.FIRE:
- int index = cursor.getCursor();
- itemSelectListener.onSelect(index, getMenuItem(index));
- break;
- }
Осталось только "повесить" обработчик, то есть связать интерфейс класса OopMenu5 с соответствующим полем в классе Menu.
- public OopMenu5() {
- setFullScreenMode(true);
- background = new Background();
- menu = new Menu();
- menu.addItemSelectListener(OopMenu5.this);
- menu.addMenuItem("Start", "start.png");
- menu.addMenuItem("Options", 0xFFA06028);
- menu.addMenuItem("Help", 0xFF3C3A83);
- menu.addMenuItem("About");
- menu.addMenuItem("Exit", "exit.png");
- }
Теперь надо внимательно разобраться с вызовами операторов.
Мы запустили программу, открылось меню, то есть сейчас управление в классе канваса - 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. Добавить обработчик (то есть связать класс с интерфейсом):
- public Background() {
- menu.addItemSelectListener(Background.this);
Что это нам даёт? Теперь мы можем брать классы Menu и MenuCursor и добавлять в свои проекты без изменения кода. То есть мы сделали эти классы для повторного использования.
Вспомните теперь наш первый вариант без ООП. Для того, чтобы перекинуть меню в другой проект, нужно было искать необходимые переменные, операторы, копировать всё это в другой класс Canvas'a. А теперь нам достаточно просто скопировать классы и вызвать их с соответствующими параметрами.
Готовый проект