Expondo variáveis de membro vs Adicionando funções de membro para modificá-las

5

Digamos que temos uma classe Car que contém objetos do tipo Wheel

class Wheel {
public:

    void SetFriction(double f) {
        friction = f;
    }

private:
    double friction;
};

class Car {

private:
    std::array<Wheel, 4> _wheels;
}

Agora, quero oferecer ao proprietário do carro o acesso ao atrito das rodas. Existem basicamente duas maneiras de pensar nisso:

  • Conceda acesso às rodas:

    class Car {
    
        Wheel& GetWheel(int id) {
            return _wheels[id];
        }
    
    private:
        std::array<Wheel, 4> _wheels;
    }
    
    Car beetle;
    beetle.GetWheel(1).SetFriction(0.4);
    
  • Conceda acesso ao atrito diretamente:

    class Car {
    
        void SetWheelFriction(int id, double friction) {
            _wheels[id].SetFriction(friction);
        }
    
    private:
        std::array<Wheel, 4> _wheels;
    }
    
    Car beetle;
    beetle.SetWheelFriction(1, 0.4);
    

O problema com a primeira versão é que ela dá acesso a um membro privado, potencialmente quebrando o encapsulamento. Talvez uma função seja adicionada ao Google Wheel (por exemplo, "SetManufacturer"), que não deve ser exposto a um proprietário de carro.

Por outro lado, a segunda versão leva a muitas funções sendo definidas no carro que não são de sua responsabilidade direta, violando assim o princípio da responsabilidade única.

class Car {

    void SetWheelFriction(int id, double friction) {
        _wheels[id].SetFriction(friction);
    }

    double GetEngineWeight() {
        return _engine.GetWeight();
    }

    double SetWindowsColor(Color& c) {
        for(auto& w : _windows){
            w.SetColor(c);
        }
    }

        // And so on...

Eu me inclino strongmente para a primeira opção, mas gostaria de ouvir a opinião de outra pessoa.

Talvez um pouco relacionado: link

    
por Fabio 22.12.2017 / 18:25
fonte

6 respostas

4

Você não menciona qual é a principal responsabilidade da classe Car .

Em um aplicativo real, sua classe Car não representa apenas um carro físico. Isso representaria em um contexto específico. E saber o contexto é importante, pois é o que moldará a API pública da sua classe.

Exemplo de contexto 1 - Simulações de corrida

Digamos, por exemplo, que esse contexto esteja executando simulações para otimizar o desempenho de um carro de corrida. A API pública da sua classe Car pode expor dados e métodos para ajustar as características de desempenho do seu carro. Nesse contexto, seria bastante razoável que uma classe Simulation ou PerformanceEvolver usasse a API Car para ajustar diretamente a fricção de roda (por exemplo, car.setWheelFriction(WheelsEnum.FrontLeft, coefficient: 0.8) .

Vamos analisar um contexto diferente agora.

Exemplo de Contexto 2 - BOM

Vamos imaginar que sua classe Car seja usada no contexto de uma lista de materiais. A principal responsabilidade do seu Car aqui é descrever os componentes necessários para criá-lo. Ele certamente terá uma referência às quatro rodas, mas isso é basicamente a extensão dos dados e comportamento centrados nas rodas incluídos na API Car public.

Então, se você estivesse interessado nos componentes necessários para construir a roda Front-Left do carro, você não esperaria fazer algo como car.getFrontLeftWheelBillOfMaterials() , porque isso seria uma preocupação roda do que uma preocupação carro . Em vez disso, você faria algo como car.Wheels[WheelsEnum.FrontLeft].GetBillOfMaterials() .

O Takeaway:

Então, tudo isso para dizer isso:

  • Não é possível dar conselhos sobre como modelar uma API em um exemplo inventado .
  • A forma de seus modelos, suas APIs e sua semântica (espero que eu esteja usando esse termo corretamente aqui) dependerá muito do contexto específico.

Se você está procurando uma regra geral rígida e rápida, acho que você está sem sorte.

Se você tiver um exemplo específico com o qual gostaria de receber ajuda, sugiro que adicione essas informações à sua pergunta.

Merry natal!

    
por 25.12.2017 / 18:21
fonte
3

Princípio de Responsabilidade Única (SRP)

O SRP não é sobre a funcionalidade oferecida por uma classe, mas sobre os motivos para mudar :

  • A opção 1 é compatível com o SRP, pois uma alteração no Wheel não exigiria a alteração do Car .
  • A opção 2 pode, ao contrário, propagar as necessidades de alteração: qualquer alteração na interface Wheel decidida por algum especialista em movimentação pode exigir também uma alteração na interface Car .

Problema com a opção 1

O principal problema da opção 1 é que ela retorna uma referência a um objeto. Essa referência pode ser salva por algum usuário do Car (por exemplo, tendo seu endereço em um ponteiro) e pode ser usada posteriormente, mesmo que o Wheel tenha sido substituído nesse meio tempo.

Uma abordagem mais segura, porém mais pesada, seria retornar um objeto proxy que exporia a interface Wheel , mas garantiria que a roda seja acessada por meio do Car .

Problema oculto com todas as abordagens

Um problema com a sua abordagem é que, apesar de sua tentativa de encapsular as rodas, você assume que um carro tem 4 rodas, numeradas de 0 a 3.

No entanto, pode-se imaginar alguns pequenos carros futuristas com 3 rodas. E hoje em dia já existem algumas grandes limusines com 6 rodas.

Portanto, você deve pelo menos oferecer uma função que forneça o número de rodas. Ou melhor, ofereça um iterador que permita iterar entre as rodas de acordo com uma ordem predefinida.

    
por 23.12.2017 / 23:38
fonte
0

Qual é o principal objetivo do princípio de encapsulamento? Como será o código quando todas as propriedades e métodos estarão disponíveis para todas as outras partes do código? Espero que as propriedades e os métodos da classe particular sejam usados por muitas outras partes do sistema, vinculando-os aos elementos dessa classe. O que há de errado com essa ligação? Está errado porque alguns desses elementos podem ser alterados durante os refatores ou na manutenção dessa classe em particular. E quando você muda esse elemento, você tem que mudar todo o código "cliente" também ("código do cliente", quero dizer outro código que usa esses elementos específicos da classe). Será uma dor de cabeça para fazer isso.

Então, quais partes da classe devem ser escondidas? Pelo menos aqueles que podem ser alterados no futuro sem mudar o comportamento da classe. "Devo usar Set aqui ou List seria melhor? Ambas as abordagens funcionam", "Meu algoritmo usará este tipo / variável ou a outra?". Essas perguntas devem avisá-lo de que essa parte da aula é apenas uma implementação interna, exatamente como você escolhe para obter um comportamento. Esconda essa parte dos outros, porque ela pode mudar e você não quer refatorar 50% da aplicação porque o tipo de uma variável foi alterado.

Acabei de perceber que seguir as ideias de Design dirigido por domínio ajuda nas decisões o que deve e o que não deve ser exposto. Se a classe for uma Raiz Agregada, você poderá expô-la. Se é apenas uma Entidade dentro do Agregado - melhor não.

Respondendo a sua pergunta: qual é a probabilidade de o seu carro não ter rodas no futuro? Se não estiver alto, você pode expor as Rodas, porque provavelmente elas não serão alteradas para outra coisa. Mas expor array que detém rodas não poderia ser uma boa decisão, porque depois de alguma remodelação talvez você perceba a falta de idéia de chassi que recolhe rodas e tem algum comportamento necessário (eu não reivindico que você vai - é apenas um exemplo). Falando em termos de DDD - Wheel parece ser uma Raiz Agregada válida no estado atual do modelo.

BTW. O atrito não é uma propriedade de uma roda. Depende da própria roda e da superfície em que a roda está rolando. ;)

    
por 22.12.2017 / 22:16
fonte
0

On the other side, the second version leads to a lot of functions being defined in Car that are not of its direct responsibility, thus violating the single responsibility principle.

Eu não concordo com a premissa que você mencionou acima. A única responsabilidade do carro é levar o motorista do ponto A ao ponto B. Essa responsabilidade encapsula muitos detalhes menores que o usuário do carro não precisa saber, como quanto combustível enviar para o motor e quando . (O usuário solicita aceleração, mas o carro decide como produzi-lo.) As rodas são definitivamente parte da única responsabilidade de levar a pessoa ao seu destino. Por essa razão, prefiro mais seu segundo exemplo, embora concorde que os usuários não precisem definir o atrito de uma roda. Seja o que for que isso possa fazer, eu daria um assessor para ele, mas se ele alterou o atrito da roda ou fez outra coisa para realizar a tarefa, cabe à classe. E escrever a interface dessa maneira permite que você mude muito espaço para mudá-la no futuro, caso surja um método melhor.

    
por 23.12.2017 / 23:05
fonte
0

Um motivo para preferir a segunda abordagem é conformar-se à Lei de Deméter. Se você escolher a primeira abordagem, poderá acabar com essas chamadas longas, o que aumenta o acoplamento entre as classes. Por exemplo, seu código pode parecer com region.getDealership().getBestSellingCar().getWheel().setFriction(0.25) , enquanto que se estiver de acordo com a Lei de Demeter, será algo como region.setWheelFrictionOnBestSellingCar(0.25)

No primeiro caso, a classe precisa saber sobre a classe Concessionária, a classe Car e a classe Wheel, portanto, uma alteração em qualquer uma delas requer uma alteração no código de chamada. No último caso, o código só precisa saber sobre a classe Dealership. Há também um argumento para brevidade ou legibilidade entre os dois.

    
por 27.12.2017 / 14:53
fonte
-1

A regra geral é expor os objetos do componente e permitir que o usuário defina itens como friction e color diretamente. No entanto, todas as regras gerais são feitas para serem quebradas. Há casos em que você pode querer ter funções como SetWheelFriction . O caso principal é quando não se espera que um usuário esteja ciente dos detalhes da classe Wheel . Às vezes é um detalhe de implementação. Outras vezes, a classe do componente deve ser um recurso avançado que você não usa porque os novos desenvolvedores que usam seu Car não precisam saber sobre Wheels até muito mais tarde.

Pode-se optar por expor apenas alguns recursos na API principal. Coisas como checkTirePressure e checkOilLevel podem aparecer no Car , enquanto a API completa para alterar o óleo pode exigir o acesso às classes OilPan e OilFilter .

Essas funções são geralmente chamadas de "açúcar sintático". Eles podem ser muito úteis de uma perspectiva de conveniência, mas como todos os açúcares, use-os com moderação. Eles podem apodrecer seus dentes!

    
por 24.12.2017 / 17:48
fonte