CodeGym /Cursos /Kotlin SELF /Como fixar a arquitetura na estrutura do projeto

Como fixar a arquitetura na estrutura do projeto

Kotlin SELF
Nível 39, Lição 1
Disponível

1. Introducao

Quando o projeto e pequeno, parece que pastas e pacotes sao “coisa de enterprise grande e assustador”, e que a gente so precisa terminar o comando add e ir tomar um cha. Mas o problema e que o crescimento do projeto acontece de mansinho: hoje sao 3 arquivos, amanha 12, e em uma semana voce esta procurando “onde mesmo a gente faz o parse do comando” e descobre 4 funcoes parseCommand que diferem por exatamente um espaco.

Pacotes em Kotlin sao uma forma de nomear os “bairros” do seu projeto: aqui mora a CLI, ali a logica de dominio, acolá o armazenamento. E quando voce segue a regra de “quem pode importar quem”, a estrutura comeca a funcionar como um corrimao na escada: nao atrapalha a andar, mas ajuda muito a nao despencar.

Tecnicamente, o Kotlin nao exige que as pastas no disco coincidam com o nome do pacote: os arquivos podem ficar “em qualquer lugar”. Mas o cerebro humano nao e um compilador, e ele gosta muito quando domain/model/Expense.kt realmente contem o package domain.model. Por isso, na pratica quase sempre vale a pena manter a estrutura de diretorios alinhada com os pacotes: a navegacao fica mais simples, os imports ficam mais claros e ha menos erros acidentais ao mover arquivos.

Dá para pensar assim: para o compilador nao importa onde o arquivo esta, mas para a pessoa importa. Nos escrevemos codigo para pessoas (e um pouquinho para o compilador, para ele nao chorar).

2. Relembrando package e import

Em Kotlin, a declaracao do pacote e escrita no comeco do arquivo, e os imports vem logo depois. Nao e so uma tradicao “porque e assim que se faz”: isso torna o arquivo legivel de cima para baixo — primeiro o endereco (pacote), depois as dependencias (imports), depois o codigo.

Ainda assim, tecnicamente o Kotlin nao exige que as pastas no disco coincidam com o nome do pacote: os arquivos podem ficar “em qualquer lugar”. Mas para o time e para a IDE e muito mais tranquilo quando a estrutura de diretorios replica a estrutura de pacotes, especialmente em projetos JVM ao lado de Java.

Mini-exemplo: pacote como “endereco” do arquivo


package domain.model

data class Expense(val title: String, val amount: Int)

Aqui esta tudo correto: o arquivo diz “eu sou do bairro domain.model”. Se quisermos usar Expense em outro pacote, vamos ter que importar.

3. Estrutura de pacotes: fixando responsabilidades

Para que os pacotes funcionem como uma dica arquitetural, os nomes precisam refletir responsabilidade. Vamos combinar uma estrutura simples e legivel, que ajuda a manter as fronteiras:

Camada (responsabilidade) Pacote O que vive aqui O que nao deve existir aqui
CLI
app.cli
leitura de comandos, parsing da linha, impressao, roteiro do programa regras de negocio e armazenamento de dados “de qualquer jeito”
Domain (modelo)
domain.model
data class
do dominio
readln()
,
println()
, comandos, formatacao de UI
Domain (regras/operacoes)
domain.service
validacao, normalizacao, operacoes sobre modelos trabalho direto com o console
Storage
storage
armazenamento e entrega de dados (em memoria) parsing de comandos e saida no console

Essa organizacao e propositalmente bem simples. O importante e que, com um unico olhar para o pacote, fique claro: “isso e parte do roteiro” ou “isso e parte da logica de dominio”.

4. Direcao das dependencias: quem importa quem

E aqui comeca a arquitetura de verdade, mesmo que o projeto ainda seja pequeno. Direcao das dependencias e a regra “quem depende de quem”. No codigo isso aparece literalmente: em quais pacotes escrevemos import para quais pacotes.

Queremos que a camada externa (CLI) dependa da interna (domain), e nao o contrario. Assim, o domain pode ser reutilizado em qualquer lugar: em testes, em outra interface, em outro jeito de receber comandos. Mesmo que voce ainda nao faca nada disso — so o fato de ser possivel deixa o codigo mais tranquilo.

De forma esquematica (mapa de imports permitidos), fica assim:

flowchart TD
    CLI[app.cli] --> DS[domain.service]
    CLI --> DM[domain.model]
    CLI --> ST[storage]
    DS --> DM
    ST --> DM

    %% restricao importante:
    %% domain NAO depende de app.cli

Se simplificar para uma unica regra: domain.* nao importa app.cli.*. Nunca. Nem “so uma funcaozinha printHelp()”. Porque isso nao e “uma funcaozinha”, e um buraco na fronteira.

Por que isso importa em um projeto real

Se domain comecar a importar a CLI, vai surgir uma “dependencia circular de significado”: voce ja nao consegue entender onde terminam as regras de dominio e onde comeca a interface. Em algum momento, qualquer ajuste vai exigir ajuste em todos os lugares ao mesmo tempo — e o projeto vai ficar fragil.

5. Exemplo pratico: organizando o codigo por pacotes

Agora vamos pegar nosso projeto de estudo (um rastreador de despesas hipotetico) e organiza-lo por pacotes de forma que a estrutura reflita a arquitetura. Importante: nao vamos fazer um “projeto dos sonhos com 200 arquivos”. Vamos fazer um projeto pequeno, mas com a geografia correta.

Vamos imaginar a seguinte estrutura em src/main/kotlin:

src/main/kotlin
├── app
│   └── cli
│       ├── Main.kt
│       └── ParsedCommand.kt
├── domain
│   ├── model
│   │   └── Expense.kt
│   └── service
│       └── ExpenseService.kt
└── storage
    └── InMemoryExpenseStorage.kt

Repare: o Kotlin nao nos obriga a fazer assim, mas as convencoes de organizacao de codigo recomendam fortemente — especialmente para que a estrutura de diretorios siga os pacotes.

domain.model: modelo de dominio

Vamos comecar pelo mais “limpo”: o modelo.

package domain.model

data class Expense(val title: String, val amount: Int)

Aqui nao tem console, nem comandos, nem “como exibir bonitinho”. Sao apenas dados.

domain.service: regras e operacoes

Agora o service: ele cria um Expense correto a partir de parametros brutos. Repare: o service nao armazena lista e nao imprime. Ele aplica regras.

package domain.service

import domain.model.Expense

class ExpenseService {
    fun createExpense(title: String, amount: Int): Expense {
        val normalizedTitle = title.trim()
        require(normalizedTitle.isNotEmpty()) { "Title must not be empty" }
        require(amount > 0) { "Amount must be > 0" }
        return Expense(title = normalizedTitle, amount = amount)
    }
}

Importamos o modelo de domain.model; essa e uma dependencia normal “service → model”.

storage: armazenamento em memoria

O armazenamento em memoria e o nosso “storage layer minimo”: ele consegue salvar uma despesa e retornar todas as despesas. Ele nao sabe nada sobre comandos e console; ele so conhece o modelo.

package storage

import domain.model.Expense

class InMemoryExpenseStorage {
    private val items = mutableListOf<Expense>()

    fun add(expense: Expense) {
        items.add(expense)
    }

    fun all(): List<Expense> = items
}

Aqui vai um ponto importante: para fora, devolvemos List<Expense>, e nao MutableList<Expense>. Isso ajuda a nao transformar o storage em um “corredor sem portas”, onde qualquer um pode alterar os dados como quiser.

app.cli: comandos e parsing da linha

A camada de CLI e o lugar onde lidamos com strings, espacos e erros humanos do tipo “eu digitei add cafe duzentos”. Classes de dominio nao devem cuidar disso.

Vamos criar um tipo pequeno ParsedCommand, para que o main nao precise ficar lidando com “indices magicos”.

package app.cli

data class ParsedCommand(val name: String, val args: List<String>)

E a funcao de parsing (por enquanto bem simples, sem pretensao de ser perfeita):

package app.cli

fun parseCommandLine(line: String): ParsedCommand {
    val parts = line.trim().split(" ").filter { it.isNotEmpty() }
    val name = parts.firstOrNull()?.lowercase() ?: ""
    val args = if (parts.size > 1) parts.drop(1) else emptyList()
    return ParsedCommand(name = name, args = args)
}

Aqui tudo e bem “de CLI”: trim, split, lowercase.

app.cli.Main: montagem do app e roteiro

E agora o main. Ele importa tudo o que precisa e cola o roteiro: leu a linha → fez o parse → aplicou regras → salvou → exibiu o resultado.

package app.cli

import domain.service.ExpenseService
import storage.InMemoryExpenseStorage

fun main() {
    val service = ExpenseService()
    val storage = InMemoryExpenseStorage()

    print("Command: ")                  // Command:
    val cmd = parseCommandLine(readln())

    when (cmd.name) {
        "add" -> {
            val title = cmd.args.getOrNull(0) ?: ""
            val amount = cmd.args.getOrNull(1)?.toIntOrNull() ?: 0

            val expense = service.createExpense(title, amount)
            storage.add(expense)

            println("Added: $expense")  // Added: Expense(title=..., amount=...)
        }
        "list" -> println(storage.all())
        else -> println("Unknown command: ${cmd.name}")
    }
}

Do ponto de vista de dependencias, tudo fica bonito: a CLI depende de domain e storage. Domain nao depende de CLI. Storage tambem nao depende de CLI. Ou seja, se amanha voce quiser outra interface (por exemplo, nao baseada em console), voce nao vai ter um “trem de println”.

6. Imports como indicador de problemas e a armadilha do utils

Existe um truque util: as vezes basta abrir um arquivo e olhar para o import dele para entender se a arquitetura “escorreu” ou nao. Se voce ve que um arquivo de domain.service importa algo de app.cli, quase sempre isso e um sinal: “enfiamos um detalhe de UI dentro da logica de dominio”.

Um detalhe adicional: as regras de resolucao de nomes em Kotlin consideram prioridades de import. Por exemplo, um import explicito geralmente tem prioridade maior do que um import com estrela (*) ou do que “encontrou no mesmo pacote”. Isso nao significa que precisamos viver com medo de import. Significa que imports sao parte do design do projeto, e nao “lixo la em cima”.

Muito rapido aparecem no projeto “funcoes convenientes”: formatMoney, readInt, normalizeTitle, parseSomething. E a mao coça para criar um pacote utils e jogar tudo la dentro. Parece organizacao, mas muitas vezes vira uma “caixa de fios”: voce joga la tudo o que ainda nao sabe onde colocar.

O problema nao e o nome utils, e sim a falta de responsabilidade. Quando uma utilidade nao esta vinculada a uma camada, ela comeca a puxar dependencias para todos os lados. Por exemplo, utils.formatExpense() de repente comeca a importar domain.model.Expense; depois alguem coloca um println “para depurar”, e a utilidade deixa de ser utilidade e vira um pequeno portal do caos.

E bem mais tranquilo quando funcoes auxiliares moram perto da sua camada. Parsing de comandos fica em app.cli, regras de normalizacao em domain.service, manipulacao da lista do storage em storage. Pacote nao e lixeira; e endereco.

7. Erros tipicos

Erro nº 1: o codigo de dominio comeca a depender da CLI por causa de um “import inocente”.
Normalmente isso comeca com a frase “eu so quero imprimir bonitinho uma mensagem de erro”. E aparece um import app.cli.printError. Depois disso, o domain ja nao pode ser usado separado do console, e a fronteira de responsabilidade some. Melhor que o domain retorne um erro claro (ou lance require), e que a CLI decida como mostrar isso ao usuario.

Erro nº 2: desalinhamento entre pastas e package, porque “Kotlin permite”.
Sim, permite: o arquivo pode ficar em qualquer lugar. Mas as recomendacoes de organizacao de fontes dizem explicitamente que a estrutura de diretorios deve seguir a estrutura de pacotes, especialmente na JVM. Caso contrario, a navegacao na IDE fica estranha, mover arquivos vira um sofrimento, e o projeto parece um labirinto sem mapa.

Erro nº 3: o storage expõe MutableList para fora, e qualquer codigo pode mutar os dados.
Por fora parece conveniente: “mas eu sou cuidadoso”. So que em uma semana voce vai esquecer onde exatamente voce foi “cuidadoso” e vai ganhar mudancas de estado inesperadas. Retorne List, e mantenha a mutacao dentro da camada de armazenamento — assim fica mais facil controlar quem altera os dados e onde.

Erro nº 4: imports com estrela (import something.*) comecam a viver vida propria.
O import com * as vezes parece conveniente, mas em projetos grandes ele piora a legibilidade e pode causar conflitos de nomes inesperados. Alem disso, em Kotlin a resolucao de nomes tem prioridades: imports explicitos “ganham” do que veio via *, e esse e mais um motivo para nao transformar import em loteria.

Erro nº 5: o pacote utils vira um “lixao de tudo”.
Enquanto o projeto e pequeno, isso parece ordem. Mas assim que entram funcoes de camadas diferentes, o pacote comeca a puxar dependencias para todos os lados e derruba as fronteiras de responsabilidade. Melhor manter as “utilidades” perto da camada e da tarefa delas — assim fica mais facil controlar imports, e a estrutura vira autoexplicativa.

1
Tarefa
Kotlin SELF, nível 39, lição 1
Bloqueado
Registro de usuário
Registro de usuário
1
Tarefa
Kotlin SELF, nível 39, lição 1
Bloqueado
Lista de compras
Lista de compras
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION