Como se cria uma Arquitetura de Software?

Há muito tempo estou tendo dificuldade para criar Arquiteturas de Softwares, quanto mais eu trabalho numa Arquitetura mais difícil fica continuar trabalhando nela, pois ela cresce e se torna mais complexa, fica mais difícil entendê-la além do que fica mais difícil adicionar novas funcionalidades nela.

Dividir Arquitetura em Partes e definir como elas interagem entre si:

Minha ideia é simples e me parece correta: dividir a Arquitetura de todo o sistema em Partes (Módulos), que por sua vez serão divididas em Sub-Partes (Sub-Módulos) e assim por diante; cada Parte tem uma responsabilidade, e, eu defino como essas Partes interagem entre si (defino os Contratos entre elas) para que elas - trabalhando juntas - entreguem todas as funcionalidades do Software.

Então eu poderia começar dividindo a Arquitetura nas partes A, B, e C, e cada parte seria responsável por alguma coisa. Também posso começar a estabelecer os Contratos entre essas partes, como dizer que A envia um ObjetoX para B e que B deverá fazer determinada coisa ao receber esse ObjetoX, tendo que ler esse ObjetoX para saber como exatamente fará esta coisa; e que B para fazer seu trabalho, terá que solicitar que C faça determinada coisa, que varia um pouco dependendo do conteúdo do ObjetoX recebido por B.

Esses Contratos entre as Partes podem ser tão bem específicados ao ponto de definirmos os parâmetros de entrada e de retorno, definindo as assinaturas dos métodos para saber exatamente qual método cada Parte chamará na outra Parte, o que deverá passar pelos parâmetros e o que receberá de retorno.

Como eu gostaria que fosse:

Até aí parece tudo muito bom, se eu estivesse trabalhando numa equipe poderia até pensar que dá pra deixar cada desenvolvedor responsável por implementar uma Parte, e, contanto que cada desenvolvedor faça uma implementação cumprindo os Contratos já estabelecidos, no final poderemos unir todas as Partes (que se “encaixarão” perfeitamente umas nas outras por estarem seguindo os devidos Contratos) e teremos um Software pronto funcionando. Assim, esses desenvolvedores poderiam trabalhar ao mesmo tempo e nem precisariam se comunicar, não precisariam saber como os outros estão implementando suas Partes, porque os Contratos garantirão a compatibilidade final das Partes.

Como de fato é:

Mas não é isso que acontece! Na verdade passa bem longe disso:

O que acontece é que, quando se começa a implementar uma Parte, ou mesmo dividí-la em Sub-Partes, logo percebe-se que um ou mais dos Contratos já estabelecidos não são adequados! Talvez a Parte precise receber mais Dados, ou talvez esteja recebendo Dados que ela não precisará usar, ou talvez ela precise Notificar outras Partes de alguma coisa que pode acontecer enquanto ela é executada (talvez ela tenha que lançar exceptions não-previstas para outras Partes, ou, talvez ela precise que outra Parte seja um Listener de eventos que ocorrem dentro dela), talvez descubra-se que a Parte não conseguirá satisfazer todas as solicitações que o Contrato dela diz que ela satisfará, ou ainda, talvez queira-se implementar uma nova funcionalidade nesta Parte e o Contrato dela não oferece meios de utilizar esta nova funcionalidade.

De qualquer modo, é necessário renegociar os Contratos. E o maior problema é que isso ocorre durante todo o projeto da Arquitetura, conforme você vai definindo ela, criando as Sub-Partes, ou mesmo quando já está codificando, você acaba descobrindo que um ou mais dos Contratos já feitos precisa ser alterado.

O Retrabalho (quase?) sem fim:

Mas o pior problema é Propagação de Alterações: Alterar um Contrato significa que as Partes envolvidas nesse Contrato precisarão ser alteradas, afim de passarem a atender a nova versão do Contrato. Entretanto, ao alterar uma Parte, você pode descobrir que precisará alterar o mesmo Contrato novamente e/ou alterar outros Contratos, que levarão a alteração de outras Partes, que levarão a alteração de outros Contratos, e assim por diante, de modo que uma Alteração na Arquitetura provoca Alterações em cascata, tipo “efeito dominó”, e o resultado de tudo isso é um imenso retrabalho e o descarte de muito trabalho que já tinha sido feito.

Então, no fim, eu tenho um “processo de desenvolvimento” onde tenho que ficar refazendo um monte de coisa que já estabeleci na Arquitetura porque acabei descobrindo que é inadequado, inviável, ou mesmo porque me dei conta de que há uma forma significativamente melhor de fazer aquilo; isso leva a propagações de alterações constantes, de modo que a Arquitetura é instável (está sempre mudando) até ela ficar pronta.

Como a Arquitetura está sempre mudando, não dá pra mim trabalhar nela “Parte por Parte”, porque trabalhar numa Parte causará mudanças que se propagarão pelas outras, forçando-me a ter que ficar indo de Parte em Parte fazendo alterações nelas para que se adequem às mudanças. Como eu não posso trabalhar na Arquitetura “Parte por Parte”, é como se estivesse trabalhando nela “Toda de uma Vez”, o que torna o trabalho muito mais difícil, pois:

  • Se a Arquitetura é instável, qualquer documentação ou diagramas também serão, aumentando muito o retrabalho (tem que ficar refazendo documentação e diagramas);
  • Ter que editar a Arquitetura “Toda de uma Vez” me força a ter que ter na mente o funcionamento de “Toda a Arquitetura” ao mesmo tempo, não posso simplesmente me concentrar em cada Parte e esquecer do resto.
  • Se eu estivesse trabalhando em Equipe, os desenvolvedores teriam que estar sempre renegociando os Contratos e refazendo suas Partes para atender as novas versões dos Contratos.

O que vocês me dizem?

  • Então, estou fazendo algo errado?
  • Vocês também fazem assim e passam por esses mesmos problemas?
  • Existe uma forma melhor de se fazer a Arquitetura do Software?
  • Qual é o “Passo-a-Passo” de vcs para fazer a “Arquitetura do Software”?

Qualquer ajuda, comentário, ou experiência é muito bem vinda :slight_smile:

1 curtida

Achievement unlocked: software design is hard! :grin:

Você acaba de descobrir que desenvolver software é uma tarefa complexa, nunca 100% exata e que os requisitos mudam com o passar do tempo, levando à reengenharia. Software real costuma ser assim, resta se acostumar e tentar fazer o melhor (como parece que você já está fazendo). Com o tempo e experiência, você ficará melhor em prever certas necessidades e fazer arquiteturas melhores.

Algumas sugestões:

  • dê uma estudada em metodologias ágeis, acredito que podem te ser úteis;
  • aceite que software e requisitos mudam e abrace essa mudança (esse é um dos princípios das metodologias ágeis, inclusive);
  • não se apegue tanto à diagramas. Faça e mantenha atualizados os que são úteis somente;
  • mesma coisa quanto à documentação. Detalhar 200% do sistema é complicado, foque no mais complexo e mais necessário;
  • tenha em mente que os problemas que você citou não desaparecem quando o trabalho é dividido numa equipe. Pelo contrário, podem até piorar (pois as partes conflitantes agora estão nas mãos de pessoas distintas);

Abraço.

2 curtidas

Não tenho Know how suficiente, mais vou colaborar com pequenas informações, pois estudei apenas o módulo I de engenharia de software e não sou desenvolvedor.
A fase mais crítica é com certeza o levantamento de requisitos e em seguida a modelagem.
Vide: https://www.youtube.com/watch?v=-TcFtvkrC50&list=PLjzmQiSlNcO6B8JuwTvZKFfweBCyU3d4l aos 29 min e 4 segundos
A porcentagem de erros de codificação é relativamente baixa.
Então as sugestões de melhoria envolvem principalmente gestão e é muita coisa, muita mesmo.
Então, fazer poucas sugestões.
1 - tentas implantar o PSP; em seguida
2 - tentar implantar o TSP;
3 - tentar implantar o MPS-BR ou o CMMI, a depender da realidade da empresa.
Quanto ao tamanho do projeto, talvez não ajude, mas tem algo bem interessante, fazer uso qualificado da rastreabilidade e do baixo acoplamento.
Como dito, eu não tenho Know How, mais tenho algo em mente, quando começar atuar nesta área:
Sun Tzu disse: Comandar muitos é o mesmo que comandar poucos. Tudo
é uma questão de organização. Controlar muitos ou poucos é uma mesma e única coisa. É apenas uma questão de formação e sinalizações.

Considerando que o levantamento de requisitos foi bem realizado, o segundo ambiente crítico passa a ser a modelagem, logo:
1 - Se a modelagem apresenta erros, a culpa é de quem a fez e não do programador; e
2 - Se a modelagem foi bem feita, a culpa é do programador e não de quem modelou.
3 - Caso a modelagem necessite de alterações por fatos inusitados, cabe ao modelador se adaptar à nova realidade, pois esta é fluida.

Eu gosto bastante da arte da guerra e suas adaptações a diversas áreas de atuação, por isto recomendo: http://unes.br/Biblioteca/Arquivos/A_Arte_da_Guerra_L&PM.pdf, depois, bastante engenharia de software.

Não misturar funcionalidades já é um ótimo passo. Pior coisa é precisar alterar uma ação do usuário e se deparar com um bando de coisas que não tem nada haver com aquela funcionalidade, o que acaba impactando muita coisa. Não sei se entendi bem sobre os contratos, mas evite processos burocráticos.

Os “Contratos” tem a ver com “Programação por Contrato”, que basicamente se trata de definir o que cada Parte do Software oferece e o que precisa, em geral trata-se das “Interfaces” das Classes/Módulos. Por exemplo:

public double somar(double num1, double num2);

Essa assinatura de método acima é um Contrato, esse contrato estabelece que existe um método chamado “somar” que retorna um “double” e que precisa receber dois “double” por parâmetro.

Após criar esse método você está estabelecendo um Contrato que inúmeros códigos pelo sistema podem precisar usar (tornando-se dependentes dele), chamando esse método. É um Contrato porque o método está “prometendo” que se ele receber dois doubles ele irá retornar um double que seja a soma deles, e você pode criar seus códigos chamando esse método confiando nesse contrato, confiando que ele fará o que promete se você passar os dois doubles pelo parâmetro.

Entretanto, o problema surge quando você precisa alterar o contrato, por exemplo, você percebeu que - devido as limitações do double - você precisará utilizar BigDecimal no lugar dele, aí você vai e muda o contrato:

public BigDecimal somar(BigDecimal num1, BigDecimal num2);

Pronto! todos os códigos que você havia criado e que chamavam esse método irão parar de funcionar, porque eles enviam para o método dois doubles e esperam receber um double como o contrato anterior prometia, mas agora o contrato mudou e eles não são capazes de se adaptar automaticamente. Isto significa que você precisará ir em cada um desses códigos e alterá-los, fazendo-os trabalhar com BigDecimal ao invés de double. Só aí já é um trabalhão, mas pode piorar: ao alterar esses códigos, pode ser necessário alterar o contrato (a interface) deles também, por exemplo, se eles recebiam double podem precisar passar a receber BigDecimal também, e, a partir disso outros códigos que funcionavam podem parar de funcionar, precisando serem alterados também. Então ocorre a propagação de alterações pelo sistema, o “efeito dominó”.

Nós podemos tentar evitar esses problemas criando códigos mais flexíveis, que se adaptam as mudanças feitas nos contratos, ou ainda, contratos flexíveis que permitem que o código mude sem que seja necessário que o contrato mude (isto evita a propagação de alterações), e a idéia de “Programar para Interfaces” está diretamente ligada a isso; o que você citou de não misturar as coisas também, pois aumentamos granularidade. Abstrair corretamente deveria ser o melhor jeito de evitar as alterações constantes no que já foi feito, porque as “Abstrações Corretas” permitiriam adicionar as funcionalidades estendo Classes/Interfaces sem ter que editar código já feito.

Mas com todos esses conceitos e vários outros que existem para tentar fazer o Software aceitar bem as mudanças, com o menor retrabalho possível, ainda assim estou tendo muita dificuldade e fazendo muito retrabalho; mas quero que foquem nisso: Estou ainda falando do trabalho em cima da Arquitetura, ou seja, da Modelagem do Software, nem estou falando de mudar o Software depois que o código já está implementado.

Pra esclarecer melhor: antes de começar a codificar podemos fazer um Diagrama de Classes, ele consegue detalhar bem a Arquiterua/Modelagem, só que, mesmo ao tentar fazer um Diagrama de Classes eu me vejo fazendo-o e refazendo-o constantemente para tentar encaixar todas as Classes e como elas funcionam juntas. Quando consigo fazer um Diagrama apto para entregar algumas funcionalidades, ao tentar adicionar mais algumas ao Diagrama vejo que será necessário fazer grandes mudanças nele (ou seja, muito retrabalho), de modo que nem o Diagrama é flexível às mudanças!

Depois de fazer e refazer o Diagrama/Arquitetura/Modelagem várias e várias vezes, até pode ser que ele fique mais flexível as mudanças, ou pelo menos a certas mundanças.

@javaflex e @TerraSkilll Quando vocês estão criando uma nova Arquitetura para um Software (antes de implementar qualquer código, apenas definindo Módulos, Classes, e talvez até as assinaturas dos Métodos, e, definindo as dependências que haverá entre eles) vocês também passam por esse problema de ter que ficar fazendo e refazendo drasticamente o Diagrama/Arquitetura/Modelagem conforme ele cresce? Ou, durante o processo de fazer o Diagrama/Arquitetura/Modelagem vocês já conseguem fazê-lo de um modo que ele sofra apenas poucas alterações no que já foi feito conforme ele vai crescendo?

@Douglas-Silva, no meu caso não uso diagramas de classes, pelo menos pra mim não servem pra nada.

public BigDecimal somar(BigDecimal num1, BigDecimal num2);

Ao invés de passar um bando de parâmetros soltos, deveria passar o objeto da entidade que pertence num1, num2. Ou se for da própria classe nem precisaria passar nada. Assim seu refactoring seria mais centralizado.

Não entendi bem isso de “passar o objeto da entidade que pertence num1, num2”, vc está falando de passar um único objeto por parâmetro que, dentro de si, conteria os números a serem somados?

Imagino que com isso você quis dizer que num1 e num2 poderiam sem variáveis de instância, de modo que o método não precisaria de parâmetros.


Mas o que estou tendo problemas mesmo é questão de criar a Modelagem do Software, como já disse. Como você faz? Você vai direto na IDE criando classes e métodos e digitando código? Sempre que tentei isso acabei tendo que descartar e alterar muito código já feito conforme ia adicionando as funcionalidades já previstas e conforme o sistema crescia :confused:

Antes de começar a codificar, eu imagino que seja necessário, pelo menos, algum esboço da arquitetura como quais classes serão criadas, o que estende o que, e como os Objetos são compostos por outros Objetos, e principalmente como os Objetos interagem entre si.

O que você faz antes de começar a codificar? Como você modela o software (pode-se entender projeta) antes de começar a implementá-lo? Ou vc não faz isso e vai direto pra codificação mesmo e consegue fazer dar tudo certo?

Sim, foi o que eu quis dizer, trabalhar diretamente com o objeto.

Não modelo software com um todo, atendo ciclos.

Como trabalho com banco de dados relacional, até existe a modelagem de dados, mas é mantida por ADs.

Mais importante do que se preocupar com diagramas técnicos, é ter o entendimento do Negócio, definir com o cliente as necessidades prioritárias, para ter entregas enxutas e consistentes. Assim o universo do que está indo para produção é menor para homologar e ajustar naquele ciclo.

O processo do cliente é o modelo natural. Então você pode implementar uma determinada funcionalidade baseado diretamente no que é definido na análise dos requisitos com o cliente, e pela sua própria vivência no Negócio dentro do processo dele, tendo conhecimento em tempo real das mudanças para amenizar retrabalhos. O feedback do cliente durante o desenvolvimento é essencial para evitar retrabalhos. Não esquecendo também da prototipação antes de começar a codificar.

2 curtidas

Vc esta certo em tudo oq falou, tb ja passei por isso, sabe como consegui gerenciar isso? Com processo Ágil + TDD! Sem timebox de iteração e sem TDD, realmente fica ingerenciavél! Requisitos mudam sim, mas eles devem ser colocados, classificados e ordenados no backlog do projeto e ir entrando a medida de valor do seu retorno. Nem tudo que muda, quer dizer que vai ser feito ou que vai entrar na solução.

Mesmo oque entra e muda, vc tem que ter um bom DESIGN de classes para suportar as mudanças. O que faz mudar rapida e propagar as mudanças em cascatas rapida, testando tudo isso em minutos e horas…é TDD. Com TDD, vc tem feedback em minutos do tamanho do estrago que vc fez.

Pensa comigo, vc não tem processo, tudo mundo chega e muda oque quer…vc não tem bom design, oq muda quebra tudo, e vc não tem TDD, não tem como testar isso rápido e ver se oq mudou expirou…dai realmente não da!

Para os interessados no assunto tenho um curso de introdução de arquitetura de software, que ensina o básico dos básicos de arquitetura - https://for-j.myedools.com/aqt-m1-introducao-a-arquitetura-de-software-com-java que seria um start na área.

1 curtida

Bem, pelo que entendi dos conselhos do @TerraSkilll, @javaflex e @FernandoFranzini a melhor forma de lidar com esses problemas são as metodologias ágeis, envolvendo a questão de ter um Backlog de requisitos ordenado por prioridade, e ir adicionando as funcionalidades a cada ciclo (Sprint).

Só uma dúvida restante: durante um Sprint, quando se está adicionando uma ou mais funcionalidades, vocês se preocupam:

  • A) apenas em fazer o software receber as funcionalidades do Sprint atual e continuar funcionado, sem se preocupar em já ir deixando-o mais flexível e preparado para receber as funcionalidades que serão implementadas em Sprints distantes no futuro;
    OU
  • B) fazer o software receber as funcionalidades do Sprint atual e continuar funcionado, mas também já ir deixando-o mais flexível e preparado para receber as funcionalidades que serão implementadas em Sprints distantes no futuro.

Acredito que a opção A) torna o trabalho da Sprint atual mais fácil já que não é necessário se preocupar com as funcionalidades que serão adicionadas nas Sprints futuras. Entretanto, imagino que pode dificultar as Sprints futuras já que o Software vem sendo confeccionado sem ser preparado para elas.

Pelo que entendi, o @javaflex adota essa abordagem A), penso isso devido a esse trecho onde ele fala de “limitar o universo” e dar atenção ao ciclo atual:

Já a abordagem B) torna o trabalho da Sprint atual mais difícil já que é necessário se preocupar com as funcionalidades que serão adicionadas nas Sprints futuras. Entretanto, imagino que pode facilitar as Sprints futuras já que o Software vem sendo confeccionado de modo a estar preparado para elas.

Então, vocês usam a abordagem A) ou B)? Ou isso varia e/ou não fica nítido no processo de vcs?

1 curtida

Opção A.
Com TDD e bom design vc consegue manutenir tudo no seu sistema.
Não faça futorologia no seu desig - https://fernandofranzini.wordpress.com/2016/01/27/desenvolvimento-de-software-futurologia/

Futuras de quanto tempo? Dependendo desse futuro as ideais estarão evoluidas e o que você “adiantou” teria que ser desfeito. Claro que não é pra levar ao pé da letra, use o bom senso para deixar as aberturas importantes. Se o sistema possui hoje 1 usuário e no futuro poderá ter mais, não quer dizer que você vá deixar de criar uma tabela pra isso.

1 curtida