next up previous contents
Next: Extensões Up: Camada de negócios Previous: Camada de negócios   Sumário

Núcleo



2.3.1.1 Introdução

O núcleo surgiu como conseqüência natural da filosofia de compartimentação de funcionalidades adotada no projeto. Foi formulado, inicialmente, como sendo a camada mais interna na arquitetura do sistema - seu papel seria o de prover funcionalidades básicas, tais como a criação, gerenciamento e atualização dos modelos de fluxo, através de primitivas pouco abstratas.

Logo no início do processo de modelagem, todavia, notamos ser o núcleo possuidor de uma grande vocação a biblioteca independente, já que fornece uma interface bem definida e um conjunto de primitivas genéricas, sendo completamente desacoplado do restante do sistema. Tais características permitiriam o seu uso em qualquer projeto que necessitasse de funcionalidades de workflows básicas ou simulações de fluxos (fluxos e workflows serão utilizados aqui como palavras equivalentes).

O núcleo provê, essencialmente, as seguintes funcionalidades:

Além disso, existe uma coleção de beans de sessão (que cumprem o papel de fachadas), criados para uso exclusivo da biblioteca em ambientes gerenciados (contêiners J2EE, especificamente).

Figura 4: Núcleo e suas fachadas para uso em ambiente J2EE.
\includegraphics[scale=1]{fig1.ps}



Conceitos básicos

Existem muitas maneiras conhecidas para representar workflows - a maior parte delas envolve grafos; a grande maioria cumpre bem os seus propósitos de modelagem. O problema encontrado em muitas dessas representações está, portanto, não nas representações em si, mas no fato delas serem heterogêneas e restritas a nichos ou grupos de desenvolvimento. Isso gera uma porção de dificuldades com relação às ferramentas de análise empregadas, que diferem significativamente de uma representação para outra. Além disso, certos modelos são mais próprios para determinados tipos de análise do que outros.

Ferramentas de análise são importantes na medida em que tornam possível avaliar um fluxo quanto a diversos aspectos qualitativos e quantitativos antes de colocá-lo em prática (evitando, potencialmente, prejuízos à empresa utilizando o sistema de workflows).

A escolha do nosso modelo interno de representação levou em conta, ainda, os seguintes aspectos:

Após estabelecidas as metas, teve início um processo de pesquisa e seleção do modelo. A representação que melhor se enquadrou nos nossos requisitos foi a das Workflow Networks, ou WF-nets.



WF-Nets[3]

WF-Nets são, essencialmente, Redes de Petri acrescidas de algumas particularidades. De maneira semelhante às Redes de Petri clássicas, as WF-Nets possuem lugares, transições, arestas (com peso) e tokens.

Formalmente, temos:

Definição 1 (WF-Net): Uma Rede de Petri PN é uma WF-Net (WorkFlow net) se, e somente se:

  1. PN tem dois lugares especiais: $i$ e $o$, tais que $i$ é uma fonte; isto é, $\bullet i = \emptyset$ e $o$ é um sorvedouro; isto é, $o \bullet = \emptyset$.
  2. Se adicionarmos uma transição $t^*$ a PN que conecte o lugar $i$ ao lugar $o$ (i.e. $\bullet t^* = \{o\}$ e $ t^* \bullet = \{i\}$), a Rede de Petri resultante será fortemente conexa.

Esta breve introdução não tem como finalidade discutir Redes de Petri clássicas. Uma discussão mais detalhada a respeito da teoria envolvida pode ser encontrada em [3]. Uma explicação pertinente, no entanto, diz respeito à maneira pela qual modelamos os construtos de roteamento e sincronização, mais especificamente os AND-splits, AND-joins, OR-splits, OR-joins e XOR-joins.

Optamos por permitir, nossas WF-Nets, que as arestas que partem de transições para lugares possuam, associadas a elas, expressões lógicas cujos átomos são propriedades. Essas propriedades assumem valores do tipo verdadeiro ou falso, que serão utilizados para avaliar a expressão associada. Durante o disparar de uma transição (onde tokens são consumidos do(s) lugar(es) de origem e produzidos no(s) lugar(es) de destino), apenas aquelas arestas cujas expressões forem determinadas verdadeiras produzem tokens.

Além disso, as arestas possuem também um peso (inteiro positivo) associado a elas. Esse peso determina quantos tokens são consumidos e/ou produzidos no disparar de uma transição. Temos, portanto:

Figura 5: Construtos de roteamento.
\includegraphics[scale=1]{fig2.ps}

É importante notar que os construtos OR-Split e XOR-Join dependem fortemente do valor assumido pelas propriedades associadas a eles. Isso pode ser visto como uma metáfora da cor do token nas Redes de Petri Coloridas - no nosso caso, ao invés do token possuir uma cor que determina o seu roteamento através da rede, temos propriedades externas ``trocando a cor do token'' a todo instante, conforme variam dinamicamente no curso de execução de um processo.

Vale ainda dizer que o construto XOR-Join pode ser modelado de inúmeras formas e que o seu correto comportamento depende da correta manipulação das propriedades conforme a evolução do sistema.

Segue abaixo um pequeno exemplo das WF-Nets sendo postas em prática na modelagem de um processo hipotético (empresa de gerenciamento de software):

Figura 6: Um exemplo de uma WF-Net. Lugares são círculos, transições são quadrados.
\includegraphics[scale=1]{fig3.ps}

Neste exemplo, um token que se encontra no lugar C irá passar, mediante uma transição, para o lugar D, se e somente se a expressão ${\bf (a \& b)}$ for verdadeira; isto é, se a for verdadeira (cliente aprova projeto) e b for verdadeira (gerente aprova projeto). Caso contrário, se !b ou !c forem verdadeiros (isto é, o cliente OU o gerente não aprovam o projeto), a transição irá consumir o token em C e produzir um token em D. O estado da WF-Net num dado instante é representado pelo conjunto dos tokens e suas respectivas posições (em quais lugares eles se encontram). Quanto às transições, podem ser disparadas tanto por ação do usuário (por exemplo, o usuário completa um item de trabalho como o apresentado na transição "registra dados") quanto pelo expirar de um timer (timeouts). Para o núcleo isso não faz diferença, todas as entidades que participam de interações com as WF-Nets são representadas através de recursos (resources). Recursos podem representar usuários, grupos ou até mesmo timers e aplicações - o significado é dado pelas camadas mais externas. Como nas Redes de Petri clássicas, transições (na verdade workitems, como veremos mais adiante) só são ativadas mediantes à presença de um número suficiente de tokens nos lugares conectados a ela em leque de entrada (fan-in). Por outro lado, contrário às Redes de Petri clássicas, transições não são disparadas aleatoriamente - requerem, como descrito acima, a participação de um terceiro elemento, uma resource. Uma resource deve possuir permissão para disparar uma dada transição. Segue abaixo o diagrama de transição de estados para um item de trabalho (workitem), cujo papel é associar as transições (dadas num template) a uma dada instância de um template de fluxo de trabalho. Isso quer dizer que, sempre que uma transição for ativada numa dada instância de um processo (template), será gerado um workitem (ou talvez mais de um) relacionado àquela transição, para que o usuário competente possa cumprí-lo.

Figura 7: possíveis estados para um item de trabalho
\includegraphics[scale=1]{fig4.ps}

O núcleo é, portanto, o responsável por criar, manter e fornecer acesso a todas essas informações.



2.3.1.2 Pequena documentação

Esta seção procura expor, de maneira sucinta e objetiva, o projeto e o estado atual de desenvolvimento do núcleo. A ênfase será dada na API e no modelo de classes, majoritariamente através da discussão de exemplos.



Geral

O núcleo está localizado no pacote br.com.easyflow.engine e é composto por quatro subpacotes, sendo o subpacote core.managers o que agrega os componentes mais relevantes (o restante dos pacotes contêm interfaces, classes utilitárias e ferramentas). O diagrama de classes abaixo revela a porção mais significativa do desenho:

Figura 8: UML do subpacote core.managers
\includegraphics[scale=0.95]{fig5.ps}

O modelo foi organizado de acordo com uma estrutura hierárquica de classes gerentes ou managers, apresentando fachadas e uma cadeia de fábricas que se distribuem sobre múltiplos participantes. A vantagem dessa divisão vem sob a forma de classes reduzidas e de uma compartimentação natural dos métodos de acordo com a sua funcionalidade, resultando numa API mais simples e mais intuitiva.

Uma outra razão importante para a divisão hierárquica está relacionada ao conceito de persistência por transitividade de um modelo de objetos. Dizemos que uma API ou um arcabouço de persistência implementa persistência por transitividade quando, ao persistir um objeto, o arcabouço persiste também o fecho transitivo de todas as referências alcançáveis partindo-se de. Cabe aqui uma digressão interessante a respeito do uso de arcabouços e API's de persistência de modelos de objetos. Embora sejam ferramentas cujo objetivo maior é livrar o programador do fardo dos mapeamentos objeto-relacionais, muitas das especificações e softwares presentes no mercado apresentam falhas que não permitem seu uso enquanto forma de persistência transparente dos modelos (isto é, os mapeamentos objeto-relacionais podem necessitar de alguns ajustes que derrubam a transparência do mapeamento). Com o passar do tempo, todavia, os programadores adquirem o mau-hábito de inserir recortes de código para manipulação direta da base de dados relacional, porque julgam ser mais fácil ou simplesmente por costume. Embora isso possa parecer algo inócuo, é importante notar que viola a filosofia do trabalho orientado a objetos e, com o passar do tempo, pode gerar código tão ou até mais difícil de manter do que sem o uso da API ou arcabouço de persistência de modelo de objetos. Tendo isso em mente, procuramos desenvolver um modelo que nos permitisse utilizar o arcabouço de persistência como único meio de persistência; isto é, sem enxertos de código para manipulação da base de dados. O JPOX, implementação de código aberto da especificação JDO 1.0, serviu bem aos nossos propósitos, implementando persistência por transitividade e recuperação automática de referências. Tendo tudo isso em mente, fica fácil inferir o quanto uma estrutura hierárquica colabora com o nosso intuito:

Figura 9: Hierarquia no modelo de objetos
\includegraphics[scale=1]{fig6.ps}

A estrutura que lembra uma árvore permite a persistência transparente de todo o modelo de objetos através de uma única chamada da API que torna persistente a instância única de RuntimeEngineFactory. O restante dos objetos são persistidos por transitividade, de forma transparente. Obviamente o modelo foi concebido de maneira a existir sempre um caminho entre a instância única de RuntimeEngineFactory e qualquer instância do modelo de objetos.

Para reduzir ainda mais a preocupação com persistência e poupar o usuário da biblioteca do fardo de ter de conhecer a API JDO [4], a instância única de RuntimeEngineFactory, que implementa uma semântica de singleton enfraquecida (como veremos mais adiante), requer apenas que sejam passados ao seu método getFactoryInstance (fig. 10) alguns parâmetros bem documentados que dizem respeito à configuração da base de dados (é razoável supor que o usuário da biblioteca detenha tais informações), sob a forma de um objeto da classe PersistenceManagerFactory.



RuntimeEngineFactory

Como mencionamos anteriormente, a classe RuntimeEngineFactory implementa a semântica de um singleton enfraquecida:

Figura 10: RuntimeEngineFactory
\includegraphics[scale=1]{fig7.ps}

Essa semântica de singleton enfraquecida se dá por meio de uma associação feita entre uma base de dados e uma instância do singleton. Isso significa, para fins práticos, que é permitida no máximo uma instância da RuntimeEngineFactory por base de dados (daí a semântica ser enfraquecida). Como a cada base de dados está associada uma única PersistenceManagerFactory, que é uma classe JDO que pode ser instanciada partindo-se da configuração da base de dados, para recuperar todas as RuntimeEngines guardadas em uma base de dados basta chamar o método estático getFactoryInstance tendo como parâmetro a PersistenceManagerFactory para essa base de dados:

Properties properties = new Properties();


// Propriedades da base de dados e da implementação JDO
properties.setProperty("javax.jdo.PersistenceManagerFactoryClass",
"com.triactive.jdo.PersistenceManagerFactoryImpl");

properties.setProperty("javax.jdo.option.ConnectionDriverName",
"com.mysql.jdbc.Driver");

properties.setProperty("javax.jdo.option.ConnectionURL",
"jdbc:mysql//localhost:3306/giuliano");

// Gera a PersistenceManagerFactory a partir das propriedades da
// base de dados.
PersistenceManagerFactory pmfactory =
JDOHelper.getPersistenceManagerFactory(properties);

// Obtém o singleton da RuntimeEngineFactory para esta base de dados
RuntimeEngineFactory ref =
RuntimeEngineFactory.getFactoryInstance(pmfactory);

Note que, no conjunto de propriedades que configura a PersistenceManagerFactory, existe uma chave de nome javax.jdo.PersistenceManagerFactoryClass. O valor que faz par com essa chave é quem determina qual implementação JDO, das implementações disponíveis, será utilizada. A PersistenceManagerFactory é criada então pela classe JDOHelper, que é distribuída pela própria Sun Microsystems e não faz parte de nenhuma implementação.

Qualquer implementação JDO que siga o padrão 1.0 deve funcionar sem maiores problemas com o núcleo. No entanto, embora possível e certamente permitido, não é aconselhado misturar implementações, ao menos não numa mesma base de dados.



Uso da API

Agora que já discutimos como obter a instância da RuntimeEngineFactory, é chegada a hora de explorar um pouco melhor a API fornecida pelo núcleo. A RuntimeEngineFactory permite ao usuário criar uma nova RuntimeEngine, que é o objeto que serve como ponto de entrada para as fachadas internas.

A partir da RuntimeEngine, podemos acessar os três gerentes:

Com apenas esses três objetos, é possível criar e destruir grupos e usuários, criar novos processos e determinar as tarefas de qualquer grupo ou usuário em qualquer instante. No caso de um trabalho teórico, seria necessário discorrer sobre os conceitos em abstrato. Felizmente, por este texto tratar da documentação de uma implementação, podemos dar um exemplo antes de seguir adiante.

Exemplo 1:

// 1 - Cria uma RuntimeEngine
IdentityKey engineKey1 = new StringIdentityKey("Engine 1");

RuntimeEngine rte = rtf.createEngine(engineKey);

// 2 - Cria dois recursos participantes, o primeiro representa um 
// grupo, o segundo representa um usuário.
ResourceManager rm = rtf.getResourceManager();

Resource r1 = rm.newResource(new StringIdentityKey("Users group");
Resource r2 = rm.newResource(new StringIdentityKey("Giuliano Mega");

// Adiciona o usuário ao grupo.
r1.addResource(r2);

// 3 - Cria uma WF-Net simples.
CaseTypeManager ctm = rte.getCaseTypeManager();

CaseType ct1 = ctm.newCaseType(new StringIdentityKey("Linear"));

Place a = ct1.newPlace(new StringIdentityKey("a"));
Place b = ct1.newPlace(new StringIdentityKey("b"));
Place c = ct1.newPlace(new StringIdentityKey("c"));

Transition t1 = ct1.newTransition(new StringIdentityKey("A"));
Transition t2 = ct1.newTransition(new StringIdentityKey("B"));

t1.bindResource(r1);

StringIdentityKey edge1 = new StringIdentityKey("<a,A>");
StringIdentityKey edge2 = new StringIdentityKey("<A,b>");
StringIdentityKey edge3 = new StringIdentityKey("<b,B>");
StringIdentityKey edge3 = new StringIdentityKey("<B,c>");

ct1.newEdge(a, A, 1, edge1);
ct1.newEdge(A, b, 1, edge2);
ct1.newEdge(b, B, 1, edge3);
ct1.newEdge(B, c, 1, edge4);

ct1.setInitialPlace(a); // Lugar onde se "inicia" o processo
ct1.setEndingPlace(c);  // Lugar onde se encerra o processo

// 4 - Registra o validador padrão para esta WF-Net
ct1.registerValidator(new DefaultPetriGraphValidator());

// 5 - Cria um conjunto de propriedades
Property p1 = new BooleanProperty(new StringIdentityKey("p1"), false);
Property p2 = new BooleanProperty(new StringIdentityKey("p2"), false);
Property p3 = new BooleanProperty(new StringIdentityKey("p3"), true);

PropertyPool pp = new PropertyPool();
pp.registerProperty(p1, "a");
pp.registerProperty(p2, "b");
pp.registerProperty(p3, "c");

PropertyString ps = new PropertyString("!(a | b) & c", pp);

Map propertyMap = new HashMap();

propertyMap.put(edge1, ps);

// 6 - Cria um mapa que representa um estado possível para a WF-Net
Map tokenMap = new HashMap();
tokenMap.put(a.getId(), new Integer(1));

// 7 - Antes de instanciar o processo, é necessário validá-lo
ct1.validate();

// 8 - Instancia o processo, sendo que o estado inicial desta instância 
// é determinado pelo mapa de Tokens e pelo mapa de Propriedades.
ConcreteCase caseInstance = ct1.getCaseInstance(
new StringIdentityKey("Process 1"), propertyMap, tokenMap);

Uma discussão do exemplo acima será bastante elucidativa com relação à organização da API.

Inicialmente, cria-se uma RuntimeEngine para que seja possível acessar os três gerentes, como discutido anteriormente. Obviamente não é necessário criar uma RuntimeEngine sempre que se desejar obter acesso aos três gerentes já que, na maior parte dos casos, o que se deseja fazer é recuperar uma RuntimeEngine pré-existente.

Neste caso, podemos substituir a primeira parte por:

RuntimeEngine rte = rtf.getRuntimeEngineByID( new StringIdentityKey("Engine 1"));

Que recupera a RuntimeEngine cuja chave é "Engine 1" (fica a cargo do usuário guardar qual a chave de sua RuntimeEngine, ou ele pode usar um FactoryIterator, que não será documentado aqui).

O exemplo prossegue criando dois recursos, r1 e r2 que, embora sejam tratados pelo núcleo de maneira indiferenciada, representam entidades semanticamente distintas. Isso porque o padrão composite, solução de design bastante elegante para casos como este, permite tais construções:

Figura 11: A classe Resource.
\includegraphics[scale=1]{fig8.ps}

Usuários passam a ser tratados, portanto, sob a forma de grupos vazios. Dessa maneira, os usuários podem ser incluídos em múltiplos grupos e os grupos podem ser incluídos em outros grupos, tudo isso por meio de um modelo bastante simplificado.

Um objeto da classe Resource é capaz de informar quais são os grupos do qual ele participa através do método getRoleList, característica essencial para o controle de permissões no sistema de gerenciamento de workflows. Além disso, é possível determinar se um dado usuário participa ou não num dado grupo através do método containsResource (que, embora seja redundante, facilita bastante a vida do programador).

Nas linhas que seguem à criação dos novos recursos e organização dos grupos (seção 3, "Cria um WF-Net simples") é demonstrado o uso da API de modelagem do núcleo. É importante notar que os objetos da classe CaseType incorporam o conceito de metaclasse; isto é, cada instância de CaseType possui um valor semântico comparável ao de uma classe estática. A vantagem do modelo orientado a instâncias é que as (meta)classes podem ser criadas dinamicamente, no nosso caso por meio da interface do próprio CaseType.

Conceitualmente, CaseType incorpora algumas das idéias utilizadas num modelo adaptativo de objetos ou AOM. Mais especificamente, a classe CaseType e a interface ConcreteCase constituem uma aplicação de uma variante do padrão TypeSquare [6]. Isso porque um objeto do tipo CaseType, sendo uma metaclasse, é capaz de gerar metaobjetos ou simplesmente objetos (metaobjetos e objetos não são radicalmente diferentes). Como nas linguagens orientadas a objeto convencionais, existe um vínculo fundamental que se forma entre os objetos de uma classe e a própria classe, sendo que as informações que não variam entre instâncias individuais são centralizadas na metaclasse.

Essa centralização, no nosso modelo, se dá por meio do uso do padrão flyweight. A informação intrínseca, no nosso caso, é dada, por exemplo, pela estrutura do grafo, pelos identificadores de arestas e pelos bindings de recursos; já a informação extrínseca é proveniente do estado de cada uma das instâncias num dado instante. A aplicação do padrão faz com que não seja necessário replicar a informação intrínseca, que é razoavelmente extensa, em todas as metainstâncias, de uma maneira similar às linguagens orientadas a objeto - a implementação binária dos métodos, por exemplo, não é replicada em cada objeto instanciado. Uma discussão do padrão flyweight, informações intrínsecas e extrínsecas pode ser encontrada em [10].

A porção do exemplo que modela o grafo é razoavelmente auto-explicativa - são apenas chamadas aos métodos de CaseType com o intuito de compor um grafo dotado de lugares, transições e arestas (uma WF-Net). Note que a distribuição inicial dos tokens não é parte do CaseType, que pode ter múltiplas instâncias, cada qual com a sua distribuição própria de tokens. Também não faz parte do CaseType o conjunto de objetos que descreve as propriedades associadas a cada aresta, isso porque pode haver casos em que se deseja um conjunto disjunto de propriedades associado a cada instância do CaseType.

Figura 12: Conjuntos de propriedades separados por instância.
\includegraphics[scale=1]{fig9.ps}

Na figura acima, a transição t1 encontra-se ativa somente na Instância 2. Na Instância 1, em virtude do valor da expressão "!a" associada à aresta que parte de t1 ser falso, há um bloqueio à passagem dos tokens, inviabilizando a transição.

Com relação às propriedades que podem ser associadas a cada aresta, o modelo é bastante flexível - basta que uma determinada classe implemente a interface Property para que seus objetos possam ser utilizados como propriedades. Com relação aos exemplos dados, a nossa famosa expressão lógica é um exemplo de tal classe:

Figura 13: PropertyString e PropertyPool
\includegraphics[scale=1]{fig10.ps}

Aqui, temos a classe PropertyString que implementa um parser de expressões lógicas e se utiliza do repositório de símbolos em PropertyPool sempre que precisa resolver um símbolo num átomo. Em outras palavras, na string "!a", a classe PropertyString iria, no momento de avaliar a expressão, buscar o objeto que implementa a interface Property no repositório de símbolos PropertyPool a ele associado. A partir daí, PropertyString iria determinar o valor de a e então finalmente determinar o valor de !a.

Seguindo adiante no exemplo vemos um exemplo de tal procedimento. As linhas:

PropertyPool pp = new PropertyPool();
pp.registerProperty(p1, "a");
pp.registerProperty(p2, "b");
pp.registerProperty(p2, "c");

PropertyString ps = new PropertyString("!(a | b) & c", pp);

Map propertyMap = new HashMap();

propertyMap.put(edge1, ps);

Criam um PropertyPool, registram nele as propriedades p1, p2 e p3 sob os símbolos a, b e c, criam um PropertyString (cuja string de expressão é begintex2html_wrap_inline$!(a | b) & c$" e associam o PropertyPool à PropertyString. O HashMap propertyMap contém associações entre chaves de arestas do CaseType e classes que implementam a interface Property. No momento em que o CaseType ct1 for instanciado, esse mapa, juntamente com um mapa que descreve a configuração inicial dos tokens (vide o exemplo), serão os responsáveis por definir os objetos-propriedade de cada aresta e o estado inicial da instância do processo.

Afora o comentado, restam quatro linhas no exemplo que não foram mencionadas:

ct1.setInitialPlace(a); // Lugar onde se "inicia" o processo
ct1.setEndingPlace(c);  // Lugar onde se encerra o processo

...

ct1.registerValidator(new DefaultPetriGraphValidator());

...

ct1.validate();

As duas primeiras linhas determinam o lugar inicial e final do processo. O lugar inicial sempre começa com um token, independentemente do mapa de distribuição de tokens passado como parâmetro (exceto no caso do usuário declarar explicitamente que o lugar inicial deve ter 0 tokens). Idealmente, processos novos são acompanhados de um mapa de distribuição de tokens vazio e apresentam apenas um token no lugar inicial. O recurso do mapa de tokens existe para permitir ao usuário instanciar um processo num ponto específico de sua evolução, mas tecnicamente é mais correto usar o lugar inicial (ver [3]). O lugar inicial deve sempre ser uma fonte.

O lugar final, por sua vez, é o lugar onde os tokens param. Uma vez que os tokens estejam todos no lugar final, a instância de um processo (ou CaseType) termina. O lugar final deve obrigatoriamente ser um sorvedouro.

A terceira linha registra o algoritmo de validação padrão no CaseType. Essa linha foi incluída por motivos meramente didáticos, uma vez que, por default, todos os CaseTypes contêm o DefaultPetriGraphValidator registrado na sua lista de algoritmos de validação. O algoritmo de validação padrão faz algumas checagens simples, tais como determinar se o grafo é conexo ou se o lugar inicial é uma fonte e o lugar final um sorvedouro. Análises mais sofisticadas (ausência de loops infinitos e deadlocks, por exemplo) podem ser conduzidas por outros algoritmos, basta que sejam registrados no CaseType através do método registerValidator.

A quarta e última linha executa a validação do CaseType. Note que instâncias de um CaseType só podem ser criadas se a etapa de validação for completada com sucesso (se o usuário tentar criar uma instância antes de validar um CaseType, uma exceção do tipo WFUserException será lançada).

Encerramos esta seção com um segundo exemplo, bastante simples quando comparado ao primeiro, que ilustra como uma aplicação do tipo do sistema de gerenciamento de workflows poderia, após ter autenticado um usuário, determinar quais tarefas estão disponíveis naquele instante para aquele usuário.

Exemplo 2:

$<$Suponha que exista uma referência para a RuntimeEngine correta numa variável de nome rte e que ao usuário autenticado corresponda uma instância da classe Resource de nome user$>$

WorkitemManager wim = rte.getWorkitemManager();
Collection worklist = wim.getWorkitemsFor(user);

A partir da coleção retornada por getWorkitemsFor, é possível determinar a quais tarefas correspondem cada workitem e oferecê-las ao usuário da maneira mais conveniente.



Conclusão

O núcleo é uma biblioteca que, embora desenvolvida com enfoque nas necessidades de workflow do sistema de gerenciamento de workflows, atua como camada independente e pode fornecer serviços básicos de workflow a qualquer aplicação que deles necessitem. A auto-persistência é uma característica intrínseca da biblioteca, que também concentra toda a lógica de modelagem e a estrutura de evolução dos processos.

Faltou uma discussão um pouco mais detalhada a respeito de alguns outros tópicos - tais como a integração da biblioteca com servidores de aplicação e até mesmo de alguns aspectos interessantes da própria API - mas a discussão dos exemplos e a UML são suficientes para que se tenha uma boa idéia de qual o papel do núcleo.

O código se encontra, para fins práticos, pronto; mas não funciona corretamente. Isso porque existem algumas limitações na implementação JDO escolhida que, embora consideradas desimportantes de início, acabaram promovidas a limitações críticas posteriormente. Tais limitações emperraram seriamente o nosso progresso - para que o código possa ser posto em uso, é necessário reescrever uma boa parte das classes, especialmente aquelas que usam mapas e sets persistentes (as implementações JDO de código aberto são bastante limitadas com relação ao uso dessas interfaces).


next up previous contents
Next: Extensões Up: Camada de negócios Previous: Camada de negócios   Sumário
Cleber Miranda Barboza 2004-02-29