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 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 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:
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:
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.
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:
- Identificar os diretórios de segundo nível
- Monstar a DSL descrevendo cada um deles como Caso de Uso
- Requisitar a geração do diagrama
- 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:
O código deste exemplo também está disponível em forma de gist no Github.