Como usar o OO Design para refatorar uma biblioteca com funções específicas do produto

5

Eu tenho uma classe que funciona como uma biblioteca de funções para vários produtos. Para calcular seus dados, a função precisa estar ciente de todos os nomes de produtos. Dependendo de qual produto chama a função, ela retorna respostas específicas do produto.

A base de código é complicada o suficiente para que as classes sejam ligadas umas às outras de maneira circular e sejam confusas e não tenham uma estrutura clara e imediatamente reconhecível. Então, eu estou refatorando muitas vezes sem um propósito específico em mente, mas neste caso eu quero refatorar para

  • reduza a necessidade da biblioteca de confiar no conhecimento do produto
  • talvez eu possa limpar o código o suficiente para que eu possa usar o polimorfismo - instanciar um produto específico e, em seguida, ter funções específicas do produto de chamada codeblock de aparência genérica

Se eu preciso para fazer esse tipo de refatoração, eu não sei, afinal de contas "As coisas funcionam bem agora", exceto que elas são difíceis de serem seguidas e os erros são difíceis de rastrear. No entanto, meu objetivo e minha pergunta é "remover a confiança no conhecimento específico do produto da função calcVar ".

Pergunta : como removo o reconhecimento do produto da função calcVar() abaixo?

Exemplo de trabalho abaixo :

class Product{}

class Graph{}

class ProductA extends Product
{
    public $calc;

    public function __construct() {
        $this->calc = new CalcLibrary();
        $this->calc->spec->product = "A";
    }
}

class GraphB extends Graph
{
    public $calc;

    public function __construct()    {
        $this->calc = new CalcLibrary();
        $this->calc->spec->product = "B";
    }
}

class Specs
{
    public $var;
    public $product;

    //loads different specs based on product name
    function __construct() {
        if ($this->product == "A") $this->var = 3;
        if ($this->product == "B") $this->var = 4;
        $this->var = 5;
    }
}

class CalcLibrary
{
    public $spec;

    public function __construct() {
        $this->spec = new Specs();
    }

    //problem - library holding formulas for "Var" is product-aware
    //loads different formulas based on product name
    public function calcVar() {
        if ($this->spec->product == "A")
        {
            //somewhat different formulas for different products
            if ($this->spec->var <= 5) return 3 * $this->spec->var;
            else return 0;
        }        
        if ($this->spec->product == "B")
        {
            if ($this->spec->var == 0) return 16;
            else return 2 * $this->spec->var;
        }
        return $this->spec->var;
    }

    public function output() {
        echo $this->calcVar() . "\n";
    }
}

// tests should output 15, 10, and 5 
(new ProductA())->calc->output();
(new GraphB())->calc->output();
(new CalcLibrary())->output();

Meus pensamentos sobre a solução

Tentativa de solução 1 Na primeira solução foi fácil eu pensei - quando eu não estava ciente da família de Graph classes

  • mova o calcVar () do CalcLibary para a classe Product
  • livre-se de instruções if / then / else e deixe a versão genérica de calcVar () dentro da classe Product, mas mova as específicas para ProductA/B/C (classes que estendem Product)

e tudo estava bem até que descobri da maneira mais difícil que existe um grupo de classes chamado GraphA / B / C, que também usam o CalcLibrary.

Tentativa de recuperação da solução 1

A solução natural era copiar CalcVar () em Graph e especializar-se basicamente usando a mesma abordagem que com Product . Mas, então, o problema é que eu terei várias funções CalcVar() , genéricas no Produto e no Graph, e especializadas em cada uma de suas subclasses. Como em um problema de duplicação, quando eu não tenho duplicação de calcVar() . Eu não queria trocar um problema por outro.

Solução 2 Idéia

Outra maneira era tornar CalcLibrary a classe superior e ter as classes Graph e Product ampliando o CalcLibrary, ter o CalcLibrary contendo calcVar, como antes, mas agora o CalcLibrary só terá a versão genérica de calcVar e, em seguida, As classes individuais ProductA / B / C e GraphA / B / C possuem versões calcVar () específicas do produto. Esta parece ser uma boa solução, pois elimina a dependência do conhecimento do produto e cuida da duplicação. Mas não tenho 100% de certeza de que estender essa biblioteca é uma boa ideia. Não é realmente uma biblioteca, se eu não quero que seja. É uma classe personalizada, para que eu possa fazer o que eu preciso que seja.

Esboço da solução 3

Talvez tudo o que eu precise seja criar um nome melhor, então, em vez de CalcLibrary use ClassThatContainsVariousFunctionsUsefulToGraphAndProductClasses e, em seguida, use a solução # 2. Talvez mais tarde, através da refatoração, verei claramente o suficiente na minha base de código para ver um melhor ajuste de classes (encontre um design melhor), mas quais são seus pensamentos agora, na refatoração desse exemplo específico com minhas necessidades em mente? A "Solução 2" é o que devo procurar? Como não é tão fácil simplesmente mover o CalcLibrary para o topo, com todas as suas outras funções e vários outros efeitos colaterais, talvez eu possa criar uma nova classe com a função calcVar lá, e então fazer com que Product e Graph estendam essa nova classe , deixe o CalcLibrary sozinho e remova partes não utilizadas dele enquanto eu movo as coisas para aquela nova classe que eu criei ...

Considerações finais

Porque, do contrário, não vejo como Plot classes e Graph classes usarem funções dentro de CalcLibrary , sem mover essas funções para dentro das respectivas classes específicas do produto de alguma forma. (Quero dizer, além de CalcLibrary está sendo usado agora, estar em uma classe separada, mas com o preço de estar ciente do produto)

Além disso, ... meu esboço de terceira solução com a criação de Graph/Product extends NewCalcType parece (talvez certo) mas um tanto esotérico ... como em não vejo outro significado além de fazer com que a situação existente funcione com um design OO. Quero dizer, outra ideia é desmontar a classe completamente e fazer com que cada classe faça seus próprios cálculos calcários diretamente dentro das classes. Haverá muita duplicação, mas será mais "natural" .....

    
por Dennis 22.01.2015 / 17:59
fonte

1 resposta

1

Em algum lugar do seu código, você tem para armazenar os detalhes de cada produto. No seu exemplo, o local natural para isso parece ser a classe Specs , pois acho que é algo como a "configuração" da sua biblioteca. Eu estenderia essa classe, por exemplo, por um atributo "calcFactor", então sua função calcVar ficará assim:

public function calcVar()    {
    return $this->spec->calcFactor * $this->spec->var;        
}

O que você precisa é de uma fábrica para criar Spec objetos para cada produto, algo como

class SpecFactory
{
    public function CreateSpec(productName) {
        spec = new Specs();
        spec->product=productName;
        if (productName == "A") 
        {
            spec->calcFactor= 3;        
            // ... more parameters for product A here
        }
        else (productName == "B")
        {
            spec->calcFactor= 2;
            // ... more parameters for product B here
        }
        else
        {
           spec->calcFactor= 1; // default
        }
        return spec; 
    }

então seu código final ficará assim:

class ProductA extends Product
{
    public $calc;

    public function __construct() {
        spec = SpecFactory->CreateSpec("A");  // sets spec->calcFactor to 3 
        $this->calc = new CalcLibrary(spec); 
    }
}

ou

class GraphB extends Graph
{
    public $calc;

    public function __construct()    {
        spec = SpecFactory->CreateSpec("A");  // sets spec->calcFactor to 2
        $this->calc = new CalcLibrary(spec); 
    }
}

O código da sua fábrica será bastante simples, ele conterá a única e única "opção" entre diferentes produtos em todo o seu programa. Sua biblioteca então recebe a especificação "injetada" (injeção de dependência simples), e sempre que houver algo específico do produto, ele consultará a especificação para fornecer os parâmetros. Os parâmetros de especificação podem ser mais complicados, talvez um objeto polimórfico com uma derivação para cada produto, para suportar cenários mais complexos. Você provavelmente já sabe como isso é chamado: é o conhecido padrão de estratégia . Como uma forma "leve" do padrão de estratégia, você também pode tentar se "objetos de função" simples atenderem às suas necessidades (o AFAIK PHP suporta esse tipo de material funcional).

Na verdade, a aparência da sua classe de especificação pode ser diferente na realidade. Eu não sei como se parece hoje, talvez ele contenha muitas coisas independentes do produto, se esse for o caso, você pode separar a configuração específica do produto e a configuração independente do produto em classes "specs" separadas, mas espero que você obtenha o idéia geral: separe todas as coisas específicas do produto em uma única configuração que seja simples e fácil de manipular, e deixe a biblioteca usar essa configuração.

    
por 22.01.2015 / 19:01
fonte