Design de Classe Orientada a Objetos


12

Eu estava pensando sobre um bom design de classe orientada a objetos. Em particular, tenho dificuldade em decidir entre essas opções:

  1. método estático vs instância
  2. método sem parâmetros ou valor de retorno vs método com parâmetros e valor de retorno
  3. sobreposição vs funcionalidade de método distinto
  4. método privado versus público

Exemplo 1:

Essa implementação usa métodos de instância, sem valor de retorno ou parâmetros, sem funcionalidade de sobreposição e todos os métodos public

XmlReader reader = new XmlReader(url);
reader.openUrl();
reader.readXml();
Document result = reader.getDocument();

Exemplo 2:

Essa implementação usa métodos estáticos, com valores e parâmetros de retorno, com funcionalidade sobreposta e métodos privados

Document result = XmlReader.readXml(url); 

No exemplo um, todos os métodos são de instância pública, o que os torna fáceis de testar na unidade. Embora todos os métodos sejam distintos, o readXml () depende de openUrl (), pois openUrl () deve ser chamado primeiro. Todos os dados são declarados nos campos da instância, portanto, não há valores ou parâmetros de retorno em nenhum método, exceto no construtor e nos acessadores.

No exemplo dois, apenas um método é público, o restante é estático privado, o que dificulta o teste de unidade. Os métodos estão sobrepostos, pois o readXml () chama openUrl (). Não há campos, todos os dados são passados ​​como parâmetros nos métodos e o resultado é retornado imediatamente.

Quais princípios devo seguir para fazer uma programação orientada a objetos adequada?


3
As coisas estáticas são ruins quando você faz multithreading. No outro dia, eu tinha um XMLWriter estático, como XMLWriter.write (data, fileurl). No entanto, como ele tinha um FileStream estático privado, usar essa classe de vários threads ao mesmo tempo, fez com que o segundo thread substituísse os primeiros threads do FileStream, causando um erro que seria muito difícil de encontrar. Classes estáticas com membros estáticos + multi-threading é uma receita para o desastre.
Por Alexandersson

1
@Paxinum. O problema que você descreve é ​​um problema de estado, não um problema "estático". Se você usasse um singleton com membros não estáticos, teria o mesmo problema com o multiencadeamento.
precisa saber é o seguinte

2
Os métodos estáticos do @Per Alexandersson não são ruins em relação à simultaneidade. O estado estático é ruim. É por isso que a programação funcional, na qual todos os métodos são estáticos, funciona muito bem em situações simultâneas.
Yuli Bonner

Respostas:


11

O exemplo 2 é muito ruim para testes ... e não quero dizer que você não possa testar os internos. Você também não pode substituir seu XmlReaderobjeto por um objeto simulado, pois não possui nenhum objeto.

O exemplo 1 é desnecessariamente difícil de usar. A respeito

XmlReader reader = new XmlReader(url);
Document result = reader.getDocument();

que não é mais difícil de usar do que o seu método estático.

Coisas como abrir o URL, ler XML, converter bytes em strings, analisar, fechar soquetes e o que for, não são interessantes. Criar um objeto e usá-lo é importante.

Então, IMHO, o projeto OO adequado é tornar públicas apenas as duas coisas (a menos que você realmente precise das etapas intermediárias por algum motivo). Estática é má.


-1. Você realmente pode substituir o XmlReader por um objeto simulado. Não com uma estrutura simples de código aberto, mas com uma boa classificação industrial, você pode;) Custa algumas centenas de dólares por desenvolvedor, mas faz maravilhas para testar a funcionalidade selada nas APIs que você publica.
TomTom

2
+1 por não atender ao discurso de vendas da TomTom. Quando quero um forro, prefiro escrever algo como Document result = new XmlReader(url).getDocument();Por que? Para que eu possa atualizá-lo Document result = whateverReader.getDocument();e ter whateverReaderme entregue por outra coisa.
Candied_orange 26/09/19

6

Aqui está a questão: não existe uma resposta certa e não existe uma definição absoluta de "design orientado a objetos adequado" (algumas pessoas oferecem uma, mas são ingênuas ... dão tempo).

Tudo se resume aos seus objetivos.

Você é um artista e o papel está em branco. Você pode desenhar um retrato lateral em preto e branco delicado e com lápis de cor ou uma pintura abstrata com enormes cortes de néons mistos. Ou qualquer coisa no meio.

Então, o que parece certo para o problema que você está resolvendo? Quais são as reclamações das pessoas que precisam usar suas aulas para trabalhar com xml? O que é difícil no trabalho deles? Que tipo de código eles estão tentando escrever que envolve as chamadas para sua biblioteca e como você pode ajudar esse fluxo a melhorar para eles?

Eles gostariam de mais sucessão? Eles gostariam que fosse muito inteligente em descobrir os valores padrão dos parâmetros, para que não precisassem especificar muito (ou nada) e adivinhar corretamente? Você pode automatizar as tarefas de configuração e limpeza que sua biblioteca exige, para que seja impossível que elas esqueçam essas etapas? O que mais você pode fazer por eles?

Inferno, o que você provavelmente precisa fazer é codificá-lo de 4 ou 5 maneiras diferentes, colocar seu chapéu de consumidor e escrever um código que use todos os 5, e ver qual é o melhor. Se você não pode fazer isso para toda a sua biblioteca, faça-o para um subconjunto. E você também precisa adicionar algumas alternativas à sua lista - que tal uma interface fluente, uma abordagem mais funcional, parâmetros nomeados ou algo baseado no DynamicObject, para que você possa criar "pseudo-métodos" significativos que os ajudem Fora?

Por que o jQuery é o rei agora? Como Resig e equipe seguiram esse processo, até encontrar um princípio sintático que reduziu incrivelmente a quantidade de código JS necessária para trabalhar com o dom e os eventos. Esse princípio sintático não estava claro para eles ou para qualquer outra pessoa quando começaram. Eles a encontraram.

Como programador, é assim que você chama mais. Você procura no escuro tentando coisas até encontrá-lo. Quando fizer isso, você saberá. E você dará a seus usuários um enorme salto de produtividade. E é disso que trata o design (na área de software).


1
Isso implica que não há diferenças entre as melhores práticas e outras práticas além da "sensação". É assim que você termina com bolas de lama não-sustentáveis ​​- porque para muitos desenvolvedores, alcançando além dos limites da classe, etc., "parece" simplesmente maravilhoso.
Amy Blankenship

@ Amy Blankenship, eu diria inequivocamente que não existe uma "melhor maneira" de fazer as escolhas que o OP está perguntando. Depende de um milhão de coisas e há um milhão de graus de liberdade. No entanto, acho que há um lugar para "Boas Práticas", e isso é em um ambiente de equipe , em que certas escolhas foram feitas, e precisamos que o restante da equipe permaneça consistente com as escolhas anteriores. Em outras palavras, em um contexto específico , pode haver razões para rotular certas coisas de "melhores práticas". Mas o OP não deu nenhum contexto. Ele está construindo alguma coisa e ...
Charlie Flowers

... ele enfrenta todas essas opções possíveis. Não existe uma "resposta certa" para essas escolhas. É conduzido pelos objetivos e pelos pontos problemáticos do sistema. Garanto que os programadores Haskell não pensam que todos os métodos devem ser métodos de instância. E os programadores de kernel Linux não acham muito importante tornar as coisas acessíveis ao TDD. E os programadores de jogos em C ++ geralmente preferem agrupar seus dados em uma estrutura de dados restrita na memória do que encapsular tudo em objetos. Toda "melhor prática" é apenas uma "melhor prática" em um determinado contexto e é um antipadrão em outro contexto.
Charlie Flowers

@AmyBlankenship Mais uma coisa: eu discordo que alcançar os limites da classe "parece maravilhoso demais". Isso leva a bolas de lama insustentáveis, que parecem horríveis . Acho que você está tentando resolver o problema de alguns trabalhadores serem desleixados / desmotivados / muito inexperientes. Nesse caso, alguém cuidadoso, motivado e experiente deve fazer as principais escolhas e chamá-las de "Melhores práticas". Porém, a pessoa que escolhe essas "Melhores Práticas" ainda está fazendo escolhas com base no que "parece certo" e não há respostas corretas. Você está apenas controlando quem faz as escolhas.
Charlie Flowers

Eu trabalhei com vários programadores que se consideravam seniores e que a gerência acreditava firmemente que a estática e os Singletons eram absolutamente o caminho certo para lidar com os problemas de comunicação. A parte estática dessa pergunta nem teria sido perguntada se estender os limites da classe como "parece" errado para os desenvolvedores, nem a resposta que advoga a alternativa estática receberia votos positivos.
Amy Blankenship

3

A segunda opção é melhor, pois é mais simples para as pessoas usarem (mesmo que seja apenas você), muito mais simples.

Para testes de unidade, basta testar a interface e não os internos, se você realmente deseja mover os internos de privado para protegido.


2
Você também pode tornar seu pacote de métodos privado (padrão), se seus casos de teste estiverem no mesmo pacote, para fins de teste de unidade.

Bom ponto - isso é melhor.

3

1. Estático versus instância

Eu acho que existem diretrizes muito claras sobre o que é bom design de OO e o que não é. O problema é que a blogosfera torna difícil separar o bom do ruim e o feio. Você pode encontrar algum tipo de referência que apóie até a pior prática possível.

E a pior prática que consigo pensar é no Estado Global, incluindo as estáticas que você mencionou e a Singleton favorita de todos. Alguns trechos do artigo clássico de Misko Hevery sobre o assunto .

Para realmente entender as dependências, os desenvolvedores devem ler todas as linhas de código. Faz com que a ação assustadora à distância: ao executar conjuntos de testes, o estado global alterado em um teste pode causar a falha inesperada de um teste subsequente ou paralelo. Quebre a dependência estática usando injeção de dependência manual ou Guice.

Ação assustadora à distância é quando executamos uma coisa que acreditamos estar isolada (já que não passamos nenhuma referência), mas interações inesperadas e mudanças de estado acontecem em locais distantes do sistema sobre os quais não falamos ao objeto. Isso só pode acontecer via estado global.

Você pode não ter pensado dessa maneira antes, mas sempre que usa o estado estático, está criando canais de comunicação secretos e não os deixando claros na API. A ação assustadora à distância força os desenvolvedores a ler todas as linhas de código para entender as interações em potencial, reduzir a produtividade do desenvolvedor e confundir os novos membros da equipe.

O que isso se resume é que você não deve fornecer referências estáticas para qualquer coisa que possua algum tipo de estado armazenado. O único lugar em que uso a estática é para constantes enumeradas e tenho dúvidas sobre isso.

2. Métodos com parâmetros de entrada e valores de retorno vs. métodos sem nenhum

O que você precisa entender é que os métodos que não têm parâmetros de entrada e de saída são praticamente garantidos para operar em algum tipo de estado armazenado internamente (caso contrário, o que eles estão fazendo?). Existem idiomas inteiros criados com a idéia de evitar o estado armazenado.

Sempre que você tiver armazenado o estado, você tem a possibilidade de efeitos colaterais, portanto, sempre use-o com atenção. Isso implica que você deve preferir funções com entradas e / ou saídas definidas.

E, de fato, funções que definiram entradas e saídas são muito mais fáceis de testar - você não precisa executar uma função aqui e ir lá para ver o que aconteceu, e não precisa definir uma propriedade em algum lugar mais antes de executar a função em teste.

Você também pode usar com segurança esse tipo de função como estática. No entanto, eu não o faria, porque, se mais tarde eu quisesse usar uma implementação ligeiramente diferente dessa função em algum lugar, em vez de fornecer uma instância diferente com a nova implementação, ficaria preso sem nenhuma maneira de substituir a funcionalidade.

3. Sobreposição vs. Distinto

Eu não entendo a pergunta. Qual seria a vantagem em 2 métodos sobrepostos?

4. Privado x Público

Não exponha nada que não precise expor. No entanto, também não sou muito fã de privado. Não sou desenvolvedor de C #, mas desenvolvedor de ActionScript. Passei muito tempo no código Flex Framework da Adobe, que foi escrito por volta de 2007. E eles fizeram algumas escolhas muito ruins sobre o que tornar privado, o que torna um pesadelo tentar estender suas Classes.

Portanto, a menos que você pense que é um arquiteto melhor do que os desenvolvedores da Adobe em 2007 (da sua pergunta, eu diria que você tem mais alguns anos antes de ter a chance de fazer essa afirmação), provavelmente deseja apenas usar o padrão protegido .


Existem alguns problemas com seus exemplos de código, o que significa que eles não são bem arquitetados, portanto, não é possível escolher A ou B.

Por um lado, você provavelmente deve separar sua criação de objeto do seu uso . Então você normalmente não tem o seu new XMLReader()direito próximo ao local onde é usado.

Além disso, como o @djna diz, você deve encapsular os métodos usados ​​no XML Reader, para que sua API (exemplo de instância) possa ser simplificada para:

_document Document = reader.read(info);

Não sei como o C # funciona, mas, como já trabalhei com várias tecnologias da Web, desconfio que você nem sempre poderá retornar um documento XML imediatamente (exceto, talvez, como promessa ou tipo futuro). objeto), mas não posso dar conselhos sobre como lidar com uma carga assíncrona em c #.

Observe que, com essa abordagem, você pode criar várias implementações que podem usar um parâmetro que diz onde / o que ler e retornar um objeto XML e trocá-los com base nas necessidades do seu projeto. Por exemplo, você pode estar lendo diretamente de um banco de dados, de uma loja local ou, como no exemplo original, de um URL. Você não pode fazer isso se usar um método estático.


2

Foco na perspectiva do cliente.

IReader reader = new XmlReader.readXml(url);  // or injection, or factory or ...
Document document = reader.read();

Os métodos estáticos tendem a limitar a evolução futura, nosso cliente está trabalhando em termos de uma interface fornecida por possivelmente muitas implementações diferentes.

O principal problema com o seu idioma de leitura / abertura é que o cliente precisa saber a ordem na qual chamar métodos, quando ele quer apenas fazer um trabalho simples. Óbvio aqui, mas em uma classe maior está longe de ser óbvio.

O método principal para testar é read (). Os métodos internos podem ser tornados visíveis para testar programas, tornando-os nem públicos nem privados e colocando testes no mesmo pacote - os testes ainda podem ser mantidos separados do código liberado.


Os métodos com visibilidade padrão ainda estão visíveis, se o conjunto de testes estiver em um projeto diferente?
S27i

Java não sabe sobre projetos. Projetos são uma construção IDE. O compilador e a JVM examinam os pacotes nas quais as classes testadas e testadas estão no mesmo pacote, a visibilidade padrão é permitida. No Eclipse, uso um único projeto, com dois diretórios de origem diferentes. Eu apenas tentei com dois projetos, funciona.

2

método estático vs instância

Na prática, você encontrará métodos estáticos geralmente confinados a classes de utilidade e não devem sobrecarregar seus objetos de domínio, gerentes, controladores ou DAOs. Os métodos estáticos são mais úteis quando todas as referências necessárias podem ser passadas razoavelmente como parâmetros e oferecem algumas funcionalidades que podem ser reutilizadas em muitas classes. Se você se encontrar usando um método estático como uma solução alternativa para ter uma referência a um objeto de instância, pergunte-se por que você simplesmente não tem essa referência.

método sem parâmetros ou valor de retorno vs método com parâmetros e valor de retorno

Se você não precisar de parâmetros no método, não os adicione. O mesmo acontece com o valor de retorno. Manter isso em mente simplificará seu código e garantirá que você não esteja codificando para uma infinidade de cenários que nunca acabam acontecendo.

sobreposição vs funcionalidade de método distinto

É uma boa idéia tentar evitar a sobreposição de funcionalidades. Às vezes, pode ser difícil, mas quando é necessária uma alteração na lógica, é muito mais fácil alterar um método que é reutilizado do que alterar vários métodos com funcionalidade semelhante

método privado versus público

Em geral, getters, setters e construtores devem ser públicos. Tudo o resto você desejará tentar manter em sigilo, a menos que haja um caso em que outra classe precise executá-la. Manter os métodos padronizados como privados ajudará a manter o encapsulamento . O mesmo vale para os campos, acostume-se ao privado como padrão


1

Não responderei à sua pergunta, mas acho que um problema foi criado pelos termos que você usou. Por exemplo

XmlReader.read => twice "read"

Eu acho que você precisa de um XML, por isso vou criar um objeto XML que pode ser criado a partir de um tipo de texto (não sei C # ... em Java, ele é chamado String). Por exemplo

class XML {
    XML(String text) { [...] }
}

Você pode testá-lo e está claro. Então, se você precisar de uma fábrica, poderá adicionar um método de fábrica (e pode ser estático como no seu segundo exemplo). Por exemplo

class XML {
    XML(String text) { [...] }

    static XML fromUrl(url) { [...] }

}

0

Você pode seguir algumas regras simples. Ajuda se você entender os motivos das regras.

método estático vs instância

Para métodos, você não precisa tomar essa decisão conscientemente. Se parecer que seu método não está usando nenhum membro do campo (seu analisador favorito deve informar isso), você deve adicionar a palavra-chave estática.

método sem parâmetros ou valor de retorno vs método com parâmetros e valor de retorno

A segunda opção é melhor por causa do escopo. Você deve sempre manter seu escopo apertado. Chegar às coisas de que você precisa é ruim, você deve ter sugestões, trabalhar localmente e retornar um resultado. Sua primeira opção é ruim pelo mesmo motivo que variáveis ​​globais geralmente são ruins: elas são significativas para apenas uma parte do seu código, mas são visíveis (e, portanto, ruídos) em qualquer outro lugar e podem ser alteradas de qualquer lugar. Isso dificulta a obtenção de uma imagem completa da sua lógica.

sobreposição vs funcionalidade de método distinto

Eu não acho que isso seja um problema. Métodos que chamam outros métodos são bons se isso dividir tarefas em pequenos pedaços distintamente funcionais.

método privado versus público

Torne tudo privado, a menos que você precise que ele seja público. O usuário da sua classe pode ficar sem o barulho, ele quer ver apenas o que importa para ele.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.