Gerando diagramas UML programaticamente com yUML e Python

Neste artigo rápido, pretendo fazer uma apresentação rápida do yUML e mostrar como podemos tirar proveito da ferramenta para gerar diagramas UML da documentação de um projeto. Tudo isso de forma programática, de modo que a documentação seja reflexo da realidade do código.

yUML

O yUML é uma ferramenta de geração de diagramas UML como esboço. Ele não tem a ambição de ser ferramenta última de diagramação, mas de ajudar a criar alguns tipos de diagrama de forma facilitada. É muito útil para geração de diagramas UML-como-rascunho.

A ferramenta está disponível na forma de serviço, ou instalação on-premise. No caso do serviço, no plano gratuito, temos um namespace que permite salvar até 5 diagramas. A título de exemplo, o meu é https://yuml.me/dyegomaas/diagrams.

Mas se você não precisa ter seus diagramas salvos em nuvem, pode gerá-los via API e baixar logo em seguida. É essa abordagem que vamos explorar mais adiante.

Para criar nossos diagramas, usamos DSLs (Domain Specific Languages) especialmente projetadas para descrever esses diagramas. Três classes de diagramas são suportadas:

  • Diagramas de classe
  • Diagramas de casos de uso
  • Diagramas de atividades

Exemplos

Não vou entrar nos detalhes das DSLs, pois é fácil encontrá-los na documentação do yUML, mas vou listar alguns exemplos, tirados do próprio site do yUML.

Diagrama de casos de uso

Um diagrama de casos de uso definido assim:

[Customer]-(Sign In)
[Customer]-(Buy Products)
(Buy Products)>(Browse Products)
(Buy Products)>(Checkout)
(Checkout)<(Add New Credit Card)
(Checkout)
[Office Staff]-(Processs Order)

Gera o seguinte diagrama:

Diagrama de casos de uso

Diagrama de classes

Um diagrama de casos de uso definido assim:

// Cool Class Diagram
// ------------------

// Chain elements like this
[Customer]<>-orders*>[Order]++-0..*>[LineItem]

// Add notes
[Order]-[note: Aggregate Root ala DDD{bg:wheat}]

// Add more detail
[≪IDisposable≫;Customer|+forname: string;+surname: string;-password: string|login(user,pass)]

Gera um diagrama como este:

Diagrama de classes

Diagrama de atividades

Um diagrama de atividades definido assim:

(start)-|a|
|a|->(Grind Coffee)->(Pour Shot)->(Froth Milk)->(Pour Coffee)->|b|
|a|->(Fry Eggs)->(Make Toast)->(Butter Toast)->|b|
|b|-><c>[want another coffee]->(Grind Coffee)
<c>[ready to go]->(end)

Gera este diagrama aqui:

Diagrama de classes

Gerando um diagrama de casos de uso com yUML

O jeito mais simples para gerar um diagrama de caso de uso, é via requisição REST. Para isso, basta realizar um POST para a URL https://yuml.me/diagram/scruffy/usecase/, cujo corpo siga o seguinte formato:

{
    "dsl_text": "[Usuario]-(ObterFilial),[Usuario]-(PersistirFilial)"
}

Esta requisição vai retornar o nome do arquivo gerado. No meu caso, retornou b55a8893.svg, como pode ser visto na imagem abaixo:

Diagrama de classes

Requisição para gerar um diagrama de casos de uso com visual Scruffy

O código do arquivo gerado é b55a8893, e com ele, podemos fazer download do diagrama gerado com um um GET em https://yuml.me/b55a8893.png. Note que eu mudei a extensão. É possível baixar nos formatos .svg, .png, .jpg, .svg e .pdf, apenas mudando a extensão.

No próximo tópico, apresento um exemplo bem simples onde usei Python para gerar um diagrama de casos de uso.

Gerando diagramas com yUML e Python

Para fins de experimento, vou utilizar aqui um exemplo do meu outro artigo sobre arquitetura gritante, mas as situações em que essa abordagem pode servir são inúmeras.

A seguir, vamos gerar um diagrama de casos de uso, mas seria igualmente simples montar um diagrama de classes lendo os fontes de um projeto.

Neste exemplo específico, tenho um projeto C# chamado Clientes.Application. O primeiro nível de pastas, a exceção do Common, descreve entidades de domínio, e o segundo nível de pastas descreve operações de negócio que afetam ou interagem com essas entidades. Cada operação dessas mapeia um caso de uso.

Uma árvore de diretórios, com as pastas de segundo nível representando casos de uso

O objetivo então é gerar um diagrama que reflita essa estrutura. Para isso, vamos usar um simples script Python.

O processo que o script precisa realizar é o seguinte:

  1. Identificar os diretórios de segundo nível
  2. Monstar a DSL descrevendo cada um deles como Caso de Uso
  3. Requisitar a geração do diagrama
  4. Fazer download da imagem no formato desejado

A primeira etapa, é muito específica desse exemplo, então não vou entrar em detalhes, mas você pode conferir no script completo no final do artigo.

Na segunda etapa, montamos a DSL considerando que cada pasta é um caso de uso:

def build_usecase_dsl_for_directories_in(usecase_directories, user_name) -> str:
    per_folder_uc = [
        f'[{user_name}]-({os.path.basename(directory)})'
        for directory in usecase_directories
    ]
    return ','.join(per_folder_uc)

O resultado dessa operação será algo como [Usuario]-(CasoUso1),[Usuario]-(CasoUso2).... Neste caso, cada par identifica uma relação.

No passo seguinte, podemos fazer uma requisição para o yUML para gerar nosso diagrama:

def request_usecase_diagram(usecase_dsl) -> str:
    r = requests.post(
        f'https://yuml.me/diagram/scruffy/usecase/',
        { "dsl_text": usecase_dsl }
    )
    svg_file_name = r.text
    return svg_file_name

Esta operação vai retornar o nome único do nosso diagrama lá no yUML. O nome do arquivo vem com a extensão .svg por padrão.

Vale notar que no caso dos diagramas de caso de uso e de classes, há três estilos visuais diferentes que podemos optar. O “scruffy” da URL acima é o mais bonitinho, mas também tem o “plain” e o “boring”.

Por fim, podemos fazer download do mesmo diagrama nos formatos .png, .jpg e .pdf.

def download_diagrams_to(target_dir, svg_file_name, desired_extensions):
    if not os.path.exists(target_dir):
        os.mkdir(target_dir)

    for extension in desired_extensions:
        file_name = svg_file_name.replace('.svg', extension)

        diagram_url = f'https://yuml.me/{file_name}' # só isso já identifica o arquivo :)
        print('Downloading file', diagram_url)

        r = requests.get(diagram_url)
        file_path = os.path.join(target_dir, f'usecase{extension}')
        with open(file_path, 'wb') as output:
            output.write(r.content)
            output.flush()
        print('Saved file', file_path)

A seguir, o script na sua versão final. Não é um código bonito, nem foi feito para produção, mas é perfeitamente adequado para ser incorporado num pipeline de CI, que atualizaria o diagrama a cada novo deploy. Se novos recursos forem incluídos na aplicação, o diagrama irá contemplá-los, e pelo menos essa parte da documentação se manterá atualizada.

import os
import requests
import sys
import glob

def get_usecase_directories_in(parent_dir):
    second_level_directories = glob.glob(f'{parent_dir}/*/*')
    second_level_directories = list(filter(lambda f: os.path.isdir(f), second_level_directories))

    usecase_directories = []
    for directory in second_level_directories:
        skip = False
        for directory_to_exclude in ['bin', 'obj', 'Common', 'Properties']:
            if (directory_to_exclude in directory):
                skip = True
                continue
        if skip:
            continue
        usecase_directories.append(directory)
    return usecase_directories

def build_usecase_dsl_for_directories_in(usecase_directories, user_name) -> str:
    per_folder_uc = [
        f'[{user_name}]-({os.path.basename(directory)})'
        for directory in usecase_directories
    ]
    return ','.join(per_folder_uc)

def request_usecase_diagram(usecase_dsl) -> str:
    r = requests.post(
        f'https://yuml.me/diagram/scruffy/usecase/',
        { "dsl_text": usecase_dsl }
    )
    svg_file_name = r.text
    return svg_file_name

def download_diagrams_to(target_dir, svg_file_name, desired_extensions):
    if not os.path.exists(target_dir):
        os.mkdir(target_dir)

    for extension in desired_extensions:
        file_name = svg_file_name.replace('.svg', extension)

        diagram_url = f'https://yuml.me/{file_name}'
        print('Downloading file', diagram_url)

        r = requests.get(diagram_url)
        file_path = os.path.join(target_dir, f'usecase{extension}')
        with open(file_path, 'wb') as output:
            output.write(r.content)
            output.flush()
        print('saved file', file_path)

def main():
    application_dir = sys.argv[1]
    print('Application dir is ', application_dir)

    usecase_directories = get_usecase_directories_in(application_dir)

    usecase_dsl = build_usecase_dsl_for_directories_in(usecase_directories, user_name='Usuario')
    print('Using DSL:', usecase_dsl)

    svg_file_name = request_usecase_diagram(usecase_dsl)

    target_dir = sys.argv[2]
    download_diagrams_to(target_dir, svg_file_name, desired_extensions=['.png'])

if __name__ == "__main__":
    main()

Podemos assim chamar o nosso script da seguinte forma:

python rebuild-uc-diagram.py <Application-Path> <Target-Path>

O resultado deste script foi o seguinte:

Diagrama de classes

Diagrama de casos de uso resultante

O código deste exemplo também está disponível em forma de gist no Github.

Outras opções para compartilhar:
comments powered by Disqus