Quando a subclasse em vez de diferenciar o comportamento

votos
9

Estou tendo dificuldades decidir quando eu deveria estar subclasse em vez de apenas adicionar uma variável de instância que representa diferentes modos de classe e, em seguida, deixar que os métodos da classe agir de acordo com o modo selecionado.

Por exemplo, digamos que eu tenho um carro básico classe. No meu programa eu vou lidar com três tipos diferentes de carros. Carros de corrida , ônibus e modelos familiares . Cada um terá sua própria implementação de engrenagens, como eles se transformam e instalação do assento. Devo subclasse meu carro em três modelos diferentes ou devo criar uma variável do tipo e fazer as engrenagens, girando e assentos genérico para que eles agiriam diferente dependendo de qual tipo de carro foi selecionado?

Na minha situação atual que estou trabalhando em um jogo, e eu vim a perceber que ele está começando a ficar um pouco confuso, então eu pedir conselhos sobre possivelmente refatoração do meu código atual. Basicamente, existem mapas diferentes, e cada mapa pode ser um dos três modos. Dependendo do modo que o mapa é definido como haverá comportamento diferente e o mapa será construído de uma maneira diferente. Em um modo eu poderia ter de dar aluguéis para jogadores e gerar criaturas em uma base de tempo limite, no qual outro o jogador é responsável por desova as criaturas e ainda em outro pode haver algumas automatizados gerou criaturas juntamente com uns e jogadores construção de edifícios jogador gerou . Então, eu estou querendo saber se seria melhor ter um mapa Classe de base e, em seguida, subclasse-lo em cada um dos diferentes modos,

Publicado 27/08/2009 em 02:18
fonte usuário
Em outras línguas...                            


5 respostas

votos
7

Todos os créditos para AtmaWeapon de http://www.xtremevbtalk.com respondendo neste tópico

Core para ambas as situações é o que eu sinto é a regra fundamental de projeto orientado a objetos: o princípio da responsabilidade única. Duas maneiras de expressá-lo são:

"A class should have one, and only one, reason to change."
"A class should have one, and only one, responsibility."

SRP é um ideal que nem sempre podem ser atendidas, e seguindo este princípio é difícil . I tendem a disparar para "A classe deve ter o mínimo de responsabilidades quanto possível." Nossos cérebros são muito bons em convencer-nos de que uma classe única muito complicada é menos complicado do que várias classes muito simples. Eu comecei a fazer o meu melhor para escrever classes menores ultimamente, e eu experimentei uma diminuição significativa no número de erros no meu código. Dê-lhe um tiro por alguns projetos antes de descartá-la.

A primeira vez que propõem que em vez de começar o projeto, criando uma classe mapa base e três classes filho, comece com um design que separa os comportamentos únicos de cada mapa em uma classe secundária que representa "o comportamento mapa" genérico. Este post está preocupado em provar esta abordagem é superior. É difícil para mim ser específico sem um conhecimento bastante íntimo de seu código, mas eu vou usar uma noção muito simples de um mapa:

Public Class Map
    Public ReadOnly Property MapType As MapType

    Public Sub Load(mapType)
    Public Sub Start()
End Class

MapType indica qual dos três tipos de mapa do mapa representa. Quando você quiser mudar o tipo de mapa, você chama Load()com o tipo de mapa que você deseja usar; isso faz tudo o que ele precisa fazer para limpar o estado atual do mapa, redefinir o fundo, etc. Depois de um mapa é carregado, Iniciar () é chamado. Se o mapa tem quaisquer comportamentos como "monstro desova x cada y segundos", Iniciar () é responsável por configurar esses comportamentos.

Isto é o que você tem agora, e você é sábio pensar que é uma má idéia. Desde que eu mencionei SRP, vamos contar as responsabilidades do Mapa .

  • Tem de gerir informações de estado para todos os três tipos de mapa. (3+ responsabilidades *)
  • Load() tem que entender como limpar o estado para todos os três tipos de mapa e como configurar o estado inicial para todos os três tipos de mapas (6 responsabilidades)
  • Start()tem que saber o que fazer para cada tipo de mapa. (3) responsabilidades

** Tecnicamente cada variável é uma responsabilidade, mas eu tenho simplificado-lo. *

Para o total final, o que acontece se você adicionar um quarto tipo mapa? Você tem que adicionar mais variáveis de estado (1+ responsabilidades), update Load()para ser capaz de limpar e inicializar o estado (2 responsabilidades), e atualização Start()para lidar com o novo comportamento (1 responsabilidade). Assim:

Número de Mapresponsabilidades: 12+

Número de mudanças necessárias para novo mapa: 4+

Há outros problemas também. As probabilidades são, vários dos tipos de mapa terá informações estado semelhante, assim que você vai compartilhar variáveis entre os estados. Isso torna mais provável que Load()se esqueça de definir ou limpar uma variável, uma vez que você pode não lembrar que um mapa usa _foo para uma finalidade e outro usa-lo para um propósito completamente diferente.

Não é fácil de testar isso, tampouco. Suponha que você queira escrever um teste para o cenário "Quando eu criar um mapa 'monstros Spawn', o mapa deve gerar um novo monstro a cada cinco segundos." É fácil discutir como você pode testar isso: criar o mapa, define o seu tipo, iniciá-lo, esperar um pouco mais de cinco segundos, e verificar a contagem inimigo. No entanto, a nossa interface não tem qualquer propriedade "contagem inimigo". Poderíamos acrescentar, mas o que se este é o único mapa que tem contar um inimigo? Se somarmos a propriedade, vamos ter uma propriedade que é inválida em 2/3 dos casos. Também não é muito claro que estamos testando o "monstros desova" mapa sem ler o código do teste, já que todos os testes será testar a Mapclasse.

Você certamente poderia fazer Mapuma classe base abstrata, Start()MustOverride, e derivar um novo tipo para cada tipo de mapa. Agora, a responsabilidade de Load()está em outro lugar, porque um objeto não pode substituir-se com uma instância diferente. Você pode também fazer uma classe de fábrica para isso:

Class MapCreator
    Public Function GetMap(mapType) As Map
End Class

Agora nossa hierarquia Mapa pode ser algo como isto (apenas um mapa derivado foi definido para simplificar):

Public MustInherit Class Map
    Public MustOverride Sub Start()
End Class

Public Class RentalMap
    Inherits Map

    Public Overrides Sub Start()
End Class

Load()não é mais necessário por razões já discutidas. MapTypeé supérfluo em um mapa porque você pode verificar o tipo do objeto para ver o que é (a menos que você tem vários tipos de RentalMap, então torna-se útil novamente.) Start()é substituído em cada classe derivada, de modo que você moveu as responsabilidades do Estado gestão para aulas individuais. Vamos fazer outra verificação SRP:

Mapa da classe base 0 responsabilidades

Mapa classe derivada - Deve gerenciar o estado (1) - deve executar algum trabalho específico do tipo (1)

Total: 2 responsabilidades

Adicionando um novo mapa (Idem) 2 responsabilidades

Número total de responsabilidades por classe: 2

Custo da adição de uma nova classe mapa: 2

Isto é muito melhor. E o nosso cenário de teste? Nós estamos em melhor forma, mas ainda não muito bem. Nós podemos fugir com a colocação de um "número de inimigos" propriedade no nosso classe derivada porque cada classe é separado e que pode lançar para tipos de mapa específicas, se precisar de informações específicas. Ainda assim, o que se tem RentalMapSlowe RentalMapFast? Você tem que duplicar seus testes para cada uma dessas classes, já que cada um tem lógica diferente. Então, se você tem 4 testes e 12 mapas diferentes, você estará escrevendo e ligeiramente aprimorando 48 testes. Como nós consertamos isso?

O que fizemos quando fizemos as classes derivadas? Identificamos a parte da classe que estava mudando cada vez e empurrou-a para baixo em sub-classes. E se, em vez de subclasses, criamos um separado MapBehaviorclasse que podemos trocar e sair à vontade? Vamos ver o que isso pode parecer com um comportamento derivado:

Public Class Map
    Public ReadOnly Property Behavior As MapBehavior

    Public Sub SetBehavior(behavior)
    Public Sub Start()
End Class

Public MustInherit Class MapBehavior
    Public MustOverride Sub Start()
End Class

Public Class PlayerSpawnBehavior
    Public Property EnemiesPerSpawn As Integer
    Public Property MaximumNumberOfEnemies As Integer
    Public ReadOnly Property NumberOfEnemies As Integer

    Public Sub SpawnEnemy()
    Public Sub Start()
End Class

Agora, usando um mapa envolve dando-lhe uma específica MapBehaviore chamando Start(), que delega para o comportamento do Start(). Todas as informações estado está no objeto de comportamento, de modo que o mapa realmente não precisa saber nada sobre isso. Ainda assim, e se você quiser um tipo de mapa específico, parece inconveniente de ter de criar um comportamento, em seguida, criar um mapa, certo? Então você derivar algumas classes:

Public Class PlayerSpawnMap
    Public Sub New()
        MyBase.New(New PlayerSpawnBehavior())
    End Sub
End Class

É isso, uma linha de código para uma nova classe. Quer um mapa desova leitor de disco?

Public Class HardPlayerSpawnMap
    Public Sub New()
        ' Base constructor must be first line so call a function that creates the behavior
        MyBase.New(CreateBehavior()) 
    End Sub

    Private Function CreateBehavior() As MapBehavior
        Dim myBehavior As New PlayerSpawnBehavior()
        myBehavior.EnemiesPerSpawn = 10
        myBehavior.MaximumNumberOfEnemies = 300
    End Function
End Class

Então, como isso é diferente de ter propriedades em classes derivadas? Do ponto de vista comportamental não é muito diferente. Do ponto de vista de teste, este é um grande avanço. PlayerSpawnBehaviortem seu próprio conjunto de testes. Mas, uma vez HardPlayerSpawnMape PlayerSpawnMaptanto para uso PlayerSpawnBehavior, em seguida, se eu testei PlayerSpawnBehavioreu não tenho que escrever todos os testes relacionados com o comportamento de um mapa que usa o comportamento! Vamos comparar cenários de teste.

No "uma classe com um tipo de parâmetro" caso, se existem 3 níveis de dificuldade para 3 comportamentos, e cada comportamento tem 10 testes, você estará escrevendo 90 testes (não incluindo testes para ver se vai de cada comportamento de outros trabalhos .) no cenário de "classes derivadas", você terá 9 classes que precisam de 10 testes cada: 90 testes. No cenário de "comportamento da classe", você vai escrever 10 testes para cada comportamento: 30 testes.

Aqui está a contagem responsabilidade: mapa tem uma responsabilidade: manter o controle de um comportamento. Comportamento tem 2 responsabilidades: manter o estado e executar ações.

Número total de responsabilidades por classe: 3

Custo da adição de uma nova classe mapa: 0 (reutilizar um comportamento) ou 2 (novo comportamento)

Então, minha opinião é que o cenário de "classe de comportamento" não é mais difícil de escrever do que o cenário de "classes derivadas", mas pode reduzir significativamente a carga de testes. Eu li sobre técnicas de como este e dispensou-os como "demais" por anos e só recentemente percebeu o seu valor. É por isso que eu escrevi cerca de 10.000 caracteres para explicar e justificar.

Respondeu 31/08/2009 em 03:04
fonte usuário

votos
3

Você deve subclasse onde quer que o seu tipo de criança é algum tipo de especialização do tipo pai. Em outras palavras, você deve evitar a herança se você só precisa funcionalidade. Como a substituição de Liskov afirma: "se S é um subtipo de T, então objectos de tipo T em um programa pode ser substituído com objectos de tipo S, sem alterar qualquer uma das propriedades desejáveis de que programa"

Respondeu 27/08/2009 em 02:37
fonte usuário

votos
1

Eu não fazer programa em C #, mas em Ruby on Rails, Xcode, e Mootools (javascript framework OOP) a mesma pergunta poderia ser feita.

Eu não gosto de ter um método que nunca serão usados ​​quando uma certa e permanente, a propriedade é a pessoa errada. Como se é um bug VW, determinadas artes nunca vai ser transformado. Isso é bobagem.

Se eu encontrar alguns métodos como a que tento tudo abstrato que pode ser compartilhado entre todos os meus diferentes "carros" em uma classe pai, com métodos e propriedades para ser usado por todos os tipos de carro e, em seguida, definir as subclasses com a sua métodos específicos.

Respondeu 27/08/2009 em 03:28
fonte usuário

votos
1

No seu caso eu iria com uma abordagem híbrida (isto pode ser chamado de composição, eu não sei), onde a variável modo de mapa é realmente um objeto separado que armazena dados / comportamento todos relacionados com o modo do mapa. Desta forma, você pode ter tantos modos como você gosta sem realmente fazer muito para a classe Map.

gutofb7 acertou em cheio na cabeça de quando você deseja subclasse algo. Dando um exemplo mais concreto: Na sua classe de carro, será que importa em qualquer lugar em seu programa que tipo de carro você estava lidando com isso? agora se você subclasse Mapa, a quantidade de código que você tem que escrever que lida com subclasses específicas?

Respondeu 27/08/2009 em 02:41
fonte usuário

votos
1

No problema específico que você falou com os mapas e desova, penso que este é um caso onde você quer favorecer composição sobre herança . Quando você pensa sobre isso, eles não são exatamente três diferentes tipos de mapa. Em vez disso, eles são o mesmo mapa com três estratégias diferentes para a desova. Então, se possível, você deve fazer a função de desova uma classe separada e tem uma instância de uma classe de desova como um membro do seu mapa. Se todas as outras diferenças em "modos" para seus mapas são de natureza semelhante, você pode não ter a subclasse o mapa em tudo, embora subclassificação os diferentes componentes (ou seja, ter uma classe base spawn_strategy e subclasse dos três tipos de desova de que) , ou pelo menos dar-lhes uma interface comum, provavelmente será necessário.

Dado o seu comentário que cada tipo de mapa é feito para ser conceitualmente diferentes, então eu sugiro que subclasses, como que parece cumprir princípio da substituição de Liskov. No entanto, isso não quer dizer que você deve desistir de composição inteiramente. Para os imóveis que todo o tipo de mapa tem, mas pode ter comportamento diferente / implementação, você deve considerar fazer sua classe base tê-los como componentes. Dessa forma, você ainda pode misturar e funcionalidade jogo se você precisa, ao usar a herança para manter uma separação de interesses.

Respondeu 27/08/2009 em 02:29
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more