Com a quantidade de recursos gratuitos, livros, aulas online e bootcamps de programação disponíveis atualmente, qualquer pessoa pode aprender a programar. No entanto, continua a existir uma diferença de qualidade entre a programação e a engenharia de software. Tem de haver uma?
Escrevi o meu primeiro "Hello world" há mais de vinte anos - é a resposta que dou quando alguém me pergunta há quanto tempo sou programador. Nos últimos dez anos, tenho desfrutado de uma carreira que me permite tocar código quase todos os dias - é a resposta que dou quando me perguntam há quanto tempo sou programador profissional.
Há quanto tempo sou um engenheiro de software? Eu diria que cerca de cinco anos. Espera aí, estes números não parecem coincidir! Então o que é que mudou? Quem é que eu consideraria um engenheiro de software e quem é "meramente" um programador?
A definição de um engenheiro de software
A codificação é relativamente fácil. Já não se trata apenas de mnemónicas assembly em sistemas ridiculamente limitados. E se estivermos a usar algo tão expressivo e poderoso como Rubié ainda mais fácil.
Basta pegar num bilhete, encontrar onde precisa de inserir o seu código, descobrir a lógica que precisa de colocar lá e pronto - está feito. Se fores um pouco mais avançado, certifica-te de que o teu código é bonito. Está logicamente dividido em métodos. Tem especificações decentes que não testam apenas o caminho feliz. É isso que um bom programador faz.
Um engenheiro de software já não pensa em métodos e classes, pelo menos não principalmente. Na minha experiência, um engenheiro de software pensa em fluxos. Antes de mais, vêem o rio furioso de dados e interações a correr pelo sistema. Eles pensam sobre o que precisam fazer em termos de desviar ou alterar esse fluxo. O código bonito, os métodos lógicos e as óptimas especificações surgem quase como uma reflexão posterior.
São tartarugas até ao fim
Em geral, as pessoas pensam de uma certa forma sobre a maioria das interações com a realidade. Por falta de um termo melhor, chamemos-lhe a perspetiva "descendente". Se o meu cérebro estiver a trabalhar na preparação de uma chávena de chá, começará por descobrir os passos gerais: ir à cozinha, pôr a chaleira ao lume, preparar a chávena, deitar água, voltar à secretária.
Não vai primeiro decidir qual a chávena a usar, enquanto eu estiver na minha secretária; essa decisão virá mais tarde, quando eu estiver em frente ao armário. Não considera que o chá pode ter acabado (ou, pelo menos, que o bom coisas). É amplo, reativo e propenso a erros. Em suma - muito humano na natureza.
À medida que o engenheiro de software considera as alterações ao fluxo de dados algo alucinante, fá-lo-á naturalmente de forma semelhante. Consideremos este exemplo de história de utilizador:
Um cliente encomenda um widget. Ao determinar o preço da encomenda, deve ser tido em conta o seguinte:
- Preço base do widget na localidade do utilizador
- Forma do widget (modificador de preço)
- Se se trata de uma encomenda urgente (modificador de preço)
- Se a entrega da encomenda tem lugar num feriado na localidade do utilizador (modificador de preço)
Tudo isto pode parecer inventado (e obviamente que é), mas não está muito longe de algumas histórias de utilizadores reais que tive o prazer de esmagar recentemente.
Agora, vamos analisar o processo de pensamento que um engenheiro de software pode empregar para resolver este problema:
"Bem, temos de obter o utilizador e a sua encomenda. Depois, começamos a calcular o total. Começamos com zero. Depois aplicamos o modificador de forma do widget. Depois, a taxa de urgência. Depois vemos se é feriado e pronto, está feito antes do almoço!"
Ah, a adrenalina que uma simples história de utilizador pode trazer. Mas o engenheiro de software é apenas um ser humano, não uma máquina multi-threaded perfeita, e a receita acima é um golpe geral. O engenheiro continua a pensar mais profundamente:
"O modificador de forma do widget é... oh, isso depende muito do widget, não é? E podem ser diferentes por localidade, mesmo que não seja agora, mas no futuro," pensam eles, queimados anteriormente pela mudança dos requisitos comerciais, "e a taxa de urgência também pode ser. E os feriados também são muito específicos de cada local, e os fusos horários também estão envolvidos! Tinha aqui um artigo sobre como lidar com horários em diferentes fusos horários em Carris aqui... ooh, pergunto-me se a hora da encomenda está guardada com a zona na base de dados! É melhor verificar o esquema".
Muito bem, engenheiro de software. Pára. Era suposto estar a fazer uma chávena de chá, mas está em frente ao armário a pensar se a chávena florida se aplica ao seu problema de chá.
Widget para preparar a chávena perfeita
Mas isso é facilmente o que pode acontecer quando se tenta fazer algo tão pouco natural para o cérebro humano como pensar com várias profundidades de pormenor simultaneamente.
Depois de uma breve pesquisa no seu espaçoso arsenal de links sobre o manuseamento de fusos horários, o nosso engenheiro recompõe-se e começa a decompor isto em código real. Se eles tentassem a abordagem ingénua, poderia parecer algo como isto:
def calculate_price(user, order)
ordem.preço = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
fim
E continuavam, desta forma deliciosamente processual, apenas para serem fortemente rejeitados na primeira revisão de código. Porque, se pensarmos bem, é perfeitamente normal pensar desta forma: primeiro os traços gerais, e os pormenores muito mais tarde. Nem sequer pensaste que estavas fora do chá bom no início, pois não?
O nosso engenheiro, no entanto, é bem treinado e não é estranho ao Objeto de Serviço, por isso eis o que começa a acontecer:
classe BaseOrderService
def self.call(user, order)
new(utilizador, encomenda).call
fim
def initialize(user, order)
@utilizador = utilizador
@ordem = ordem
fim
def chamada
puts "[WARN] Implementar chamada não predefinida para #{self.class.name}!"
utilizador, ordem
fim
fim
class WidgetPriceService < BaseOrderService; end
class ShapePriceModifier < BaseOrderService; end
class RushPriceModifier < BaseOrderService; end
class HolidayDeliveryPriceModifier < BaseOrderService; end
class OrderPriceCalculator < BaseOrderService
def call
utilizador, encomenda = WidgetPriceService.call(utilizador, encomenda)
utilizador, encomenda = ShapePriceModifier.call(utilizador, encomenda)
utilizador, encomenda = RushPriceModifier.call(utilizador, encomenda)
utilizador, encomenda = HolidayDeliveryPriceModifier.call(utilizador, encomenda)
utilizador, encomenda
fim
fim
```
Muito bem! Agora podemos usar um bom TDD, escrever um caso de teste para ele e desenvolver as classes até que todas as peças se encaixem. E vai ser lindo, também.
Para além de ser completamente impossível de raciocinar.
O inimigo é o Estado
Claro, são todos objectos bem separados com responsabilidades únicas. Mas aqui está o problema: eles ainda são objetos. O padrão de objeto de serviço com seu "forçosamente fingir que este objeto é uma função" é realmente uma muleta. Não há nada que impeça alguém de chamar HolidayDeliveryPriceModifier.new(user, order).something_else_entirely. Não há nada que impeça as pessoas de acrescentarem estado interno a estes objectos.
Para além disso utilizador e ordem também são objectos, e mexer neles é tão fácil como alguém fazer uma order.save em algum lugar nesses objetos funcionais "puros", alterando a fonte subjacente do estado da verdade, também conhecida como banco de dados. Neste exemplo artificial, não é um grande problema, mas pode voltar a ser um problema se este sistema crescer em complexidade e se expandir para partes adicionais, muitas vezes assíncronas.
O engenheiro tinha a ideia correta. E utilizou uma forma muito natural de exprimir essa ideia. Mas saber como exprimir esta ideia - de uma forma bonita e fácil de raciocinar - foi praticamente impedido pelo facto de a OOP paradigma. E se alguém que ainda não deu o salto para exprimir os seus pensamentos como desvios do fluxo de dados tentar alterar o código subjacente de forma menos hábil, acontecerão coisas más.
Tornar-se funcionalmente puro
Se ao menos houvesse um paradigma em que expressar as suas ideias em termos de fluxos de dados fosse não só fácil, mas também necessário. Se o raciocínio pudesse ser simples, sem possibilidade de introduzir efeitos secundários indesejados. Se os dados pudessem ser imutáveis, tal como a chávena florida onde se faz o chá.
Sim, estou a brincar, claro. Esse paradigma existe, e chama-se programação funcional.
Vamos considerar como o exemplo acima pode parecer em um favorito pessoal, Elixir.
defmodule WidgetPrices do
def priceorder([utilizador, encomenda]) do
[utilizador, encomenda]
|> widgetprice
|> modificador shapepricemodifier
|> modificador rushpricemodifier
|> modificador holidaypricemodifier
fim
defp widgetprice([utilizador, encomenda]) do
%{widget: widget} = encomenda
preço = WidgetRepo.getbase_price(widget)
[utilizador, %{ordem | preço: preço }]
fim
defp shapepricemodifier([utilizador, encomenda]) do
%{widget: widget, price: currentprice} = encomenda
modificador = WidgetRepo.gethapeprice(widget)
[utilizador, %{ordem | preço: preço atual * modificador} ]
fim
defp rushpricemodifier([utilizador, encomenda]) do
%{pressa: pressa, preço: preço atual} = encomenda
if rush do
[utilizador, %{ordem | preço: preço atual * 1,75} ]
senão
[utilizador, %{encomenda | preço: preço_actual} ]
end
fim
defp holidaypricemodifier([utilizador, encomenda]) do
%{date: date, price: currentprice} = encomenda
modificador = HolidayRepo.getholidaymodifier(user, date)
[utilizador, %{ordem | preço: preço atual * modificador}]
end
fim
```
Poderá notar que é um exemplo completo de como a história do utilizador pode realmente ser alcançada. Isso é porque é menos complicado do que seria em Ruby. Estamos a usar algumas funcionalidades chave exclusivas do Elixir (mas geralmente disponíveis em linguagens funcionais):
Funções puras. Não alteramos efetivamente a entrada ordem Estamos apenas a criar novas cópias - novas iterações do estado inicial. Também não saltamos para o lado para alterar nada. E mesmo que quiséssemos, ordem é apenas um mapa "burro", não podemos chamar order.save em qualquer altura, porque simplesmente não sabe o que isso é.
Correspondência de padrões. De forma semelhante à desestruturação do ES6, isto permite que nós depenar preço e widget da ordem e passá-la adiante, em vez de obrigar os nossos amigos WidgetRepo e FériasRepo para saber como lidar com um ordem.
Operador de tubagem. Visto em preço_ordempermite-nos passar dados através de funções numa espécie de "pipeline" - um conceito imediatamente familiar para qualquer pessoa que tenha executado ps aux | grep postgres para verificar se a maldita coisa ainda estava a funcionar.
É assim que se pensa
Os efeitos secundários não são, de facto, uma parte básica do nosso processo de pensamento. Depois de deitar água na chávena, geralmente não nos preocupamos com a possibilidade de um erro na chaleira fazer com que esta sobreaqueça e expluda - pelo menos não o suficiente para irmos espreitar os seus componentes internos para verificar se alguém não deixou inadvertidamente explodir_depois_de verter virado para cima.
O caminho de programador a engenheiro de software - passando da preocupação com objectos e estados para a preocupação com fluxos de dados - pode, em alguns casos, demorar anos. Foi o que aconteceu com o meu amigo que aprendeu OOP. Com linguagens funcionais, começa-se a pensar em fluxos na vossa primeira noite.
Fizemos engenharia de software complicado para nós próprios e para todos os recém-chegados a esta área. A programação não tem de ser difícil e desgastante para o cérebro. Pode ser fácil e natural.
Não vamos complicar as coisas e vamos tornar-nos já funcionais. Porque é assim que pensamos.
Leia também: