Архив рубрики: Java

Многоинтерфейсное приложение

Заголовок темы может звучать, для Вас, странно, но довольно часто требуется, чтобы приложение имело сразу несколько вариантов интерфейса. Например, GUI, CLI и/или web. В этой статье пойдёт речь о построении такого приложения с использованием паттерна Observer.
По традиции, я буду вести рассказ на примере. Пример, как всегда, высосан из пальца и мало похож на реальное приложение.
Я уже сказал, что использовать мы будем паттерн Observer, но кроме него, нам понадобится ещё и паттерн MVC (Model-View-Controller). MVC состоит из трёх частей: модель, контроллер и представление. Модель - это те данные, с которыми мы работаем. Контроллер определяет способ работы с данными. Представление - это, не что иное, как интерфейс приложения, с которым взаимодействует пользователь. Если данные достаточно просты, то, часто, модель и контроллер объединяют в одном объекте. В своём примере, я именно так и поступил.
На следующей диаграмме показана упрощённая схема приложения.
Из диаграммы видно, что мы имеем два подприложения: консольное и оконное. MyObject, в данном примере, является как моделью, так и контроллером. Объект класса MyObject содержит список наблюдателей и оповещает их о смене состояние через интерфейс MyObjectObserver.
Предполагается, что мы имеем один объект класса MyObject разделённый между двумя приложениями. Когда одно из приложений меняет состояние объекта, второе приложение реагирует на это и меняет своё отображение.
Думаю, что с теорией всё ясно, давайте посмотрим на код.
package test.core;

import java.util.HashSet;

public class MyObject
{

State state = State.State1;
HashSet<MyObjectObserver> observers = new HashSet<MyObjectObserver>();

public static enum State
{
State1,
State2,
State3
}

public synchronized void addObserver(MyObjectObserver observer)
{
if(observer != null)
observers.add(observer);
}

public synchronized void removeObserver(MyObjectObserver observer)
{
observers.remove(observer);
}

public synchronized void setState(State newState)
{
if(state != newState)
{
State oldState = state;
state = newState;
for(MyObjectObserver observer : observers)
{
observer.onStateChanged(oldState, newState);
}
}
}

public synchronized State getState()
{
return state;
}
}
Так как класс MyObject выполняет роль контроллера в многопоточном приложении, то некоторые методы в нём объявлены  как synchronized.
Далее, интерфейс наблюдателя
package test.core;

public interface MyObjectObserver
{

public void onStateChanged(MyObject.State oldState, MyObject.State newState);
}
Каждое подприложение реализует интерфейс Application
package test;

import test.core.MyObject;

public interface Application
{

public void run(MyObject object);
}
Приложение с консольным интерфейсом могло бы выглядеть следующим образом
package test.cli;

import test.Application;
import test.core.MyObject;
import test.core.MyObjectObserver;
import java.util.Scanner;

public class CliApplication
implements Application, MyObjectObserver
{

private boolean needPrompt = true;

@Override
public void run(MyObject object)
{
Scanner stdin = new Scanner(System.in);
object.addObserver(this);
while(true)
{
showPrompt();
String input = stdin.nextLine().trim();
if(input.equals("q")) break;
int number;
try
{
number = Integer.parseInt(input);
}
catch(NumberFormatException e)
{
System.out.println(String.format("\"%s\" is not number", input));
needPrompt = true;
continue;
}
MyObject.State state;
switch(number)
{
case 1:
state = MyObject.State.State1;
break;
case 2:
state = MyObject.State.State2;
break;
case 3:
state = MyObject.State.State3;
break;
default:
System.out.println(
String.format("\"%d\" is wrong number", number));
needPrompt = true;
continue;
}
object.setState(state);
}
object.removeObserver(this);
}

void showPrompt()
{
if(needPrompt)
{
System.out.print("Enter state number or 'q' for exit:\n" +
" 1: State1\n" +
" 2: State2\n" +
" 3: State3\n: ");
}
needPrompt = false;
}

@Override
public void onStateChanged(MyObject.State oldState, MyObject.State newState)
{
System.out.println(String.format(
"\nObject state was changed from \"%s\" to \"%s\"",
oldState.toString(), newState.toString()));
needPrompt = true;
showPrompt();
}
}
Пользователю предлагается ввести номер состояния, после чего оно меняется в объекте. Кроме того, приложение следит за своим объектом и выводит сообщения о том, что его состояние меняется.
Оконное приложение требует двух классов: приложение, наследник от интерфейса Application и, собственно, окно. Класс GuiApplication является всего лишь средством запуска окна.
package test.gui;

import test.Application;
import test.core.MyObject;


public class GuiApplication
implements Application
{

@Override
public void run(MyObject object)
{
MainWindow window = new MainWindow(object);
window.setDefaultCloseOperation(MainWindow.EXIT_ON_CLOSE);
window.setVisible(true);
}
}
Объект класса MyObject транзитом проходит через класс GuiApplication в класс MainWindow.
package test.gui;

import test.core.MyObject;
import test.core.MyObjectObserver;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class MainWindow
extends JFrame
implements MyObjectObserver
{

private JComboBox comboBox;
private MyObject object;

class ComboBoxListener
implements ActionListener
{

@Override
public void actionPerformed(ActionEvent e)
{
MyObject.State state = (MyObject.State)comboBox.getSelectedItem();
if(state != null)
object.setState(state);
}
}

class MainWindowListener
extends WindowAdapter
{

@Override
public void windowOpened(WindowEvent e)
{
object.addObserver(MainWindow.this);
}

@Override
public void windowClosed(WindowEvent e)
{
object.removeObserver(MainWindow.this);
}
}

public MainWindow(MyObject object)
{
this.object = object;
comboBox = new JComboBox(MyObject.State.values());
comboBox.setSelectedItem(object.getState());
comboBox.addActionListener(new ComboBoxListener());
add(comboBox);
addWindowListener(new MainWindowListener());
setMinimumSize(new Dimension(200, 50));
pack();
}

@Override
public void onStateChanged(MyObject.State oldState, MyObject.State newState)
{
comboBox.setSelectedItem(newState);
}
}
Каждое подприложение запускается в отдельном потоке методом main.
package test;

import test.cli.CliApplication;
import test.core.MyObject;
import test.gui.GuiApplication;

class ApplicationRunner
implements Runnable
{

private Application application;
private MyObject object;

public ApplicationRunner(Application application, MyObject object)
{
this.application = application;
this.object = object;
}

@Override
public void run()
{
application.run(object);
}
}

public class Main
{

static MyObject object = new MyObject();

static Application[] getApplications()
{
return new Application[]
{
new CliApplication(),
new GuiApplication()
};
}

public static void main(String[] args)
{
for(Application app : getApplications())
{
ApplicationRunner runner = new ApplicationRunner(app, object);
Thread thread = new Thread(runner);
thread.start();
}
}
}
То, что в итоге получилось, можно увидеть в ролике ниже.

Спецификация исключений: друг или враг?

Исторически сложилось, что разные языки программирования, поддерживающие работу с исключениями, по-разному относятся к спецификации исключений. В Java спецификации обязательны и контролируются статически, в C# и Python их вообще нет, а в C++ этот вопрос является одним из самых "сырых" мест.
В двух словах о том, что такое спецификация исключений, для тех, кто не сталкивался с этим понятием. Спецификация исключений - это явное описание тех типов исключений, которые могут быть сгенерированы некой функцией. В java, языке, который поддерживает наилучшим образом спецификацию исключений, это выглядит так
void funct() throw MyException
{
throw new MyException();
}
MyException - это единственно возможный тип исключений, который может покинуть метод. При спецификации можно указать несколько типов исключений.

C++

С самого начала, идея спецификации исключений была чужда языку C++. Язык унаследовал миллиарды строк кода на языке C и, к тому времени, уже были написаны миллионы строк кода на C++, в которых не было ничего о спецификации исключений. Тем не менее, в C++ была добавлена возможность спецификаций. В своей книге "Дизайн и эволюция C++" Бьёрн Страуструп пишет о том, что спецификации, изначально могли контролироваться только во время исполнения, но, позже, были добавлены некоторые возможности для статического контроля, но, как мы увидим, этого не достаточно. В том же параграфе, Бьёрн приводит пример, который я сейчас Вам продемонстрирую. Итак, предположим, что у нас есть библиотека, написанная на языке C++, вот заголовок этой библиотеки
#ifndef SHAREDCPP_H
#define SHAREDCPP_H

#ifdef __cplusplus
extern "C" {
#endif

void foo();

#ifdef __cplusplus
} // extern "C"
#endif

#endif // SHAREDCPP_H
И её реализация
#include <iostream>
#include <string>

extern "C" {

void foo()
{
std::cout << "Throw exceptionn";
throw std::string("test exception");
}

}
Я назвал эту библиотеку libsharedcpp. Я использую ОС Linux, поэтому я не писал конструкции типа __declspec(dllexport). Если Вы используете ОС MS Windows, то для компиляции примеров, Вам нужно будет добавить эти конструкции (думаю, не нужно Вас учить это делать). Для компиляции, я буду использовать компиляторы из набора GCC. Собираем эту библиотеку командой
g++ -shared -fPIC sharedcpp.cpp -o libsharedcpp.so
Теперь создадим библиотеку libsharedc на языке C со следующим кодом
#ifndef SHAREDC_H
#define SHAREDC_H

#ifdef __cplusplus
extern "C" {
#endif

void bar();

#ifdef __cplusplus
} // extern "C"
#endif

#endif // SHAREDC_H
#include "sharedc.h"
#include "sharedcpp.h"

void bar()
{
foo();
}
Как видно, библиотека libsharedc использует функцию из библиотеки libsharedcpp, поэтому, при компиляции, надо указать этот факт
gcc -shared -fPIC sharedc.c -lsharedcpp -L. -o libsharedc.so
Теперь создадим исполняемый модуль на языке C++.
#include <string>
#include <iostream>
#include "sharedc.h"

int main()
{
try
{
bar();
}
catch(const std::string & str)
{
std::cout << str << std::endl;
}
return 0;
}
Если Вы используете UNIX-like ОС, то, перед компиляцией программы, Вам следует либо поместить собранные библиотеки в место, где Ваша ОС их найдёт, например в /usr/lib, либо добавить в путь для поиска текущий каталог командой
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:`pwd`
Далее собираем программу командой
g++ program.cpp -lsharedc -L. -o program
Запустим программу
$ ./program 
Throw exception
test exception
Итак, что же я хотел продемонстрировать этой программой. Прежде всего, обратите внимание, на то, что исключение прошло через два скомпилированных модуля. Мало того, один из скомпилированных модулей написан на языке C, который знать не знает ни о каких исключениях. Из этого уже можно сделать вывод о том, что указать спецификацию исключений для функции bar невозможно.
Теперь, давайте дополним программу таким образом
#include <string>
#include <exception>
#include <iostream>
#include "sharedc.h"

void test() throw(std::exception)
{
bar();
}

int main()
{
try
{
test();
}
catch(const std::exception & err)
{
}
return 0;
}
Я добавил функцию test, которая специфицирует исключения только стандартного типа - std::exception. Статические анализаторы не смогут определить, что функция bar генерирует исключение типа std::string, и компиляция пройдёт успешно. Но при выполнении нас ждёт ошибка.
$ ./program 
Throw exception
terminate called after throwing an instance of 'std::string'
Аварийный останов
И даже добавление catch блока, обрабатывающего std::string, не устранит проблему , так как она возникает до момента обработки исключения, а именно - в момент выхода исключения из функции.
try
{
test();
}
catch(const std::exception & err)
{
}
catch(const std::string & err)
{
}
Таким образом, я продемонстрировал ситуацию, когда пользы от спецификации исключений меньше, чем вреда.

Java

По-другому дела обстоят с Java. В этом языке спецификация исключений контролируется статически, а компиляция в байт-код позволяет выявить все спецификации на этапе компиляции или раньше. В отличие от C++, спецификация исключений в Java носит обязательный характер, что тоже сопряжено с некоторыми трудностями.
Предположим, что у нас в программе, есть базовый класс
package test;

public class MyBase
{

private int value;

public MyBase(int value)
{
this.value = value;
}
}
И от него наследуются множество наследников. Например
public class MyFirst
extends MyBase
{

public MyFirst()
{
super(1);
}
}
И, некоторый наследник содержит в себе данные, исходный код которых, не доступен
package test;

import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.SparseMultigraph;

public class MySecons
extends MyFirst
{

private Graph<Integer, String> graph;

public MySecons()
{
graph = new SparseMultigraph<Integer, String>();
}
}
Внезапно Вам становится необходимо сделать Ваш базовый класс клонируемым и Вы добавляете переопределение метода clone и наследование от интерфейса Cloneable
package test;

public class MyBase
implements Cloneable
{

private int value;

public MyBase(int value)
{
this.value = value;
}

@Override
public Object clone()
{
try
{
MyBase base = (MyBase)super.clone();
base.value = value;
return base;
} catch(CloneNotSupportedException ex)
{
// Этого не должно произойти.
return null;
}
}
}
Я задушил исключение, так как оно не может возникнуть, если класс реализует интерфейс Cloneable. Теперь я должен реализовать метод clone в потомках базового класса. С классом MyFirst проблем нет; он достаточно примитивен, чтобы не писать эту реализацию вообще. Но, вот, класс MySecond принесёт нам не мало хлопот. Склонировать граф из библиотеки JUNG мы не можем, так как он не предоставляет нам такой возможности. После некоторых раздумий, мы можем прийти к выводу, что клонировать граф - действительно не лучшая затея, и мы решаем запретить клонировать объекты класса MySecond. Но мы уже разрешили клонировать базовый класс и всех его детей, поэтому единственным верным решением остаётся выкинуть исключение при попытке вызова метода clone. Но не тут-то было. В java запрещается специфицировать исключения у переопределённых методов таким образом, чтобы это расходилось со спецификацией, определённой в родительском классе (убирать исключения из спецификации можно). И теперь, как ни старайся, ничего с методом clone, дельного не выйдет, либо возвращаем задушенное исключение, либо придумываем другой способ копирования. Я, к слову, смог ввести некое подобие конструкторов копирования из C++. Другой вариант решения проблемы описан в статье "Фабрика клонов".

Итого

Я не могу говорить за большинство языков, которые существуют, но о некоторых сказать кое-что могу. В частности, в языках C# и Python от спецификации исключений отказались вовсе, и правильно сделали, на мой взгляд. Как видно из предыдущих частей статьи: спецификации исключений зачастую оказываются вредными, а в случае с C++, ещё и бесполезными.

Определяем, лежит ли точка внутри полигона

Эта статья является продолжением цикла "Математика в программировании". На этот раз я хочу показать, как можно по координатам определить, лежит ли точка внутри замкнутого многоугольника или нет. Для решения этой задачи существует несколько способов. Оптимальный, для вычислительной машины, способ заключается в подсчёте количества пересечённых сторон лучём проведённым из проверяемой точки.


Как видно из рисунка 1, если точка принадлежит полигону, то луч пересечёт нечётное количество сторон. Если же луч пересечёт чётное количество сторон, или ниодной, то это значит, что точка лежит вне полигона.
У данного способа есть недостаток, связанный с случаем, когда точка проходит через вершину, но в большинстве случаев такой вероятностью можно пренебречь. Кроме того, можно выполнить дополнительные действия для изменения условия, например изменить направление луча, если последний проходит через одну из вершин.
Недавно я начать изучать язык программирования Java и в связи с этим решил реализовать пример к этой статье именно на нём. Это мой первый опус на Java, так что не обессудьте.
Итак, что должно получится в итоге:
В окне можно "натыкать" левой кнопкой мыши точек, которые будут являться вершинами полигона. Во время рисования все линии будут отображаться. Закончить рисовать полигон нужно нажатием правой кнопки, после чего все щелчки левой кнопкой будут приводить к установке точки, вхождение которой нужно проверить. Для того, чтобы начать всё сначала нужно нажать Esc.
Итак, исходники:


Файл CPoints.java - это мой вспомогательный класс, который я использовал, для хранения массивов точек. Он динамически выделяет память под массивы блоками.
package polygon;


public class CPoints
{

// Массив абсцисс.
private int[] m_x;
// Массив ординат.
private int[] m_y;
// Количество точек.
private int m_count;
// Вместимость.
private int m_capacity;
// Количество элементов, добавляемых при расширении.
private final int m_block_size;
// Минимальный размер m_block_size.
private final int m_block_size_minimal = 10;

// Устанавливает размер блока в значение по умолчанию.
public CPoints()
{
m_block_size = m_block_size_minimal;
m_count = 0;
m_capacity = 0;
increase();
}

// default_block_size - размер блока, если меньше чем
// m_block_size_minimal,то игнорируется.
public CPoints(int default_block_size)
{
if(default_block_size < m_block_size_minimal)
default_block_size = m_block_size_minimal;
m_block_size = default_block_size;
m_count = 0;
m_capacity = 0;
increase();
}

// Добавляет точку в конец массива.
public void push(final int x, final int y)
{
// Если добавлять некуда, то увеличиваем размер массивов.
if(m_count == m_capacity)
increase();

m_x[m_count] = x;
m_y[m_count] = y;
m_count++;
}

// Удаляет последнюю точку.
public void pop()
{
if(m_count > 0)
m_count--;
}

/// Возвращает размер массива.
public int count()
{
return m_count;
}

// Возвращает массив X'ов.
public final int[] getXArray()
{
return m_x;
}

// Возвращает массив Y'ов.
public final int[] getYArray()
{
return m_y;
}

// Увеличевает размер массивов на m_block_size.
private void increase()
{
int new_capasity = m_capacity + m_block_size;
if(m_capacity != 0)
{
int[] tempx = new int[new_capasity];
int[] tempy = new int[new_capasity];

for(int i = 0; i < m_capacity; i++)
{
tempx[i] = m_x[i];
tempy[i] = m_y[i];
}

m_x = tempx;
m_y = tempy;
}
else
{
m_x = new int[new_capasity];
m_y = new int[new_capasity];
}
m_capacity = new_capasity;
}
}

Непосредственно сам определитель, модуль CDeterminant.java
package polygon;


public class CDeterminant
{

public static boolean determine(final CPoints points, int x, int y)
{

boolean result = false;
int count = points.count();
int[] xp = points.getXArray();
int[] yp = points.getYArray();


for (int i = 0, j = count - 1; i < count; j = i++)
{
if(xp[j] == xp[i] || yp[j] == yp[i]) // Деление на ноль.
continue;

if(((yp[i] <= y && y < yp[j]) || (yp[j] <= y && y < yp[i])))
{
float x1 = xp[i];
float y1 = yp[i];
float x2 = xp[j];
float y2 = yp[j];

float k = (y2 - y1) / (x2 - x1);
float b = y1 - k * x1;
float cross_x = (y - b) / k;

if((float)x > cross_x)
result = !result;
}
}
return result;
}
}

Здесь я нарочно не стал оптимизировать, даже наоборот, написал всё более подробно для того, чтобы было проще понять. Мы в цикле берём каждую пару смежных точек полигона и находим точку пересечения с лучём f(x) = y, где y - ордината определяемой точки. Луч проводим в левую сторону. Сначала определяем, пересекутся ли вообще отрезки. Затем находим абсциссу пересечения (ордината известна исходя из того, что наш луч параллелен оси абсцисс). Так как все вычисления выполняются в числах с плавающей точкой, то я привёл сразу всё к типу float. Далее восстанавливаем уравнение прямой стороны полигона, находя тангенс угла наклона - коэффициент при x и точку пересечения с осью ординат. Затем вычисляем точку пересечения с лучём. Если точка пересечения оказывается левее точки, то мы получили необходимое пересечение и отмечаем его инвентированием результирующего флага.
Оптимизированную версию этого алгоритма можно найти здесь.

Ну и далее я приведу остальные части программы.
Файл CMainWindow.java с главным окном
package polygon;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

class CPanel
extends JPanel
{

// Флаг того, что полигон был замкнут.
private boolean m_polygon_end;
// Вершины полигона.
private CPoints m_points;
// Абсциса определяемой точки.
private int m_x;
// Ордината определяемой точки.
private int m_y;
// Флаг того, что точка была установлена.
private boolean m_point_end;

public CPanel()
{
m_points = new CPoints();
m_polygon_end = false;
m_point_end = false;

CMouseHandler mouse_handler = new CMouseHandler();
addMouseListener(mouse_handler);
addMouseMotionListener(mouse_handler);
addKeyListener(new CKeyHandler());
setFocusable(true); // Для обработки клавиатурных сообщений.
setBackground(Color.white);
}

@Override public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D graphics = (Graphics2D)g;

if(m_polygon_end)
{
graphics.drawPolygon(m_points.getXArray(),
m_points.getYArray(), m_points.count());
if(m_point_end)
graphics.fillOval(m_x - 5, m_y - 5, 10, 10);
}
else
{
graphics.drawPolyline(m_points.getXArray(),
m_points.getYArray(), m_points.count());
}
}


private class CMouseHandler
extends MouseAdapter
{

@Override public void mouseClicked(MouseEvent event)
{
if(!m_polygon_end)
{
if(event.getButton() == MouseEvent.BUTTON1)
{
m_points.push(event.getX(), event.getY());
repaint();
}
else if(event.getButton() == MouseEvent.BUTTON3)
{
// В полигоне не может быть меньше трёх точек.
if(m_points.count() >= 4)
{
m_points.pop(); // Убираем хвост.
m_polygon_end = true;
repaint();
}
}
}
else if(event.getButton() == MouseEvent.BUTTON1)
{
m_x = event.getX();
m_y = event.getY();
m_point_end = true;
repaint();
String msg = CDeterminant.determine(m_points, m_x, m_y) ?
"Точка лежит внутри полигона" :
"Точка лежит вне полигона";
JOptionPane.showMessageDialog(null, msg, "Положение точки",
JOptionPane.INFORMATION_MESSAGE);
}
}

@Override public void mouseMoved(MouseEvent event)
{
if(!m_polygon_end)
{
m_points.pop();
m_points.push(event.getX(), event.getY());
repaint();
}
}
} // private class CMouseHandler

private class CKeyHandler
extends KeyAdapter
{

@Override public void keyPressed(KeyEvent event)
{
if(event.getKeyCode() == KeyEvent.VK_ESCAPE)
{
m_points = new CPoints();
m_point_end = false;
m_polygon_end = false;
repaint();
}
}
} // private class CKeyHandler
} // class CPanel

public class CMainWindow
extends JFrame
{

public CMainWindow()
{
setTitle("Определение принадлежности точки полигону");
setSize(800, 600);
setBackground(Color.white);

getContentPane().add(new CPanel());
}
}
И файл, запускающий всё это добро - CMain.java:

package polygon;

public class CMain
{

public static void main(String params[])
{
CMainWindow wnd = new CMainWindow();
wnd.setDefaultCloseOperation(CMainWindow.EXIT_ON_CLOSE);
wnd.setVisible(true);
}
}