A maioria dos projetos em que estou envolvido usa vários componentes de código aberto. Como princípio geral, é sempre bom evitar vincular todos os componentes do código às bibliotecas de terceiros e, em vez disso, passar por um invólucro de encapsulamento para evitar a dor da mudança?
Como exemplo, a maioria dos nossos projetos PHP usa log4php diretamente como uma estrutura de registro, ou seja, eles instanciam via \ Logger :: getLogger (), usam métodos -> info () ou -> warn () etc. No futuro, no entanto, uma estrutura de registro hipotético pode aparecer, melhor de alguma forma. Tal como está, todos os projetos que se aproximam das assinaturas do método log4php precisariam ser alterados, em dezenas de lugares, para caber nas novas assinaturas. Obviamente, isso teria um grande impacto na base de código e qualquer alteração é um problema em potencial.
Para proteger novas bases de código desse tipo de cenário à prova do futuro, geralmente considero (e algumas vezes implemento) uma classe de wrapper para encapsular a funcionalidade de log e facilitar, embora não seja infalível, alterar a maneira como o log funciona no futuro com alterações mínimas ; o código chama o wrapper, o wrapper passa a chamada para o logging framework du jour .
Tendo em mente que existem exemplos mais complicados com outras bibliotecas, estou com excesso de engenharia ou essa é uma precaução sábia na maioria dos casos?
EDIT: Mais considerações - o uso de injeção e teste de dependência praticamente exige que abstraiamos a maioria das APIs de qualquer maneira ("Quero verificar se meu código executa e atualiza seu estado, mas não escreve um comentário de log / acessa um banco de dados real"). Isso não é uma decisão?
fonte
Respostas:
Se você usar apenas um pequeno subconjunto da API de terceiros, faz sentido escrever um wrapper - isso ajuda no encapsulamento e na ocultação de informações, garantindo que você não exponha uma API possivelmente enorme ao seu próprio código. Também pode ajudar a garantir que qualquer funcionalidade que você não queira usar esteja "oculta".
Outro bom motivo para um wrapper é se você espera alterar a biblioteca de terceiros. Se essa é uma parte da infraestrutura que você sabe que não será alterada, não escreva um wrapper para ela.
fonte
Sem saber quais são os grandes novos recursos que esse alegado futuro logger aprimorado terá, como você escreveria o wrapper? A opção mais lógica é fazer com que seu wrapper instancie algum tipo de classe de logger e tenha métodos como
->info()
ou->warn()
. Em outras palavras, essencialmente idêntico à sua API atual.Em vez de código à prova de futuro que talvez eu nunca precise alterar, ou que possa exigir uma reescrita inevitável de qualquer maneira, prefiro código à prova de "passado". Ou seja, nas raras ocasiões em que altero significativamente um componente, é quando escrevo um wrapper para torná-lo compatível com o código anterior. No entanto, qualquer novo código usa a nova API, e refato o código antigo para usá-lo sempre que fizer uma alteração no mesmo arquivo, de qualquer maneira, ou conforme a programação permitir. Depois de alguns meses, posso remover o invólucro e a mudança foi gradual e robusta.
Em outras palavras, os wrappers realmente só fazem sentido quando você já conhece todas as APIs que precisa quebrar. Bons exemplos são se o seu aplicativo precisar atualmente suportar diversos drivers de banco de dados, sistemas operacionais ou versões PHP.
fonte
Ao agrupar uma biblioteca de terceiros, você adiciona uma camada adicional de abstração sobre ela. Isso tem algumas vantagens:
Sua base de código se torna mais flexível a alterações
Se você precisar substituir a biblioteca por outra, basta alterar sua implementação no wrapper - em um só lugar . Você pode alterar a implementação do wrapper e não precisa mudar nada sobre qualquer outra coisa; em outras palavras, você tem um sistema fracamente acoplado. Caso contrário, você teria que passar por toda a sua base de código e fazer modificações em todos os lugares - o que obviamente não é o que você deseja.
Você pode definir a API do wrapper independentemente da API da biblioteca
Bibliotecas diferentes podem ter APIs muito diferentes e, ao mesmo tempo, nenhuma delas pode ser exatamente o que você precisa. E se alguma biblioteca precisar que um token seja passado junto com todas as chamadas? Você pode transmitir o token em seu aplicativo sempre que precisar usar a biblioteca ou pode protegê-lo em algum lugar mais central, mas, em qualquer caso, você precisa do token. Sua classe de wrapper simplifica tudo isso novamente - porque você pode manter o token dentro da sua classe de wrapper, nunca o expõe a nenhum componente do aplicativo e abstrai completamente a necessidade. Uma enorme vantagem se você já usou uma biblioteca que não enfatiza o bom design da API.
O teste de unidade é muito mais simples
Os testes de unidade devem testar apenas uma coisa. Se você deseja testar uma unidade, precisa zombar de suas dependências. Isso se torna ainda mais importante se essa classe fizer chamadas de rede ou acessar algum outro recurso fora do seu software. Ao agrupar a biblioteca de terceiros, é fácil zombar dessas chamadas e retornar dados de teste ou o que o teste de unidade exigir. Se você não tem essa camada de abstração, fica muito mais difícil fazer isso - e na maioria das vezes isso resulta em muito código feio.
Você cria um sistema fracamente acoplado
Alterações no seu wrapper não afetam outras partes do seu software - pelo menos desde que você não altere o comportamento do seu wrapper. Ao introduzir uma camada de abstração como esse wrapper, você pode simplificar as chamadas para a biblioteca e remover quase completamente a dependência do seu aplicativo nessa biblioteca. Seu software usará apenas o wrapper e não fará diferença a maneira como o wrapper é implementado ou como ele faz o que faz.
Exemplo Prático
Sejamos honestos. As pessoas podem discutir sobre as vantagens e desvantagens de algo assim por horas - e é por isso que prefiro apenas mostrar um exemplo.
Digamos que você tenha algum tipo de aplicativo Android e precise baixar imagens. Existem várias bibliotecas por aí que facilitam o carregamento e o armazenamento de imagens em cache, por exemplo, o Picasso ou o Universal Image Loader .
Agora podemos definir uma interface que vamos usar para agrupar qualquer biblioteca que acabarmos usando:
Essa é a interface que agora podemos usar em todo o aplicativo sempre que precisarmos carregar uma imagem. Podemos criar uma implementação dessa interface e usar injeção de dependência para injetar uma instância dessa implementação em todos os lugares em que usamos o
ImageService
.Digamos que inicialmente decidimos usar o Picasso. Agora podemos escrever uma implementação para a
ImageService
qual usa o Picasso internamente:Bem direto, se você me perguntar. O empacotador em torno das bibliotecas não precisa ser complicado para ser útil. A interface e a implementação têm menos de 25 linhas de código combinadas; portanto, não houve nenhum esforço para criar isso, mas já ganhamos algo fazendo isso. Veja o
Context
campo na implementação? A estrutura de injeção de dependência de sua escolha já cuidará de injetar essa dependência antes de usarmos o nossoImageService
, seu aplicativo agora não precisa se preocupar com a forma como as imagens são baixadas e com as dependências que a biblioteca possa ter. Tudo o que seu aplicativo vê é umImageService
e, quando precisa de uma imagem, chamaload()
com um URL - simples e direto.No entanto, o benefício real ocorre quando começamos a mudar as coisas. Imagine que agora precisamos substituir o Picasso pelo Universal Image Loader porque o Picasso não suporta alguns recursos que precisamos absolutamente no momento. Agora temos que vasculhar nossa base de código e substituir tediosamente todas as chamadas para o Picasso e depois lidar com dezenas de erros de compilação porque esquecemos de algumas chamadas do Picasso? Não. Tudo o que precisamos fazer é criar uma nova implementação
ImageService
e dizer à nossa estrutura de injeção de dependência para usar essa implementação a partir de agora:Como você pode ver, a implementação pode ser muito diferente, mas isso não importa. Não tivemos que alterar uma única linha de código em nenhum outro lugar do nosso aplicativo. Usamos uma biblioteca completamente diferente, que pode ter recursos completamente diferentes ou pode ser usada de maneira muito diferente, mas nosso aplicativo simplesmente não se importa. O mesmo de antes do resto do nosso aplicativo, apenas vê a
ImageService
interface com seuload()
método e, no entanto, esse método é implementado não importa mais.Pelo menos para mim tudo isso já parece muito bom, mas espere! Ainda tem mais. Imagine que você está escrevendo testes de unidade para uma classe em que está trabalhando e essa classe usa o
ImageService
. É claro que você não pode permitir que seus testes de unidade façam chamadas de rede para algum recurso localizado em outro servidor, mas como você está usando agora,ImageService
você pode facilmenteload()
retornar uma estáticaBitmap
usada para os testes de unidade implementando uma simulaçãoImageService
:Para resumir, agrupando bibliotecas de terceiros, sua base de código se torna mais flexível a alterações, em geral mais simples, mais fácil de testar e você reduz o acoplamento de diferentes componentes em seu software - tudo que se torna cada vez mais importante quanto mais tempo você mantém um software.
fonte
Penso que o agrupamento de bibliotecas de terceiros hoje, caso algo melhor aconteça amanhã, é uma violação muito desnecessária do YAGNI. Se você estiver chamando repetidamente o código de terceiros de uma maneira peculiar ao seu aplicativo, refatorará (deverá) essas chamadas em uma classe de agrupamento para eliminar a repetição. Caso contrário, você estará usando totalmente a API da biblioteca e qualquer wrapper se parecerá com a própria biblioteca.
Agora, suponha que uma nova biblioteca apareça com desempenho superior ou o que seja. No primeiro caso, você acabou de reescrever o wrapper para a nova API. Sem problemas.
No segundo caso, você cria um wrapper adaptando a interface antiga para conduzir a nova biblioteca. Um pouco mais de trabalho, mas não há problema, e não há mais trabalho do que você teria feito se tivesse escrito o wrapper anteriormente.
fonte
O motivo básico para escrever um wrapper em torno de uma biblioteca de terceiros é para que você possa trocar essa biblioteca de terceiros sem alterar o código que a utiliza. Você não pode evitar o acoplamento a algo, então o argumento é que é melhor associar uma API que você escreveu.
Se vale a pena o esforço é uma história diferente. Esse debate provavelmente continuará por muito tempo.
Para pequenos projetos, onde a probabilidade de que tal mudança seja necessária é baixa, é provavelmente um esforço desnecessário. Para projetos maiores, essa flexibilidade pode muito mais do que o esforço extra para agrupar a biblioteca. No entanto, é difícil saber se esse é o caso de antemão.
Outra maneira de ver é o princípio básico de abstrair o que provavelmente mudará. Portanto, se a biblioteca de terceiros estiver bem estabelecida e provavelmente não será alterada, pode ser bom não quebrá-la. No entanto, se a biblioteca de terceiros for relativamente nova, há uma chance maior de que ela precise ser substituída. Dito isto, o desenvolvimento de bibliotecas estabelecidas foi abandonado várias vezes. Portanto, essa não é uma pergunta fácil de responder.
fonte
Além do que o @Oded já disse, eu gostaria de adicionar esta resposta com o objetivo especial de registrar.
Eu sempre tenho uma interface para log, mas nunca tive que substituir uma
log4foo
estrutura ainda.Leva apenas meia hora para fornecer a interface e gravar o wrapper, então acho que você não perde muito tempo se isso for desnecessário.
É um caso especial de YAGNI. Embora eu não precise, não leva muito tempo e me sinto mais seguro com isso. Se realmente chegar o dia de trocar o logger, ficarei feliz por ter investido meia hora, pois isso me poupará mais de um dia trocando chamadas em um projeto do mundo real. E nunca escrevi ou vi um teste de unidade para registro (além dos testes para a implementação do criador de logs), portanto, espere defeitos sem o wrapper.
fonte
Estou lidando com esse problema exato em um projeto em que estou trabalhando atualmente. Mas, no meu caso, a biblioteca é para gráficos e, portanto, sou capaz de restringir seu uso a um pequeno número de classes que lidam com gráficos, em vez de espalhá-lo por todo o projeto. Portanto, é muito fácil alternar as APIs mais tarde, se necessário; no caso de um madeireiro, o assunto se torna muito mais complicado.
Assim, eu diria que a decisão tem muito a ver com o que exatamente a biblioteca de terceiros está fazendo e com quanto sofrimento estaria associado à sua alteração. Se alterar todas as chamadas de API seria fácil, independentemente, provavelmente não vale a pena fazer. Se, no entanto, mudar a biblioteca mais tarde for realmente difícil, provavelmente eu a envolverei agora.
Além disso, outras respostas cobriram muito bem a questão principal, então só quero focar nessa última adição, sobre injeção de dependência e objetos simulados. Depende, é claro, de como exatamente sua estrutura de criação de log funciona, mas na maioria dos casos não é necessário um wrapper (embora provavelmente se beneficie de um). Basta tornar a API do seu objeto simulado exatamente a mesma da biblioteca de terceiros e, em seguida, você pode facilmente trocar o objeto simulado para teste.
O principal fator aqui é se a biblioteca de terceiros é implementada ou não por meio de injeção de dependência (ou um localizador de serviço ou algum desses padrões fracamente acoplados). Se as funções da biblioteca forem acessadas por meio de métodos singleton ou estáticos ou algo assim, você precisará agrupá-lo em um objeto com o qual possa trabalhar na injeção de dependência.
fonte
Estou fortemente no campo de encerramento e não consigo substituir a biblioteca de terceiros com a maior prioridade (embora isso seja um bônus). Minha lógica principal que favorece a embalagem é simples
E isso se manifesta, normalmente, na forma de um monte de duplicação de código, como desenvolvedores escrevendo 8 linhas de código apenas para criar
QButton
e estilizá-lo da maneira que deve procurar o aplicativo, apenas para o designer querer não apenas a aparência mas também a funcionalidade dos botões para mudar completamente para todo o software, o que acaba exigindo voltar e reescrever milhares de linhas de código, ou descobrir que a modernização de um pipeline de renderização requer uma reescrita épica, porque a base de código borrifou ad-hoc fixo de baixo nível. canalize o código OpenGL em todo o lugar, em vez de centralizar o design do renderizador em tempo real e deixar o uso do OGL estritamente para sua implementação.Esses projetos não são adaptados às nossas necessidades específicas. Eles tendem a oferecer um superconjunto enorme do que é realmente necessário (e o que não faz parte de um design é tão importante, se não mais, do que é), e suas interfaces não são projetadas para atender especificamente às nossas necessidades em um "nível superior". thought = one request "de uma maneira que nos priva de todo controle central do design se os usarmos diretamente. Se os desenvolvedores acabam escrevendo um código de nível muito inferior ao necessário para expressar o que precisam, às vezes podem envolvê-lo de maneira ad-hoc que faz com que você acabe com dezenas de documentos escritos às pressas e de forma grosseira. invólucros projetados e documentados em vez de um bem projetado e bem documentado.
É claro que eu aplicaria fortes exceções a bibliotecas nas quais os wrappers são quase traduções individuais do que as APIs de terceiros têm a oferecer. Nesse caso, pode não ser necessário procurar um design de nível superior que expresse mais diretamente os requisitos de negócios e de design (pode ser o caso de algo que se assemelha mais a uma biblioteca de "utilitários"). Mas se houver um design muito mais personalizado disponível, que expresse mais diretamente nossas necessidades, estou fortemente no campo de empacotamento, assim como sou fortemente a favor de usar uma função de nível superior e reutilizá-la sobre o código de montagem embutido por todo o lugar.
Estranhamente, colidi com os desenvolvedores de maneiras que pareciam tão desconfiadas e pessimistas quanto à nossa capacidade de projetar, digamos, uma função para criar um botão e devolvê-lo, que eles preferem escrever 8 linhas de código de nível inferior focadas em microscópicos. detalhes da criação do botão (que acabou precisando mudar repetidamente no futuro) sobre o design e o uso dessa função. Eu nem vejo o propósito de tentarmos projetar algo em primeiro lugar, se não podemos confiar em nós mesmos para projetar esses tipos de embalagens de maneira razoável.
Em outras palavras, vejo as bibliotecas de terceiros como formas de economizar potencialmente um enorme tempo na implementação, e não como substitutos do design de sistemas.
fonte
Minha ideia sobre bibliotecas de terceiros:
Recentemente, houve uma discussão na comunidade iOS sobre prós e contras (OK, principalmente contras) do uso de dependências de terceiros. Muitos argumentos que vi foram bastante genéricos - agrupando todas as bibliotecas de terceiros em uma única cesta. Como na maioria das coisas, porém, não é assim tão simples. Então, vamos tentar focar em um único caso
Devemos evitar o uso de bibliotecas de interface do usuário de terceiros?
Razões para considerar bibliotecas de terceiros:
Parece haver duas razões principais que os desenvolvedores consideram usar uma biblioteca de terceiros:
A maioria das bibliotecas de interface do usuário ( nem todas! ) Tendem a se enquadrar na segunda categoria. Esse material não é ciência de foguetes, mas leva tempo para construí-lo corretamente.
Existem praticamente dois tipos de controles / visualizações:
UICollectionView
deUIKit
.UIPickerView
. A maioria das bibliotecas de terceiros tende a pertencer à segunda categoria. Além disso, eles geralmente são extraídos de uma base de código existente para a qual foram otimizados.Suposições iniciais desconhecidas
Muitos desenvolvedores fazem revisões de código de seu código interno, mas podem estar considerando a qualidade do código fonte de terceiros. Vale a pena gastar um pouco de tempo apenas navegando no código de uma biblioteca. Você pode se surpreender ao ver algumas bandeiras vermelhas, por exemplo, swizzling usado onde não é necessário.
Você não pode esconder
Devido à maneira como o UIKit foi projetado, você provavelmente não poderá ocultar a biblioteca de UI de terceiros, por exemplo, atrás de um adaptador. Uma biblioteca entrelaçará com o código da interface do usuário, tornando-se um fato de seu projeto.
Custo futuro
O UIKit muda a cada versão do iOS. As coisas vão quebrar. Sua dependência de terceiros não será tão livre de manutenção quanto o esperado.
Conclusão:
Da minha experiência pessoal, a maioria dos usos de código de interface do usuário de terceiros se resume a trocar flexibilidade menor por algum ganho de tempo.
Usamos código pronto para enviar nosso release atual mais rapidamente. Mais cedo ou mais tarde, porém, chegamos aos limites da biblioteca e ficamos diante de uma decisão difícil: o que fazer a seguir?
fonte
Usar a biblioteca diretamente é mais amigável para a equipe de desenvolvedores. Quando um novo desenvolvedor ingressa, ele pode ter uma experiência completa com todas as estruturas usadas, mas não poderá contribuir produtivamente antes de aprender sua API desenvolvida em casa. Quando um desenvolvedor mais jovem tenta progredir em seu grupo, ele é forçado a aprender que sua API específica não está presente em nenhum outro lugar, em vez de adquirir competência genérica mais útil. Se alguém conhece recursos ou possibilidades úteis da API original, talvez não seja possível acessar a camada escrita por alguém que não a conhecia. Se alguém conseguir uma tarefa de programação enquanto procura um emprego, talvez não consiga demonstrar as coisas básicas que ele usou muitas vezes, apenas porque todas essas vezes ele estava acessando a funcionalidade necessária através do seu wrapper.
Eu acho que essas questões podem ser mais importantes do que a possibilidade remota de usar uma biblioteca completamente diferente posteriormente. O único caso em que eu usaria um wrapper é quando a migração para outra implementação é definitivamente planejada ou a API empacotada não está congelada o suficiente e continua mudando.
fonte