É possível refatorar a herança para composição quando métodos virtuais são chamados dentro da classe base?

5

Digamos que eu tenha uma classe chamada Country e duas subclasses chamadas AI e Player. A classe Country possui vários métodos virtuais para permitir um comportamento específico do jogador ou específico da IA.

Eu quero tornar possível ao jogador trocar de país durante um jogo. Para este fim, estou refatorando a relação de herança com a composição. Então poderei trocar a instância interna de um jogador por outro para qualquer momento.

Mas isso traz um pequeno problema: Quando um objeto externo tenta chamar um desses métodos virtuais em um Player, ele é delegado corretamente ao método Country depois que qualquer processamento apropriado tiver sido feito. Mas quando um método internal Country chama um dos métodos virtuais, o código na versão Player ou AI não tem mais chance de ser executado.

Então, agora fico com a atualização de cada um desses métodos para garantir que ambas as versões sejam chamadas, o que em si não é uma tarefa trivial (loops infinitos, alguém?).

Isso parece muito mais difícil do que deveria ser. Já fiz refatorações semelhantes antes e nunca me deparo com esse problema. Por quê? Estou perdendo alguma observação óbvia?

Editar para um exemplo concreto agora que tenho acesso ao código novamente:

class Country
{
public:
    virtual void AddMoney(float money)
    {
        _vMoney += money;
    }
    void TakeLoan()
    {
        float loan = ...;
        AddMoney(loan); // <-- does not route through AI or Player implementation
    }
}

class AI  // : public Country
{
private:
    Country *pCountry;

public:
    virtual void AddMoney(float x)
    {
        pCountry->AddMoney(x); // formerly Country::AddMoney(x)
        AllocateMoney(x);
    }
}
    
por mmyers 06.06.2011 / 19:41
fonte

2 respostas

5

Então, vamos fazer isso de concreto.

class Country {
  virtual void foo();
  void bar() {
    foo();
  }
}

Movendo-se para:

class Country {
  private thingWhichWasSubclass;
  void bar() {
    thingWhichWasSubclass.foo();
  }
}

Isso é certo? Que tal como um passo intermediário:

class Country {
  private thingWhichWasSubclass;
  void foo() {
    thingWhichWasSubclass.foo();
  }
  void bar() {
    foo();
  }
}

?

Isso faria isso? Então (é claro) inline Country.foo ().

    
por 06.06.2011 / 19:52
fonte
1

Eu tinha praticamente a mesma pergunta, mas desta vez em Java. Eu encontrei uma solução que usa classes aninhadas não estáticas em java - que em C ++ seria traduzido para algo como:

class AI  // : public Country
{
private:
    Country *pCountry; 

public:
    class Super : public Country {
        AI* outerThis;

    public:
        Super(AI* outer, ...)
        {
            outerThis = outer;
        }


        virtual void AddMoney(float x)
        {
            Country::AddMoney(x);
            outerThis->AllocateMoney(x);
        }
    } 

    // Just delegating calls to pCountry...
    virtual void AddMoney(float x)
    {
        pCountry->AddMoney(x);
    }
}

Então, é claro que você precisa instanciar o AI :: Super para obter um País associado a um objeto AI específico. Talvez instanciá-lo a partir do próprio objeto AI (o que elimina a necessidade de que o AI: Super seja público).

Basicamente, substituir a herança pela composição é fazer o que está sendo feito por trás da cena explicitamente. Por trás da cena está a composição, além de estender a tabela de despacho de funções virtuais. No caso de nenhuma função virtual, ela é direta, mas no caso de funções virtuais, você terá que levar isso em consideração. A maneira normal de fazer isso (nos bastidores) é ter a tabela virtual consistindo de entradas da classe base (com a função overriden substituída) seguida pelas entradas da classe derivada, mas também pode ser implementada tendo vtables separados para o objeto base (com as funções substituídas substituídas) e uma para o novo método virtual da classe derivada (como no meu exemplo). No último caso, você poderia ter o objeto base alocado separadamente e armazenar apenas o ponteiro para ele.

A única referência circular dela é o AI < - > AI :: Super que pode resultar em chamadas circulares. No entanto, as únicas chamadas de AI para AI :: Super devem ser para envolver os métodos da classe base e as chamadas de AI :: Super para AI são análogas a chamar métodos na classe Derived. Nenhuma recursão infinita deve surgir.

    
por 08.05.2015 / 11:09
fonte