Programa de desenho de grafos Alunos: Alexandre Marinho Ferreira Filipe Vieira da Silva N.º 3163 Eng. da Comunicação 4º Ano. Disciplinas: Teoria dos Grafos Paradigmas da Computação Docentes: Eng. José Torres Eng. Rui Silva Moreira Programa de desenho de grafos Descrição do trabalho realizado 1 Programa de desenho de grafos Índice Índice .......................................................................................................................................... 2 Especificação do problema ......................................................................................................... 4 Modelo Orientado aos objectos do Protótipo ............................................................................. 5 Estrutura do Protótipo................................................................................................................. 7 Conclusão ................................................................................................................................. 11 Bibliografia ............................................................................................................................... 12 2 Programa de desenho de grafos Introdução Este relatório pretende descrever o trabalho realizado para as disciplinas de Teoria dos Grafos e Paradigmas da Computação. O objectivo é a descrição e implementação de um protótipo de geração e manipulação de Grafos. No capitulo de especificação do problema será feita uma especificação funcional dos objectivos a atingir com este trabalho. Os capítulos de modelo OO associado e estrutura do protótipo constituem a maior parte deste relatório. No modelo OO será feita a descrição dos diagramas UML criados na fase de concepção do protótipo. Na estrutura do protótipo será feita uma descrição da fase de implementação, com a descrição especifica dos métodos e algoritmos implementados. Finalmente será feita uma conclusão sobre as mais valias que este trabalho proporcionou e possíveis melhoramentos futuros. 3 Programa de desenho de grafos Especificação do problema Neste projecto era pretendida a implementação de um programa que permitisse desenho e manipulação de grafos. Genericamente, a aplicação desenvolvida consegue desenhar qualquer tipo de grafo, graças á abordagem de concepção usada. Por insuficiência de tempo não conseguimos implementar um interface do utilizador que permitisse uma verdadeira interactividade com a aplicação, podendo dessa forma haver uma manipulação mais eficiente dos grafos. Como este projecto inseria-se no âmbito de uma disciplina que estuda o modelo de programação orientado aos objectos e a linguagem de programação Java, o desenvolvimento da aplicação foi feito usando uma abordagem orientada aos objectos e utilizando a linguagem de programação Java. Para a concepção do modelo de classes da aplicação foi usada a ferramenta de modelação gráfica, Rational Rose para Java baseada na Unified Modelling Language (UML), linguagem sucessora da Object Modelling Technique(OMT) dos mesmos autores. Esta ferramenta CASE(computer assisted software engeneering) permite fazer a concepção orientada a objectos, de alto nível do modelo de classes da aplicação, dos seus atributos, interrelacionamentos (relacionamentos simples, agregações, heranças, etc) e métodos, gerando posteriormente o código correspondente constituído pelo esqueleto das classes e dos seus métodos. Na fase seguinte passamos á implementação propriamente dita da aplicação, utilizando, como já foi referido, a linguagem Java. Foram implementadas funcionalidades para conseguir um controlo total sobre as propriedades dos grafos a desenhar. Nomeadamente foram consideradas aspectos como cores, conteúdo dos nós, suporte para grafos pesados e digrafos, etc... Foram posteriormente feitos testes para avaliar as funcionalidades da aplicação. 4 Programa de desenho de grafos Modelo Orientado aos objectos do Protótipo Na figura seguinte apresenta-se o diagrama UML de classes da aplicação, que será descrito pormenorizadamente a seguir: Grafo m _idGrafo : int = 0 m _areaDesenho : AreaDes enho No m _idNo : int = 0 m _dados No : String m _corNo : Color m _x : int = 0 m _y : int = 0 m _pertenceGrafo : Grafo No( ) getIdNo( ) getDados No( ) getCorNo( ) getX( ) getY( ) changeDados No( ) des enharNo( ) m overNo( ) 2 lis taNos * pertenceGrafo Grafo( ) getIdGrafo( ) ins erirNo( ) ins erirNo( ) apagarNo( ) encontrarNo( ) ins erirRam o( ) ins erirRam o( ) encontrarRam o( ) apagarRam o( ) rem oveNo( ) rem oveRam o( ) getAreaDes enho( ) des enharGrafo( ) gravarGrafo( ) ligaNos Tes teApp m ain( ) AreaDes enho grafo : Grafo = Null larguraAreaDes enho : int = 500 alturaAreaDes enho : int = 500 AreaDes enho( ) paint( ) ligaGrafo lis taRam os * Ram o m _idRam o : int = 0 m _corRam o : Color m _pes o : int = 0 s eta : boolean = true lis taRam os * Ram o( ) getNos ( ) getIdRam o( ) getCorRam o( ) getSeta( ) pes o( ) des enharRam o( ) As 3 classes principais são: a classe Grafo, que contem todas as propriedades e operações que podem ser feitas num grafo. Basicamente é constituído por um identificador, métodos para adicionar/remover nós e ramos, e métodos adicionais para desenhar ou gravar o grafo. Contem duas relações fundamentais entre a classe Ramo e a classe No. Um grafo é constituído por agregações de Ramos e Nós representadas aqui pelas associações listaRamos e listaNos. A classe No, que contem igualmente todas as suas propriedades e operações possíveis que um nó pode ter. Tal como as outras 2 classes principais, um nó também tem o seu próprio identificador, tendo adicionalmente coordenadas de localização na área de desenho, cor própria e opcionalmente armazena dados. A maior parte dos métodos desta classe servem para operar as suas variáveis membro, tendo tal como as outras 5 Programa de desenho de grafos classes, o seu próprio método desenhar. Existe ainda a possibilidade de mover um nó graças ao respectivo método. Finalmente a classe Ramo tem atributos e métodos semelhantes ao Nó. Tem uma relação que posteriormente na implementação, torna-se muito importante: um ramo é uma ligação entre 2 nós, havendo pois uma relação de N para 2 entre ramos e nós. Tem ainda dois atributos adicionais importantes, peso e seta. O atributo peso é usado para determinar se estamos perante um grafo pesado, ou seja, é o peso de um determinado ramo. Se o peso for igual a 0, trata-se de um grafo convencional. A seta é usada para determinar se o ramo tem um sentido, ou seja, se é o ramo de um digrafo. Caso seja, o sentido é de No1 para No2. Existem ainda duas classes secundarias: AreaDesenho, que é responsável pela interacção entre as classes anteriores e a área de desenho, conhecida em java por canvas. Tem, para alem do construtor, um único método paint, responsável pelo desenho de figuras geométricas, propriamente dito. Do ponto de vista da nossa aplicação, essas figuras geométricas, vão ser pequenas circunferências, no caso dos nós, e segmentos de rectas no caso dos ramos. TesteApp que não tem atributos nem construtor, possuindo apenas um método main(). Esta classe é responsável por executar toda a aplicação usando as outras classes. Uma 3ª classe a implementar seria a UserInterface, responsável pelo interface do utilizador e pela interactividade com o mesmo, respondendo a eventos. Esta classe teria relações com TesteApp e Grafo e provavelmente com AreaDesenho. Infelizmente por falta de tempo, foi inviável esta implementação. 6 Programa de desenho de grafos Estrutura do Protótipo Tendo em conta que a linguagem de programação usada foi Java, tivemos de considerar alguns aspectos particulares desta linguagem. Uma das características particulares mais evidentes foi a utilização do Canvas. Em java o Canvas é uma classe do AWT(Abstract Window Tookit) que corresponde a uma área de desenho . O Canvas tem relativamente poucos métodos associados e não pode ser acedido directamente. Para o utilizar temos que criar uma classe (neste caso AreaDesenho), que redefine um dos métodos mais importantes do Canvas, o paint(). Este método é responsável por definir as propriedades do Canvas a usar e todas as operações (métodos invocados dentro do paint()) que podem ser realizadas no Canvas. Na nossa classe AreaDesenho, o paint() é redefinido para desenhar o Canvas e invocar o método Grafo.desenharGrafo() que irá desenhar o grafo propriamente dito. Especificamente, para que as operações de desenho possam ser feitas usa-se uma classe independente do Canvas, a Graphics que é uma abstracção de todo o conjunto de operações que pode ser feito num canvas através do paint(), entretanto já redefinido e usado implicitamente. No nosso projecto usamos o Graphics para desenhar circunferências e segmentos de recta, correspondentes aos nós e grafos. Estes métodos são invocados quando pretendemos especificamente usar os métodos No.desenharNo() e Ramo.desenharRamo(), que podem ser acedidos directamente, ou mais frequentemente através do método Grafo.desenharGrafo(). Na classe Grafo (tal como nas classes No e Ramo), existem muitos métodos bastante simples que pura e simplesmente resumem-se a leitura das suas variáveis membro. No entanto há alguns mais complexos. Quer os nós, quer os ramos, estão armazenados em 2 vectores de objectos respectivamente m_listaNos e m_listaRamos Para criar nós e ramos podemos optar por duas soluções. 7 Programa de desenho de grafos Podemos criar os nós e os ramos invocando os construtores das respectivas classes e depois invocarmos respectivamente os métodos inserirNo() e inserirRamo(), ou então usarmos estes métodos para fazer tudo numa única chamada. Como cada um destes 2 métodos tem duas implementações, estamos perante um exemplo de polimorfismo em Java. Se fizermos tudo numa única chamada, o inserirRamo() invoca o Grafo.encontrarNo() para determinar a quais nós irá estar ligado. O Grafo.encontrarNo() e o Grafo.encontrarRamo() procuram ciclicamente, nos respectivos vectores de nos/ramos qual o ramo/no que está associado á id que foi passada como parâmetro. Estes algoritmos são conseguidos á custa dos métodos da classe Vector para manipular objectos dessa classe. Os métodos apagarNo() e apagarRamo() seguem algoritmos semelhantes. Primeiro invocam o Grafo.encontrarNo() ou Grafo.encontrarRamo() para encontrarem o nó ou ramo correspondente á id que foi passada inicialmente como parâmetro. Depois simplesmente executam o método removeElementAt() da classe vector para remover o objecto encontrado. Se soubermos a localização dos nos ou ramos a apagar nos vectores respectivos, podemos usar os métodos Grafo.removeNo() e Grafo.removeRamo(), que passam como parâmetro o índice na lista de ramos/nos do objecto a apagar. Nesta classe existem ainda dois métodos importantes e independentes dos anteriores que são o Grafo.desenharGrafo() e o Grafo.gravarGrafo(). No primeiro vamos desenhar o grafo em 2 passos. No primeiro passo vamos percorrer ciclicamente o vector de nos e por cada passagem invocar o método No.desenharNo(). No segundo passo vamos seguir exactamente o mesmo procedimento para os ramos: percorrer o vector de ramos e por cada passagem invocar Ramo.desenharRamo(). O Grafo.gravarGrafo() grava num ficheiro de texto todo o conteúdo do Grafo no momento em que é invocado por TesteApp.main(). Este método invoca o método createfile() utiliza métodos da package java.io (contem todas os métodos e classes necessários para gestão de ficheiros) para escrever sequencialmente o conteúdo do grafo no ficheiro, novamente de forma cíclica. 8 Programa de desenho de grafos Este conteúdo é passado como parâmetro para o createfile() através de uma string que contem os dados estruturados do grafo. A organização desses dados é feita previamente pelo Grafo.gravarGrafo() através de dois ciclos que percorrem o vector de nos e o vector de ramos lendo as variáveis membro desses nos e ramos e colocando-as na string final, que irá ser passada como parâmetro. Na classe No a maior parte dos métodos resume-se á leitura/escrita simples das variáveis membro. Os dois métodos mais importantes são No.desenharNo() e o No.moverNo(). O No.desenharNo() utiliza os métodos do objecto da classe Graphics (passado como parâmetro) para atribuir-lhe uma cor e desenhar-lhe uma Elipse de eixos iguais (uma circunferência). O No.moverNo() move o No para a nova posição (x,y) passada como parâmetro e invoca o Grafo.desenharGrafo(), redesenhando o grafo, para que as alterações sejam visíveis. Para que o No se possa associar ao grafo, utiliza a variável membro m_pertenceGrafo que é inicializada pelo construtor do No como o Grafo ao qual o No pertence. Na classe Ramo existem dois aspectos importantes a salientar. O construtor ao inicializar as variáveis membro utiliza um vector com 2 elementos m_ligaNos que é constituído pelos 2 nós ao qual o ramo está ligado. Estes nós são passados como parâmetro pelo construtor. O outro aspecto importante a descrever, é obviamente o método Ramo.desenharRamo(). Tal como no No.desenharNo(), este método utiliza métodos do objecto da classe Graphics (passado como parâmetro) para atribuir uma cor ao ramo e desenha-lo. Do ponto de vista de algoritmia o ramo é um segmento de recta de um nó de origem para um nó de destino (podemos falar em “origem” e “destino” para uma implementação de digrafos). Tudo o que o método que desenha o segmento de recta (ramo) faz é desenhar um segmento de recta das coordenadas do no de origem para as coordenadas do nó de destino). Em java e noutras linguagens de programação OO, tem que existir um método main() numa classe qualquer para executar a aplicação. Neste caso essa classe é a TesteApp. Devido á inexistência de um interface de utilizador é aqui que temos que definir o que a nossa aplicação de desenho de grafos deverá fazer. Para efeitos de teste foram criados alguns nós e ramos entre esses nós, sendo depois criado um frame ao qual adicionamos o nosso canvas com os ramos e nós desenhados. 9 Programa de desenho de grafos Invocou-se ainda o método Grafo.gravarGrafo() também para efeitos de teste de gravação do ficheiro de texto com a informação do Grafo. Todos os testes foram bem sucedidos. 10 Programa de desenho de grafos Conclusão O programa de desenho de grafos, é uma demonstração simples da resolução de um problema, utilizando a modelação orientada aos objectos, e demonstrando as potencialidades da linguagem Java como linguagem OO e algumas das suas características especificas como o Canvas. Por outro lado, as vantagens da utilização de uma ferramenta CASE num projecto de software ficaram bem demonstradas com a utilização do Rational Rose para java. A modelação OO fica muito mais facilitada com esta ferramenta estruturando melhor o modelo da aplicação a desenvolver. A aplicação resultante é flexível ao ponto de conseguir englobar diversas características dos grafos, tomando como base muitas noções de teoria dos grafos, tais como os grafos pesados, ou os digrafos. Espera-se pois, que este projecto possa ser usado como base para uma aplicação mais eficaz a nível de funcionalidades disponibilizadas. Por exemplo, o passo seguinte que seria correcto tomar, poderia ser a implementação de um interface de utilizador interactivo. A nível de teoria dos grafos, o programa possui uma estrutura base que permite que futuramente sejam introduzidas melhorias tais como, por exemplo, calculo de caminhos em digrafos, usando os algoritmos estudados (que seriam praticamente o único código adicional a implementar). A nível pessoal o trabalho proporcionou-nos um melhor entendimento do modelo OO e abordagens respectivas, com um exemplo pratico bem como um melhor entendimento dos tipos de grafos existentes. 11 Programa de desenho de grafos Bibliografia Eckel, Bruce: Thinking in Java , 1998 Prentice Hall 12