O livro "Objetos elegantes. Edição Java »

imagem Oi, habrozhiteli! Este livro revisa seriamente a essência e os princípios da programação orientada a objetos (OOP) e pode ser metaforicamente chamado de "Lobachevsky OOP". Egor Bugaenko , um desenvolvedor com 20 anos de experiência, analisa criticamente os dogmas da OOP e se oferece para examinar esse paradigma de uma maneira completamente nova. Então, ele estigmatiza métodos estáticos, getters, setters, métodos mutáveis, acreditando que isso é mau. Para um programador iniciante, esse volume pode se tornar iluminação ou choque e, para um programador experiente, é uma leitura obrigatória.

Trecho "Não use métodos estáticos"


Ah, métodos estáticos ... Um dos meus tópicos favoritos. Levei vários anos para perceber o quão importante é esse problema. Agora me arrependo de todo o tempo que gastei escrevendo software processual em vez de orientado a objetos. Eu era cego, mas agora eu vi. Os métodos estáticos são tão grandes, se não um problema maior no POO, quanto ter uma constante NULL. Os métodos estáticos, em princípio, não deveriam estar em Java e em outras linguagens orientadas a objetos, mas, infelizmente, estão lá. Não devemos saber sobre coisas como a palavra-chave estática em Java, mas, infelizmente, somos forçados. Não sei quem os trouxe para Java, mas eles são pura maldade. Métodos estáticos, não os autores desse recurso. Espero que sim.

Vamos ver o que são métodos estáticos e por que ainda os criamos. Digamos que eu precise da funcionalidade de carregar uma página da web por meio de solicitações HTTP. Eu crio uma "classe":

class WebPage { public static String read(String uri) { //  HTTP- //     UTF8- } } 

É muito conveniente usá-lo:

 String html = WebPage.read("http://www.java.com"); 

O método read () pertence à classe de métodos que eu me oponho. Sugiro usar um objeto (também alterei o nome do método de acordo com as recomendações da seção 2.4):

 class WebPage { private final String uri; public String content() { //  HTTP- //     UTF8- } } 

Veja como usá-lo:

 String html = new WebPage("http://www.java.com") .content(); 

Você pode dizer que não há muita diferença entre eles. Os métodos estáticos funcionam ainda mais rápido, porque não precisamos criar um novo objeto toda vez que precisamos baixar uma página da web. Basta chamar o método estático, ele fará o trabalho, você obterá o resultado e continuará a trabalhar. Não há necessidade de mexer nos objetos e no coletor de lixo. Além disso, podemos agrupar vários métodos estáticos em uma classe de utilitário e nomear, por exemplo, WebUtils.

Esses métodos ajudarão a carregar páginas da web, obter informações estatísticas, determinar o tempo de resposta, etc. Haverá muitos métodos neles, e usá-los é simples e intuitivo. Além disso, como aplicar métodos estáticos também é intuitivo. Todo mundo entende como eles funcionam. Basta escrever WebPage.read () e - você adivinhou! - a página será lida. Demos instruções ao computador e ele é executado. Simples e claro, certo? E não!

Métodos estáticos em qualquer contexto são um indicador inconfundível de um programador ruim que não tem idéia sobre OOP. Não há justificativa para aplicar métodos estáticos em qualquer situação. Cuidar do desempenho não conta. Os métodos estáticos são uma zombaria de um paradigma orientado a objetos. Eles existem em Java, Ruby, C ++, PHP e outras linguagens. Infelizmente Não podemos jogá-los fora, não podemos reescrever todas as bibliotecas de código aberto cheias de métodos estáticos, mas podemos parar de usá-las em nosso código.

Devemos parar de usar métodos estáticos.

Agora, vamos examiná-los de várias posições diferentes e discutir suas deficiências práticas. Posso generalizá-los com antecedência: métodos estáticos degradam a manutenção do software. Isso não deveria surpreendê-lo. Tudo se resume à manutenção.

Objetivo versus pensamento computacional


Inicialmente, chamei essa subseção de Objetivo versus Pensamento Processual, mas depois a renomeei. “Pensamento procedural” significa quase a mesma coisa, mas a frase “pense como um computador” descreve melhor o problema. Herdamos essa maneira de pensar das linguagens de programação iniciais, como Assembly, C, COBOL, Basic, Pascal e muitas outras. A base do paradigma é que o computador funciona para nós, e dizemos a ele o que fazer, fornecendo instruções explícitas, por exemplo:

  CMP AX, BX JNAE greater MOV CX, BX RET greater: MOV CX, AX RET 

Essa é uma "rotina" de montagem do processador Intel 8086. Ele encontra e retorna o maior dos dois números. Nós os colocamos nos registros AX e BX, respectivamente, e o resultado cai no registro CX. Aqui está exatamente o mesmo código C:

 int max(int a, int b) { if (a > b) { return a; } return b; } 

"O que há de tão errado nisso?" - você pergunta. Nada .. Está tudo bem com este código - ele funciona como deveria.É assim que todos os computadores funcionam. Eles esperam que nós lhes demos instruções de que eles seguirão um após o outro. Por muitos anos, escrevemos programas dessa maneira. A vantagem dessa abordagem é que permanecemos próximos ao processador, direcionando seu movimento adicional. Estamos no comando, e o computador segue nossas instruções. Dizemos ao computador como encontrar o maior dos dois números. Tomamos decisões, ele as segue. O fluxo de execução é sempre consistente, desde o início do script até o fim.

Esse tipo de pensamento linear é chamado "pense como um computador". Em algum momento, o computador começa a executar instruções e, em algum momento, termina de fazê-lo. Ao escrever código em C, somos forçados a pensar dessa maneira. Operadores separados por ponto e vírgula vão de cima para baixo. Esse estilo é herdado do assembler.
Embora as linguagens de nível superior ao assembler possuam procedimentos, sub-rotinas e outros mecanismos de abstração, elas não eliminam uma maneira consistente de pensar.O programa ainda é executado de cima para baixo. Não há nada errado com essa abordagem ao escrever pequenos programas, mas em uma escala maior é difícil pensar assim.

Dê uma olhada no mesmo código escrito na linguagem de programação funcional Lisp:

 (defun max (ab) (if (> ab) ab)) 

Você pode dizer onde a execução desse código começa e termina? Não. Não sabemos como o processador obterá o resultado ou como a função if funcionará. Estamos muito distantes do processador. Pensamos como uma função, não como um computador. Quando precisamos de uma coisa nova, nós a definimos:

 (def x (max 5 9)) 

Definimos, não fornecemos instruções ao processador. Com esta linha, ligamos x a (max 5 9). Não pedimos ao computador que calcule o maior dos dois números. Simplesmente dizemos que x é o maior dos dois números. Não controlamos como e quando será calculado. Observe que isso é importante: x é o maior dos números. A relação “é” (“ser”, “ser”) é a diferença entre um paradigma de programação funcional, lógico e orientado a objetos e um paradigma processual.

Com uma mentalidade de computador, estamos no comando e controlamos o fluxo de instruções. Com uma maneira de pensar orientada a objetos, simplesmente determinamos quem é quem e deixamos que eles interajam quando precisam. Veja como o cálculo do maior de dois números deve parecer no OOP:

 class Max implements Number { private final Number a; private final Number b; public Max(Number left, Number right) { this.a = left; this.b = right; } } 

E então eu vou usá-lo:

 Number x = new Max(5, 9); 

Olha, eu não estou calculando o maior dos dois números. Eu determino que x é o maior dos dois números. Eu realmente não me importo com o que está dentro do objeto da classe Max e como exatamente ele implementa a interface Number. Não dou instruções ao processador em relação a este cálculo. Eu apenas instanciamos o objeto. Isso é muito semelhante ao def no Lisp. Nesse sentido, o OOP é muito semelhante à programação funcional.

Por outro lado, os métodos estáticos no OOP são os mesmos que as sub-rotinas em C ou assembler. Eles não estão relacionados ao OOP e nos forçam a escrever código processual na sintaxe orientada a objetos. Aqui está o código Java:

 int x = Math.max(5, 9); 

Isso está completamente errado e não deve ser usado no design real orientado a objetos.

Estilo declarativo versus imperativo


A programação imperativa “descreve cálculos em termos de operadores que alteram o estado de um programa”. A programação declarativa, por outro lado, “expressa a lógica da computação sem descrever o fluxo de sua execução” (cito a Wikipedia). De fato, conversamos sobre isso ao longo de várias páginas anteriores. A programação imperativa é semelhante ao que os computadores fazem - executando instruções em sequência. A programação declarativa está mais próxima da maneira natural de pensar em que temos entidades e relacionamentos entre elas. Obviamente, a programação declarativa é uma abordagem mais poderosa, mas uma abordagem imperativa é mais compreensível para os programadores de procedimentos. Por que a abordagem declarativa é mais poderosa? Não mude, e depois de algumas páginas chegamos ao ponto.

O que tudo isso tem a ver com métodos estáticos? Não importa se é um método ou objeto estático, ainda temos que escrever se (a> b) em algum lugar, certo? Sim exatamente. O método estático e o objeto são apenas um invólucro na instrução if, que executa a tarefa de comparar a com b. A diferença é como essa funcionalidade é usada por outras classes, objetos e métodos. E esta é uma diferença significativa. Considere isso com um exemplo.
Digamos que eu tenha um intervalo limitado a dois números inteiros e um número inteiro que deve cair nele. Eu tenho que garantir que sim. Aqui está o que devo fazer se o método max () for estático:

 public static int between(int l, int r, int x) { return Math.min(Math.max(l, x), r); } 

Precisamos criar outro método estático, between (), que usa os dois métodos estáticos disponíveis, Math.min () e Math.max (). Há apenas uma maneira de fazer isso - uma abordagem imperativa, pois o valor é calculado imediatamente. Quando faço uma chamada, recebo imediatamente o resultado:

 int y = Math.between(5, 9, 13); //  9 

Recebo o número 9 logo após ligar entre (). Quando a chamada é feita, meu processador começará imediatamente a trabalhar nesse cálculo. Esta é uma abordagem imperativa. E então, como é a abordagem declarativa?

Aqui, dê uma olhada:

 class Between implements Number { private final Number num; Between(Number left, Number right, Number x) { this.num = new Min(new Max(left, x), right); } @Override public int intValue() { return this.num.intValue(); } } 

É assim que eu vou usá-lo:

 Number y = new Between(5, 9, 13); //   ! 

Sente a diferença? Ela é extremamente importante. Esse estilo será declarativo, porque não digo ao processador que os cálculos precisam ser feitos imediatamente. Eu apenas determinei o que era e deixei que o usuário decidisse quando (e se era necessário) calcular a variável y usando o método intValue (). Talvez nunca seja calculado e meu processador nunca saberá qual é o número 9. Tudo o que fiz foi declarar o que é y. Acabou de anunciar. Ainda não dei nenhum trabalho ao processador. Conforme indicado na definição, expressou a lógica sem descrever o processo.

Eu já ouvi: “Ok, eu entendo você. Existem duas abordagens - declarativas e processuais, mas por que a primeira é melhor que a segunda? ” Mencionei anteriormente que é óbvio que a abordagem declarativa é mais poderosa, mas não explicou o porquê. Agora que examinamos as duas abordagens com exemplos, discutiremos as vantagens de uma abordagem declarativa.

Em primeiro lugar, é mais rápido. À primeira vista, pode parecer mais lento. Mas se você olhar mais de perto, ficará claro que, na verdade, é mais rápido, pois a otimização do desempenho está completamente em nossas mãos. De fato, levará mais tempo para criar uma instância da classe Between do que chamar o método static between (), pelo menos na maioria das linguagens de programação disponíveis no momento da redação deste livro. Espero realmente que tenhamos uma linguagem no futuro próximo. em que instanciar um objeto será tão rápido quanto chamar um método. Mas ainda não chegamos a ele. É por isso que a abordagem declarativa é mais lenta ... quando o caminho da execução é simples e direto.

Se estamos falando de uma simples chamada para um método estático, certamente será mais rápido do que criar uma instância de um objeto e chamar seus métodos. Porém, se tivermos muitos métodos estáticos, eles serão chamados sequencialmente ao resolver o problema, e não apenas para trabalhar nos resultados que realmente precisamos. Que tal isso:

 public void doIt() { int x = Math.between(5, 9, 13); if (/*  ? */) { System.out.println("x=" + x); } } 

Neste exemplo, calculamos x independentemente de precisarmos ou não de seu valor .. O processador encontrará o valor 9. Nos dois casos, o próximo método usando a abordagem declarativa funcionará tão rápido quanto o anterior?

 public void doIt() { Integer x = new Between(5, 9, 13); if (/*  ? */) { System.out.println("x=" + x); } } 

Eu acho que o código declarativo será mais rápido. É melhor otimizado. E não diz ao processador o que fazer. Pelo contrário, permite que o processador decida quando e onde o resultado é realmente necessário - os cálculos são realizados sob demanda.

O ponto principal é que a abordagem declarativa é mais rápida porque é ideal. Este é o primeiro argumento a favor de uma abordagem declarativa em comparação com o imperativo na programação orientada a objetos. O estilo imperativo definitivamente não tem lugar no OOP, e a primeira razão para isso é a otimização do desempenho. Você não deve dizer que quanto mais você controla a otimização do código, mais ele é seguido. Em vez de deixar a otimização do processo de cálculo para o compilador, máquina virtual ou processador, fazemos isso sozinhos.

O segundo argumento é polimorfismo. Simplificando, polimorfismo é a capacidade de quebrar dependências entre blocos de código. Suponha que eu queira alterar o algoritmo para determinar se um número cai em um determinado intervalo. É bastante primitivo em si, mas eu quero mudar isso. Eu não quero usar as classes Max e Min. E eu quero que ele faça uma comparação usando as instruções if-then-else. Veja como fazer isso declarativamente:

 class Between implements Number { private final Number num; Between(int left, int right, int x) { this(new Min(new Max(left, x), right)); } Between(Number number) { this.num = number; } } 

É a mesma classe Between que no exemplo anterior, mas com um construtor adicional. Agora eu posso usá-lo com outro algoritmo:

 Integer x = new Between( new IntegerWithMyOwnAlgorithm(5, 9, 13) ); 

Este provavelmente não é o melhor exemplo, já que a classe Between é muito primitiva, mas espero que você entenda o que quero dizer. A classe Between é muito fácil de separar das classes Max e Min, pois são classes. Na programação orientada a objetos, um objeto é um cidadão completo, mas o método estático não é. Podemos passar o objeto como argumento para o construtor, mas não podemos fazer o mesmo com o método estático. No OOP, os objetos são associados a objetos, se comunicam com objetos e trocam dados com eles. Para desanexar completamente um objeto de outros objetos, devemos garantir que ele não use o novo operador em nenhum de seus métodos (consulte a seção 3.6), bem como no construtor principal.

Deixe-me repetir: para desanexar completamente um objeto de outros objetos, você só precisa garantir que o novo operador não seja usado em nenhum de seus métodos, incluindo o construtor principal.

Você pode fazer o mesmo desacoplamento e refatoração com um trecho de código imperativo?

 int y = Math.between(5, 9, 13); 

Não, você não pode. O método static between () usa dois métodos estáticos, min () e max (), e você não pode fazer nada até reescrevê-lo completamente. E como você pode reescrevê-lo? Passar o quarto parâmetro para o novo método estático?

Quão feio vai parecer? Eu acho muito.

Aqui está o meu segundo argumento a favor de um estilo de programação declarativo - ele reduz a coesão dos objetos e o torna muito elegante. Sem mencionar o fato de que menos coesão significa mais facilidade de manutenção.

O terceiro argumento a favor da superioridade da abordagem declarativa sobre o imperativo - a abordagem declarativa fala dos resultados, enquanto o imperativo explica a única maneira de obtê-los. A segunda abordagem é muito menos intuitiva que a primeira. Preciso primeiro "executar" o código na minha cabeça para entender o resultado esperado. Aqui está uma abordagem imperativa:

 Collection<Integer> evens = new LinkedList<>(); for (int number : numbers) { if (number % 2 == 0) { evens.add(number); } } 

Para entender o que esse código faz, eu tenho que passar por ele, visualizar esse ciclo. Na verdade, eu tenho que fazer o que o processador faz - passar por toda a matriz de números e colocar os números pares na nova lista. Aqui está o mesmo algoritmo, escrito em um estilo declarativo:

 Collection<Integer> evens = new Filtered( numbers, new Predicate<Integer>() { @Override public boolean suitable(Integer number) { return number % 2 == 0; } } ); 

Esse trecho de código está muito mais próximo do idioma inglês do que o anterior. Ele lê da seguinte maneira: "evens é uma coleção filtrada que inclui apenas os elementos que são pares". Não sei exatamente como a classe Filtrada cria uma coleção - ela usa uma instrução for ou algo mais. Tudo o que preciso saber ao ler este código é que a coleção foi filtrada. Os detalhes da implementação estão ocultos e o comportamento é expresso.

Percebo que, para alguns leitores deste livro, foi mais fácil perceber o primeiro fragmento. É um pouco mais curto e muito semelhante ao que você vê diariamente no código com o qual está lidando. Garanto-lhe que isso é uma questão de hábito. Este é um sentimento enganador. Comece a pensar em termos de objetos e seu comportamento, em vez de algoritmos e sua execução, e você obterá uma verdadeira percepção. O estilo declarativo se relaciona diretamente aos objetos e seu comportamento, e o imperativo - aos algoritmos e à sua execução.

Se você achar esse código feio, tente o Groovy, por exemplo:

 def evens = new Filtered( numbers, { Integer number -> number % 2 == 0 } ); 

O quarto argumento é a integridade do código. Dê uma olhada nos dois trechos anteriores. Observe que no segundo fragmento declaramos o evens como um operador - evens = Filtrado (...). Isso significa que todas as linhas de código responsáveis ​​pelo cálculo dessa coleção estão próximas umas das outras e não podem ser separadas por engano. Pelo contrário, no primeiro fragmento não existe uma "colagem" óbvia das linhas. Você pode facilmente alterar a ordem deles por engano, e o algoritmo será interrompido.

Em um pedaço de código tão simples, esse é um pequeno problema, pois o algoritmo é óbvio. Mas se o fragmento do código imperativo for maior - digamos, 50 linhas, pode ser difícil entender quais linhas de código estão relacionadas entre si. Discutimos o problema da concatenação temporal um pouco antes - durante a discussão de objetos imutáveis. O estilo de programação declarativo também ajuda a eliminar essa concatenação , devido a qual manutenção é aprimorada.

Provavelmente ainda existem argumentos, mas citei o mais importante, do meu ponto de vista, daqueles relacionados ao POO. Espero ter conseguido convencê-lo de que estilo declarativo é o que você precisa. Alguns de vocês podem dizer: “Sim, entendo o que você quer dizer. Vou combinar abordagens declarativas e imperativas, quando apropriado. Usarei objetos onde faz sentido, e métodos estáticos quando precisar fazer algo simples rapidamente, como calcular o maior de dois números. ".." "Não, você está errado!" - Eu responderei. Você não deve combiná-los. Nunca use um estilo imperativo. Isso não é um dogma. Isso tem uma explicação muito pragmática.

O estilo imperativo não pode ser combinado com o declarativo puramente técnico. Quando você começa a usar a abordagem imperativa, está condenado - gradualmente todo o seu código se tornará imperativo.

Suponha que tenhamos dois métodos estáticos - max () e min (). Eles fazem pequenos cálculos rápidos, por isso os tornamos estáticos. Agora precisamos criar um algoritmo maior para determinar se o número pertence ao intervalo. Desta vez, queremos ir declarativamente - para criar a classe Between, e não o método estático between (). Podemos fazer isso? Provavelmente sim, mas de uma maneira substituta, e não como deveria ser. Não podemos usar construtores e encapsulamento. E forçado a fazer chamadas diretas e explícitas para métodos estáticos diretamente dentro da classe Between. Em outras palavras, não podemos escrever código puramente orientado a objetos se os componentes reutilizáveis ​​forem métodos estáticos.

Os métodos estáticos lembram o câncer de um software orientado a objetos: depois de permitir que eles se estabeleçam no código, não poderemos nos livrar deles - sua colônia só crescerá. Apenas ignore-os em princípio.

“Mas eu os tenho em todos os lugares! - você vai exclamar. "O que fazer?" O que posso dizer ... você tem problemas, como todos nós. Existem milhares de bibliotecas orientadas a objetos que são quase inteiramente compostas de classes de utilidade (iremos discuti-las na próxima seção). Aqui, como em um tumor, o melhor remédio é uma faca. Não use esses programas se puder pagar. No entanto, na maioria dos casos, você não pode usar uma faca, pois essas bibliotecas são muito populares e oferecem funcionalidade útil. Nesse caso, a melhor coisa que você pode fazer é isolar o tumor criando suas próprias classes que envolvem métodos estáticos para que seu código funcione exclusivamente com objetos. Por exemplo, na biblioteca do Apache Commons, existe um método estático FileUtils.readLines (), que lê todas as linhas de um arquivo de texto.Veja como podemos transformá-lo em um objeto:

 class FileLines implements Iterable<String> { private final File file; public Iterator<String> iterator() { return Arrays.asList( FileUtils.readLines(this.file) ).iterator(); } } 

Agora, para ler todas as linhas de um arquivo de texto, nosso aplicativo precisará fazer o seguinte:

 Iterable<String> lines = new FileLines(f); 

Um método estático será chamado apenas dentro da classe FileLines e, com o tempo, podemos nos livrar dele. Ou isso nunca vai acontecer. Mas a conclusão é que, em nosso código, os métodos estáticos não serão chamados em nenhum lugar, exceto em um local - a classe FileLines. Portanto, isolamos os mortos, o que nos permite lidar com eles gradualmente.

Classes de utilidade


As chamadas classes de utilitário, na verdade, não são classes, mas apenas um conjunto de métodos estáticos usados ​​por outras classes por conveniência (eles também são conhecidos como métodos auxiliares). Por exemplo, a classe java.lang.Math é um exemplo clássico de uma classe de utilitário. Esses produtos são muito populares em Java, Ruby e, infelizmente, em quase todas as linguagens de programação modernas. Por que eles não são classes? Por causa deles, você não pode instanciar objetos. Na seção 1.1, discutimos a diferença entre um objeto e uma classe e chegamos à conclusão de que uma classe é uma fábrica de objetos. A classe de utilitário não é uma fábrica, por exemplo:

 class Math { private Math() { //   } public static int max(int a, int b) { if (a < b) { return b; } return a; } } 

É uma boa prática para quem usa classes de utilitário criar um construtor privado, como no exemplo, para evitar a criação de uma instância da classe. Como o construtor é privado, ninguém além dos métodos da classe pode instanciar a classe.

As classes de utilitário são o triunfo dos programadores de procedimentos no campo da programação orientada a objetos. Uma classe de utilidade não é apenas uma coisa terrível, como um método estático - é um monte de coisas terríveis. Todos os palavrões falados sobre métodos estáticos podem ser repetidos com amplificação múltipla. As classes de utilidade são um terrível antipadrão no OOP. Fique longe delas.

Padrão Singleton


O padrão Singleton é uma técnica popular que afirma ser uma substituição de métodos estáticos. De fato, haverá apenas um método estático na classe, e o singleton quase parecerá um objeto real. No entanto, ele não é:

 class Math { private static Math INSTANCE = new Math(); private Math() {} public static Math getInstance() { return Math.INSTANCE; } public int max(int a, int b) { if (a < b) { return b; } return a; } } 

O exemplo acima é um exemplo típico de singleton. Há uma única instância da classe Math chamada INSTANCE. Todos podem acessá-la simplesmente chamando getInstance (). O construtor é tornado privado, a fim de impedir a instanciação direta de objetos dessa classe. A única maneira de acessar o INSTANCE é chamar getInstance ().

Singleton é conhecido como padrão de design, mas, na realidade, é um terrível antipadrão. Há muitas razões pelas quais essa é uma técnica de programação ruim. Vou dar apenas alguns deles sobre métodos estáticos. Obviamente, seria mais fácil se discutíssemos primeiro como o singleton difere da classe de utilidade sobre a qual acabamos de falar. Aqui está a aparência de uma classe de utilitário de matemática, que faz o mesmo que o singleton fornecido anteriormente:

 class Math { private Math() {} public static int max(int a, int b) { if (a < b) { return b; } return a; } } 

É assim que o método max () será usado:

 Math.max(5, 9); // - Math.getInstance().max(5, 9); //  

Qual a diferença? Parece que a segunda linha é apenas mais longa, mas faz o mesmo. Por que reinventar o singleton se já tínhamos métodos estáticos e classes de utilidade? Costumo fazer essa pergunta em entrevistas com programadores Java. A primeira coisa que geralmente ouço em resposta é: "Singleton permite que você encapsule um estado". Por exemplo:

 class User { private static User INSTANCE = new User(); private String name; private User() {} public static User getInstance() { return User.INSTANCE; } public String getName() { return this.name; } public String setName(String txt) { this.name = txt; } } 

Este é um pedaço terrível de código, mas tenho que citar isso como uma ilustração para meus argumentos. Esse singleton significa literalmente "o usuário atualmente usando o sistema". Essa abordagem é muito popular em muitas estruturas da Web onde existem singles de usuários, sessões da Web etc. Portanto, a resposta típica para minha pergunta sobre a diferença entre um singleton e uma classe de utilitário é: “Singleton encapsula estado” .. Mas isso está incorreto a resposta O objetivo do singleton não é armazenar o estado. Aqui está uma classe de utilitário que faz o mesmo que o singleton mencionado anteriormente:

 class User { private static String name; private User() {} public static String getName() { return User.name; } public static String setName(String txt) { User.name = txt; } } 

Essa classe de utilitário armazena o estado e não há diferença entre ela e o singleton mencionado. Então qual é o problema? E qual é a resposta correta? A única resposta verdadeira é que um singleton é uma dependência que pode ser quebrada e uma classe de utilitário é uma conexão estreita codificada que não pode ser quebrada. Em outras palavras, a vantagem das singletones é que você pode adicionar o método setInstance () a elas, juntamente com getInstance (). Esta resposta está correta, embora eu a ouça com pouca frequência. Suponha que eu use singleton da seguinte maneira:

 Math.getInstance().max(5, 9); 

Meu código está vinculado à classe Math. Em outras palavras, a classe Math é a dependência em que confio. Sem essa classe, o código não funcionaria e, para testá-lo, eu teria que deixar a classe Math disponível para poder atender a solicitações. No caso dessa classe em particular, esse problema é pequeno, pois é muito primitivo. No entanto, se o singleton for grande, talvez seja necessário aplicar zombaria ou substituí-lo por algo mais adequado para o teste. Simplificando, não quero que o método Math.max () seja executado enquanto o teste de unidade estiver em execução. Como eu faço isso? E aqui está como:

 Math math = new FakeMath(); Math.setInstance(math); 

O padrão Singleton fornece a capacidade de substituir um objeto estático encapsulado, o que permite testar o objeto. A verdade é a seguinte: um singleton é muito melhor do que uma classe de utilitário apenas porque permite substituir um objeto encapsulado. Não há objeto na classe de utilitário - não podemos mudar nada. Uma classe de utilidade - uma dependência codificada e inextricável - é o mal mais puro da OOP.

Então, o que eu estou falando? Singleton é melhor que a classe de utilidade, mas ainda é antipadrão e bastante ruim. PorqueComo lógica e tecnicamente, um singleton é uma variável global, nem mais nem menos. Mas no OOP não há escopo global. Portanto, variáveis ​​globais não pertencem aqui. Aqui está um programa C no qual uma variável é declarada no escopo global:

 #include <stdio> int line = 0; void echo(char* text) { printf("[%d] %s\n", ++line, text); } 

Sempre que chamamos echo (), a linha variável global é incrementada. Puramente tecnicamente, a variável de linha é visível em cada função e cada linha de código no arquivo * .s É visível globalmente. Elogie os desenvolvedores Java por não copiarem esse recurso do C. No Java, como no Ruby e em muitas outras linguagens sub-OOP, variáveis ​​globais são proibidas. PorquePorque eles não têm nada a ver com OOP. Esta é uma oportunidade puramente processual. Variáveis ​​globais violam claramente o princípio do encapsulamento. Eles são simplesmente terríveis. Espero não ter mais que explicar isso neste livro. Parece-me óbvio que variáveis ​​globais são tão ruins quanto o operador GOTO é ruim.

No entanto, apesar de todos os argumentos contra variáveis ​​globais, alguém encontrou uma maneira de trazê-las para Java, criando assim o padrão "Singleton". Isso é simplesmente uma zombaria dos princípios do design orientado a objetos, possibilitado pela presença de métodos estáticos. Esses métodos tecnicamente permitem essa fraude.
Nunca use singletones. Nem pense.

Como substituí-los? - você pergunta. "Se precisamos de algo acessível a muitas classes de todo o produto de software, o que podemos fazer?" Digamos que realmente precisamos da maioria das classes para saber qual usuário está conectado no momento. Não temos classes de utilidade ou singletones. O que nós temos? Encapsulamento!

Apenas encapsule o usuário em todos os objetos nos quais ele pode ser útil.

Tudo o que sua classe precisa para trabalhar deve ser passado pelo construtor e encapsulado dentro da classe. Isso é tudo. Sem exceção. Um objeto não deve afetar nada além de suas propriedades encapsuladas. Você pode dizer que precisa encapsular demais: conexões com os bancos de dados do usuário logado, argumentos de linha de comando etc. Sim, de fato, tudo isso pode ser demais se a classe for muito grande e não for concluída o suficiente. Se você precisar encapsular muito, redesenhe a classe - reduza-a, conforme discutido na seção 2.1.

Mas nunca use singleton. Não há exceções a esta regra.

Programação funcional


Costumo ouvir esse argumento: se os objetos são pequenos e imutáveis ​​e os métodos estáticos não estão envolvidos, por que não usar a programação funcional (FP)? De fato, se os objetos são tão elegantes quanto o recomendado neste livro, eles são muito semelhantes às funções. Então, por que precisamos de objetos? Por que não usar Lisp, Clojure ou Haskell em vez de Java ou C ++?

Aqui está uma classe que representa um algoritmo para determinar o maior de dois números:

 class Max implements Number { private final int a; private final int b; public Max(int left, int right) { this.a = left; this.b = right; } @Override public int intValue() { return this.a > this.b ? this.a : this.b; } } 

Eis como devemos aplicá-lo:

 Number x = new Max(5, 9); 

E aqui está como definiríamos uma função no Lisp que faria o mesmo:

 (defn max (ab) (if (> ab) ab)) 

Então, por que usar objetos? O código Lisp é muito mais curto.

OOP é mais expressivo e possui ótimos recursos, pois opera em objetos e métodos, enquanto funções somente de FP. Algumas linguagens FP também têm objetos, mas eu as chamaria de linguagens OOP com recursos FP, e não vice-versa. Eu também acredito que as expressões lambda em Java, sendo um movimento em direção ao FP, tornam o Java mais livre, nos afastando do verdadeiro caminho OOP. AF é um grande paradigma, mas POO é melhor. Especialmente quando usado corretamente.

Parece-me que, em uma linguagem OOP ideal, teríamos aulas com funções internas. Não métodos de microprocedimento, como é agora em Java, mas funções reais (no sentido de um paradigma funcional) que possuem um único ponto de saída. Isso seria perfeito.

Decoradores compostáveis


Parece que cunhei o termo. Decoradores vinculáveis ​​são simplesmente objetos de invólucro sobre outros objetos. Eles são decoradores - um padrão bem conhecido de design orientado a objetos - mas se tornam compostáveis ​​quando os combinamos em estruturas multicamadas, por exemplo:

 names = new Sorted( new Unique( new Capitalized( new Replaced( new FileNames( new Directory( "/var/users/*.xml" ) ), "([^.]+)\\.xml", "$1" ) ) ) ); 

Esse código, do meu ponto de vista, parece muito limpo e orientado a objetos. É exclusivamente declarativo, conforme explicado na seção 3.2. Ele não faz nada, apenas declara um objeto de nomes, que é uma coleção classificada de seqüências maiúsculas exclusivas que representam os nomes dos arquivos em um diretório que são modificados por uma expressão regular específica. Acabei de explicar o que é esse objeto, sem dizer uma palavra sobre como ele é organizado. Acabei de anunciar.

Você acha esse código limpo e fácil de entender? Espero que sim, levando em consideração tudo o que falamos anteriormente.

É isso que chamo de decoradores composíveis. As classes Directory, FileNames, Replaced, Capitalized, Unique e Sorted são decoradoras, porque seu comportamento é completamente determinado pelos objetos que eles encapsulam. Eles adicionam algum comportamento aos objetos encapsulados. Seu estado coincide com o estado dos objetos encapsulados.

Às vezes, eles fornecem a mesma interface que os objetos que encapsulam (mas isso não é necessário). Por exemplo, Exclusivo é Iterável, que também encapsula um iterador de linha. No entanto, FileNames é um iterador de linha que encapsula um iterador de arquivo.
A maior parte do código em software orientado a objetos puro deve ser semelhante ao fornecido anteriormente. Temos que compor os decoradores um no outro, e até um pouco mais do que isso. Em algum momento, chamamos app.run (), e toda a pirâmide de objetos começa a responder. O código não deve ter todas as instruções processuais como if, for, switch e while. Parece utopia, mas não é utopia.

A instrução if é fornecida pela linguagem Java e é usada por nós de maneira processual, operador por operador. Por que não criar uma linguagem Java de substituição na qual a classe If seria? Em vez do código de procedimento a seguir:

 float rate; if (client.age() > 65){ rate = 2.5; } else { rate = 3.0; } 

nós escreveríamos esse código orientado a objetos:

 float rate = new If( client.age() > 65, 2.5, 3.0 ); 

E esse aqui?

 float rate = new If( new Greater(client.age(), 65), 2.5, 3.0 ); 

E, finalmente, a última melhoria:

 float rate = new If( new GreaterThan( new AgeOf(client), 65 ), 2.5, 3.0 ); 

É assim que parece o código declarado e orientado a objeto puro. Ele não faz nada - ele simplesmente anuncia qual é a taxa.

Do meu ponto de vista, em uma OOP pura, não são necessários operadores herdados de linguagens procedurais como C. Se, por, alternar e enquanto não forem necessários. Precisamos das classes If, For, Switch e While. Sente a diferença?

Ainda não alcançamos essas línguas, mas mais cedo ou mais tarde certamente chegaremos a ela. Eu tenho certeza disso. Enquanto isso, tente ficar longe de métodos longos e procedimentos complicados. Projete microclasses para que sejam compostáveis. Verifique se eles podem ser reutilizados como elementos de composição em objetos maiores.
Eu diria que a programação orientada a objetos é a montagem de objetos grandes a partir de objetos menores.

O que isso tem a ver com métodos estáticos? Tenho certeza que você já entendeu: métodos estáticos não podem ser vinculados de forma alguma. Eles inviabilizam tudo o que falei e o que mostrei anteriormente. Não podemos coletar objetos grandes de objetos menores usando métodos estáticos. Esses métodos são contrários à idéia de layout. Aqui está outra razão pela qual os métodos estáticos são pura maldade.

Concluindo: em lugar nenhum e nunca use a palavra-chave estática no seu código - isso provará para você e para aqueles que usarão seu código um ótimo serviço.

»Mais informações sobre o livro podem ser encontradas no site do editor

20% de desconto no cupom para Java Fermenters - Java

Source: https://habr.com/ru/post/pt418157/


All Articles