Como você evita interações intermináveis ​​através de projetos igualmente subótimos?

10

Portanto, provavelmente como muitos, muitas vezes me vejo enfrentando problemas de design nos quais, por exemplo, há algum padrão / abordagem de design que parece se encaixar intuitivamente no problema e tem os benefícios desejados. Muitas vezes, existem algumas ressalvas que dificultam a implementação do padrão / abordagem sem algum tipo de trabalho em torno do qual nega os benefícios do padrão / abordagem. Posso facilmente iterar através de muitos padrões / abordagens, porque previsivelmente quase todos eles têm algumas advertências muito significativas em situações do mundo real nas quais simplesmente não há uma solução fácil.


Exemplo:

Vou lhe dar um exemplo hipotético baseado vagamente em um exemplo real que encontrei recentemente. Digamos que eu queira usar a composição sobre a herança, porque as hierarquias de herança impediram a escalabilidade do código no passado. Eu posso refatorar o código, mas depois descobrir que há alguns contextos em que a superclasse / classe básica simplesmente precisa chamar a funcionalidade na subclasse, apesar das tentativas de evitá-lo.

A próxima melhor abordagem parece estar implementando um padrão de meio delegado / observador e meio de composição para que a superclasse possa delegar comportamento ou para que a subclasse possa observar eventos da superclasse. Em seguida, a classe é menos escalável e sustentável, porque não está claro como deve ser estendido; também é complicado estender os ouvintes / delegados existentes. Além disso, as informações não ficam ocultas porque é necessário conhecer a implementação para ver como estender a superclasse (a menos que você use comentários muito extensivamente).

Então, depois disso, posso optar por simplesmente usar completamente os observadores ou delegados para evitar as desvantagens que surgem com a mistura pesada das abordagens. No entanto, isso vem com seus próprios problemas. Por exemplo, eu acho que acabo precisando de observadores ou delegados para uma quantidade crescente de comportamentos até acabar precisando de observadores / delegados para praticamente todos os comportamentos. Uma opção pode ser ter apenas um grande ouvinte / delegado para todo o comportamento, mas a classe de implementação acaba com muitos métodos vazios, etc.

Então eu poderia tentar outra abordagem, mas há tantos problemas com isso. Em seguida, o próximo e o outro, etc.


Esse processo iterativo fica muito difícil quando cada abordagem parece ter tantos problemas quanto qualquer outro e leva a uma espécie de paralisia da decisão de design . Também é difícil aceitar que o código acabe igualmente problemático, independentemente de qual padrão ou abordagem de design seja usada. Se eu acabar nessa situação, isso significa que o problema em si precisa ser repensado? O que os outros fazem quando se deparam com essa situação?

Edit: Parece haver uma série de interpretações da pergunta que eu quero esclarecer:

  • Tirei OOP da questão completamente porque, na verdade, ele não é específico para OOP, além de ser muito fácil interpretar erroneamente alguns dos comentários que fiz ao passar sobre OOP.
  • Alguns afirmaram que eu deveria adotar uma abordagem iterativa e tentar padrões diferentes, ou que deveria descartar um padrão quando ele parar de funcionar. Este é o processo que pretendi referir em primeiro lugar. Eu pensei que isso estava claro no exemplo, mas eu poderia ter deixado mais claro, então editei a pergunta para fazer isso.
Jonathan
fonte
3
Não "assine a filosofia OO". É uma ferramenta, não uma decisão importante da vida. Use-o quando ajudar e não use-o quando não ajudar.
user253751
Se você está pensando em certos padrões, talvez tente escrever um projeto moderadamente complexo em C, será interessante experimentar o quanto você pode fazer sem esses padrões.
user253751
2
Oh C tem padrões. Eles são apenas padrões muito diferentes. :)
candied_orange 28/02
Eu ter esclarecido a maioria desses erros de interpretação na edição
Jonathan
Tendo estes problemas aparecem de vez em quando é normal. Tê-los ocorrer com freqüência não deve ser o caso. Provavelmente, você está usando o paradigma de programação errado para o problema ou seus projetos são uma mistura de paradigmas nos lugares errados.
Dunk

Respostas:

8

Quando chego a uma decisão difícil como essa, geralmente me faço três perguntas:

  1. Quais são os prós e os contras de todas as soluções disponíveis?

  2. Existe uma solução que ainda não considerei?

  3. Mais importante:
    quais são exatamente os meus requisitos? Não são os requisitos em papel, os reais fundamentais?
    Posso de alguma forma reformular o problema / ajustar seus requisitos, para que ele permita uma solução simples e direta?
    Posso representar meus dados de uma maneira diferente, para permitir uma solução tão simples e direta?

    Essas são as perguntas sobre os fundamentos do problema percebido. Pode acontecer que você estivesse realmente tentando resolver o problema errado. Seu problema pode ter requisitos específicos que permitem uma solução muito mais simples do que o caso geral. Questione sua formulação do seu problema!

Acho muito importante pensar nas três perguntas antes de continuar. Faça uma caminhada, passeie pelo escritório, faça o que for preciso para realmente refletir sobre as respostas a essas perguntas. No entanto, responder a essas perguntas não deve demorar muito tempo. Os horários adequados podem variar de 15 minutos a algo como uma semana, eles dependem da maldade das soluções que você já encontrou e de seu impacto sobre o todo.

O valor dessa abordagem é que você às vezes encontrará soluções surpreendentemente boas. Soluções elegantes. As soluções valem o tempo que você investiu em responder a essas três perguntas. E você não encontrará essa solução, se você digitar a próxima iteração imediatamente.

Obviamente, às vezes parece não existir uma boa solução. Nesse caso, você está preso às suas respostas à primeira pergunta, e o bom é simplesmente o menos ruim. Nesse caso, o valor de reservar um tempo para responder a essas perguntas é que você possivelmente evita iterações que provavelmente falharão. Apenas certifique-se de voltar à codificação dentro de um período de tempo adequado.

cmaster - restabelece monica
fonte
Posso muito bem marcar isso como a resposta, isso faz mais sentido para mim e parece haver um consenso sobre simplesmente reavaliar o problema em si.
Jonathan
Também gosto de como você a dividiu em etapas, para que haja algum tipo de procedimento solto para avaliar a situação.
Jonathan
OP, vejo você preso à mecânica da solução de código, então sim "você também pode marcar esta resposta". O melhor código que escrevemos foi depois de uma análise cuidadosa dos requisitos, requisitos derivados, diagramas de classe, interações de classe etc. Graças a Deus, resistimos a todos os problemas dos assentos baratos (gerenciamento de leitura e colegas de trabalho): "Você estamos gastando muito tempo em design "," apresse-se e comece a codificar! "," são muitas aulas! ", etc. Quando chegamos a isso, a codificação foi uma alegria - mais do que divertida. Objetivamente, esse foi o melhor código em todo o projeto.
Radarbob #
13

Primeiras coisas primeiro - os padrões são abstrações úteis, e não o fim, tudo é todo design, muito menos design OO.

Em segundo lugar - o OO moderno é inteligente o suficiente para saber que nem tudo é um objeto. Às vezes, o uso de funções simples e antigas, ou mesmo alguns scripts de estilo imperativo, produz uma solução melhor para determinados problemas.

Agora, para a carne das coisas:

Isso fica muito difícil quando cada abordagem parece ter tantos problemas quanto os outros.

Por quê? Quando você tem várias opções semelhantes, sua decisão deve ser mais fácil! Você não perderá muito escolhendo a opção "errada". E realmente, o código não é fixo. Tente algo, veja se é bom. Iterar .

Também é difícil aceitar que o código acabe sendo muito problemático, independentemente de qual padrão ou abordagem de design seja usada.

Nozes duras. Problemas difíceis - problemas reais matematicamente difíceis são difíceis. Provavelmente difícil. Não há literalmente uma boa solução para eles. E acontece que os problemas fáceis não são realmente valiosos.

Mas tenha cuidado. Muitas vezes, tenho visto pessoas frustradas por não ter boas opções porque ficam presas ao problema de uma certa maneira ou cortam suas responsabilidades de uma maneira que não é natural para o problema em questão. "Nenhuma boa opção" pode ser um cheiro de que há algo fundamentalmente errado com sua abordagem.

Isso resulta em uma perda de motivação porque o código "limpo" deixa de ser uma opção às vezes.

Perfeito é o inimigo do bem. Faça algo funcionar e depois refatorá-lo.

Existem abordagens de design que podem ajudar a minimizar esse problema? Existe uma prática aceita para o que se deve fazer em uma situação como esta?

Como mencionei, o desenvolvimento iterativo geralmente minimiza esse problema. Depois de conseguir algo funcionando, você está mais familiarizado com o problema, colocando-o em uma posição melhor para resolvê-lo. Você tem um código real para examinar e avaliar, não um design abstrato que não parece certo.

Telastyn
fonte
Apenas pensei em dizer que esclareci algumas interpretações erradas na edição. Geralmente faço a abordagem "fazê-lo funcionar" e refatoro. No entanto, muitas vezes isso acaba gerando muita dívida técnica em minha experiência. Por exemplo, se eu apenas colocar uma coisa em funcionamento, mas depois eu ou outros construímos sobre ela, algumas dessas coisas podem ter dependências inevitáveis ​​que também precisam de muita refatoração. Claro que você nem sempre pode saber disso com antecedência. Mas se você já sabe que a abordagem é falha, deve-se fazê-la funcionar e depois refatorar iterativamente antes de criar o código?
Jonathan
@ Jonathan - todos os projetos são uma série de compensações. Sim, você deve iterar imediatamente se o design for defeituoso e você tiver uma maneira clara de melhorá-lo. Se não houver nenhuma melhoria clara, pare de brincar.
Telastyn
"eles cortaram suas responsabilidades de uma maneira que não é natural para o problema em questão. Nenhuma boa opção pode ser o cheiro de que há algo fundamentalmente errado com sua abordagem". Eu apostaria neste. Alguns desenvolvedores nunca se deparam com os problemas descritos no OP e outros parecem fazê-lo com bastante frequência. Freqüentemente, a solução não é fácil de ser vista quando apresentada pelo desenvolvedor original, porque eles já influenciaram o design da maneira como enquadram o problema. Às vezes, a única maneira de encontrar a solução bastante óbvia é começar de volta aos requisitos do design.
Dunk
8

A situação que você está descrevendo parece uma abordagem de baixo para cima. Você pega uma folha, tenta consertá-la e descobre que está conectada a um galho que também está conectado a outro galho etc.

É como tentar construir um carro começando com um pneu.

O que você precisa é dar um passo atrás e olhar para a foto maior. Como essa folha se encaixa no design geral? Ainda está atual e correto?

Como seria o módulo se você o projetasse e implementasse do zero? A que distância desse "ideal" está sua implementação atual.

Dessa forma, você tem uma imagem maior do que trabalhar. (Ou, se você decidir que é muito trabalhoso, quais são os problemas).

Pieter B
fonte
4

Seu exemplo descreve uma situação que geralmente ocorre com partes maiores do código legado e quando você está tentando fazer uma refatoração "muito grande".

O melhor conselho que posso dar para essa situação é:

  • não tente alcançar seu objetivo principal em um "big bang" ,

  • aprenda a melhorar seu código em etapas menores!

Claro, isso é mais fácil de escrever do que fazer, então como fazer isso na realidade? Bem, você precisa de prática e experiência, isso depende muito do caso, e não existe uma regra rígida que diga "faça isso ou aquilo" que seja adequado para todos os casos. Mas deixe-me usar seu exemplo hipotético. "composição sobre herança" não é um objetivo de design de tudo ou nada, é um candidato perfeito para ser alcançado em várias pequenas etapas.

Digamos que você tenha notado que "composição por herança" é a ferramenta certa para o caso. Deve haver algumas indicações para que este seja um objetivo sensato, caso contrário você não o teria escolhido. Então, vamos supor que há muita funcionalidade na superclasse que é apenas "chamada" das subclasses, portanto essa funcionalidade é candidata a não permanecer nessa superclasse.

Se você notar que não pode remover imediatamente a superclasse das subclasses, pode começar primeiro refatorando a superclasse em componentes menores, encapsulando a funcionalidade mencionada acima. Comece com os frutos mais baixos, extraia primeiro alguns componentes mais simples, o que já tornará sua superclasse menos complexa. Quanto menor a superclasse, mais fáceis serão as refatorações adicionais. Use esses componentes das subclasses e da superclasse.

Se você tiver sorte, o código restante na superclasse se tornará tão simples ao longo deste processo que você poderá remover a superclasse das subclasses sem mais problemas. Ou, você notará que manter a superclasse não é mais um problema, já que você já extraiu o suficiente do código em componentes que deseja reutilizar sem herança.

Se você não sabe por onde começar, porque não sabe se uma refatoração será simples, às vezes a melhor abordagem é fazer uma refatoração de arranhões .

Obviamente, sua situação real pode ser mais complicada. Portanto, aprenda, ganhe experiência e seja paciente, pois isso leva anos. Existem dois livros que posso recomendar aqui, talvez você os ache úteis:

Doc Brown
fonte
1
Isso também é muito útil. Uma pequena refatoração passo a passo ou de arranhões é uma boa idéia, pois se torna algo que pode ser concluído progressivamente junto com outros trabalhos sem dificultá-lo demais. Eu gostaria de poder aprovar várias respostas!
28418 Jonathan
1

Às vezes, os dois melhores princípios de design são o KISS * e o YAGNI **. Não sinta a necessidade de agrupar todos os padrões de design conhecidos em um programa que apenas imprima "Olá, mundo!".

 * Keep It Simple, Stupid
 ** You Ain't Gonna Need It

Edite após a atualização da pergunta (e espelhando o que Pieter B diz até certo ponto):

Às vezes, você toma uma decisão arquitetural desde o início, o que leva a um design específico que leva a todo tipo de feiura quando você tenta implementá-lo. Infelizmente, nesse ponto, a solução "adequada" é dar um passo atrás e descobrir como você chegou a essa posição. Se você ainda não consegue ver a resposta, continue recuando até conseguir.

Mas se o trabalho para fazer isso for desproporcional, é preciso uma decisão pragmática para apresentar a solução menos feia para o problema.

Simon B
fonte
Eu resolvi um pouco disso na edição, mas em geral estou me referindo exatamente àqueles problemas em que não há uma solução simples disponível.
28418 Jonathan
Ah, sim, parece haver um consenso emergente sobre recuar e reavaliar o problema. Essa é uma boa ideia.
28418 Jonathan
1

Quando estou nessa situação, a primeira coisa que faço é parar. Eu mudo para outro problema e trabalho nisso por um tempo. Talvez uma hora, talvez um dia, talvez mais. Isso nem sempre é uma opção, mas meu subconsciente trabalha com as coisas enquanto meu cérebro consciente faz algo mais produtivo. Eventualmente, volto a ele com novos olhos e tento novamente.

Outra coisa que faço é perguntar a alguém mais inteligente que eu. Isso pode assumir a forma de perguntar no Stack Exchange, ler um artigo na Web sobre o tópico ou perguntar a um colega que tem mais experiência nessa área. Muitas vezes, o método que eu acho que é a abordagem correta acaba sendo completamente errado para o que estou tentando fazer. Eu descaracterizei algum aspecto do problema e ele realmente não se encaixa no padrão que eu acho. Quando isso acontece, alguém mais diz: "Sabe, isso parece mais ..." pode ser uma grande ajuda.

Relacionado ao acima, há design ou depuração por confissão. Você vai a um colega e diz: "Vou lhe contar o problema que estou tendo e depois vou explicar as soluções que tenho. Você aponta problemas em cada abordagem e sugere outras abordagens. . " Muitas vezes, antes que a outra pessoa fale, como estou explicando, começo a perceber que um caminho que parecia igual aos outros é realmente muito melhor ou pior do que eu pensava inicialmente. A conversa resultante pode reforçar essa percepção ou apontar coisas novas em que não pensei.

Então TL; DR: Faça uma pausa, não force, peça ajuda.

user1118321
fonte