Como uma classe `Employee` deve ser projetada?

11

Estou tentando criar um programa para gerenciar funcionários. No entanto, não consigo descobrir como projetar a Employeeclasse. Meu objetivo é ser capaz de criar e manipular dados de funcionários no banco de dados usando um Employeeobjeto.

A implementação básica que pensei foi simples:

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

Usando esta implementação, encontrei vários problemas.

  1. O IDde um funcionário é fornecido pelo banco de dados; portanto, se eu usar o objeto para descrever um novo funcionário, ainda não haverá IDarmazenamento, enquanto um objeto que representa um funcionário existente terá um ID. Portanto, tenho uma propriedade que às vezes descreve o objeto e outras que não (O que poderia indicar que violamos o SRP ? Como usamos a mesma classe para representar funcionários novos e existentes ...).
  2. O Createmétodo deve criar um funcionário no banco de dados, enquanto o Updatee Deletedeve atuar em um funcionário existente (Novamente, SRP ...).
  3. Quais parâmetros o método 'Criar' deve ter? Dezenas de parâmetros para todos os dados do funcionário ou talvez um Employeeobjeto?
  4. A classe deve ser imutável?
  5. Como vai Updatefuncionar? Ele pega as propriedades e atualiza o banco de dados? Ou talvez seja necessário dois objetos - um "antigo" e um "novo" e atualizar o banco de dados com as diferenças entre eles? (Eu acho que a resposta tem a ver com a resposta sobre a mutabilidade da classe).
  6. Qual seria a responsabilidade do construtor? Quais seriam os parâmetros necessários? Buscaria dados de funcionários do banco de dados usando um idparâmetro e eles preencheriam as propriedades?

Então, como você pode ver, estou um pouco confuso na cabeça e estou muito confuso. Você poderia, por favor, me ajudar a entender como deve ser essa classe?

Observe que eu não quero opiniões, apenas para entender como uma classe geralmente usada é geralmente projetada.

Sipo
fonte
3
Sua violação atual do SRP é que você tem uma classe representando uma entidade e sendo responsável pela lógica CRUD. Se você separá-lo, que as operações CRUD e a estrutura da entidade terão classes diferentes, então 1. e 2. não quebram o SRP. 3. deve pegar um Employeeobjeto para fornecer abstração, as perguntas 4. e 5. geralmente não podem ser respondidas, dependem de suas necessidades e, se você separar a estrutura e as operações CRUD em duas classes, é bastante claro que o construtor do Employeenão pode buscar dados do db anymore, para que responda 6. #
Andy
@DavidPacker - Obrigado. Você poderia colocar isso em uma resposta?
Sipo 03/07
5
Repito, não faça com que seu ctor chegue ao banco de dados. Fazer isso acopla firmemente o código ao banco de dados e torna as coisas terrivelmente difíceis de testar (até o teste manual fica mais difícil). Olhe para o padrão do repositório. Pense nisso por um segundo, você é Updateum funcionário ou atualiza um registro de funcionário? Você Employee.Delete()faz ou faz Boss.Fire(employee)?
precisa
1
Além do que já foi mencionado, faz sentido para você que você precisa de um funcionário para criar um funcionário? No registro ativo, pode fazer mais sentido recrutar um Funcionário e chamar Save nesse objeto. Mesmo assim, agora você tem uma classe responsável pela lógica de negócios e pela persistência de dados.
Sr. Cochese

Respostas:

10

Esta é uma transcrição mais bem-formada do meu comentário inicial na sua pergunta. As respostas às perguntas abordadas pelo OP podem ser encontradas na parte inferior desta resposta. Além disso, verifique a nota importante localizada no mesmo local.


O que você está descrevendo atualmente, Sipo, é um padrão de design chamado Registro ativo . Como em tudo, mesmo este encontrou seu lugar entre os programadores, mas foi descartado em favor dos padrões do repositório e do mapeador de dados por uma simples razão, escalabilidade.

Em resumo, um registro ativo é um objeto que:

  • representa um objeto em seu domínio (inclui regras de negócios, sabe como lidar com determinadas operações no objeto, como se você pode ou não alterar um nome de usuário e assim por diante),
  • sabe como recuperar, atualizar, salvar e excluir a entidade.

Você aborda vários problemas com o seu design atual e o principal problema do seu design é abordado no último, sexto, ponto (por último, mas não menos importante, eu acho). Quando você tem uma classe para a qual está projetando um construtor e nem sabe o que o construtor deve fazer, a classe provavelmente está fazendo algo errado. Isso aconteceu no seu caso.

Mas fixar o design é realmente bastante simples, dividindo a representação da entidade e a lógica CRUD em duas (ou mais) classes.

É assim que seu design se parece agora:

  • Employee- contém informações sobre a estrutura do funcionário (seus atributos) e métodos sobre como modificar a entidade (se você decidir seguir o caminho mutável), contém lógica CRUD para a Employeeentidade, pode retornar uma lista de Employeeobjetos, aceita um Employeeobjeto quando deseja atualizar um funcionário, pode retornar um único Employeeatravés de um método comogetSingleById(id : string) : Employee

Uau, a classe parece enorme.

Esta será a solução proposta:

  • Employee - contém informações sobre a estrutura do funcionário (seus atributos) e métodos para modificar a entidade (se você decidir seguir o caminho mutável)
  • EmployeeRepository- contém lógica CRUD para a Employeeentidade, pode retornar uma lista de Employeeobjetos, aceita um Employeeobjeto quando você deseja atualizar um funcionário, pode retornar um único Employeepor meio de um método comogetSingleById(id : string) : Employee

Você já ouviu falar de separação de preocupações ? Não, você vai agora. É a versão menos rigorosa do Princípio da Responsabilidade Única, que diz que uma classe deve realmente ter apenas uma responsabilidade, ou como o tio Bob diz:

Um módulo deve ter um e apenas um motivo para mudar.

É bastante claro que, se eu era capaz de dividir claramente sua classe inicial em duas que ainda possuem uma interface bem arredondada, a classe inicial provavelmente estava fazendo muito, e foi.

O que é ótimo no padrão de repositório, ele não apenas atua como uma abstração para fornecer uma camada intermediária entre o banco de dados (que pode ser qualquer coisa, arquivo, noSQL, SQL, orientado a objetos), mas nem precisa ser concreto. classe. Em muitas linguagens OO, você pode definir a interface como real interface(ou uma classe com um método virtual puro, se você estiver em C ++) e, em seguida, terá várias implementações.

Isso elimina completamente a decisão de se um repositório é uma implementação real. Você simplesmente depende da interface, dependendo de uma estrutura com a interfacepalavra - chave. E repositório é exatamente isso, é um termo sofisticado para abstração da camada de dados, mapeando dados para seu domínio e vice-versa.

Outra grande coisa sobre separá-lo em (pelo menos) duas classes é que agora a Employeeclasse pode gerenciar claramente seus próprios dados e fazê-lo muito bem, porque não precisa cuidar de outras coisas difíceis.

Pergunta 6: Então, o que o construtor deve fazer na Employeeclasse recém-criada ? É simples Ele deve aceitar os argumentos, verificar se eles são válidos (como uma idade provavelmente não deve ser negativa ou o nome não deve estar vazio), gerar um erro quando os dados forem inválidos e se a validação aprovada atribuir os argumentos a variáveis ​​privadas da entidade. Agora ele não pode se comunicar com o banco de dados, porque simplesmente não tem idéia de como fazê-lo.


Pergunta 4: Não é possível responder a todos, de modo geral, porque a resposta depende muito do que exatamente você precisa.


Pergunta 5: Agora que você separou a classe inchado em dois, você pode ter vários métodos de atualização diretamente na Employeeclasse, como changeUsername, markAsDeceased, que irá manipular os dados da Employeeclasse apenas na RAM e, em seguida, você poderia introduzir um método como registerDirtydo Padrão de Unidade de Trabalho para a classe do repositório, através da qual você deixaria o repositório saber que este objeto alterou propriedades e precisará ser atualizado depois que você chamar o commitmétodo.

Obviamente, para uma atualização, um objeto precisa ter um ID e, portanto, já deve ser salvo, e é responsabilidade do repositório detectar isso e gerar um erro quando os critérios não forem atendidos.


Pergunta 3: Se você optar por seguir o padrão da Unidade de Trabalho, o createmétodo será agora registerNew. Se não, provavelmente eu chamaria isso save. O objetivo de um repositório é fornecer uma abstração entre o domínio e a camada de dados; por isso, recomendo que este método (seja registerNewou save) aceite o Employeeobjeto e que cabe às classes que implementam a interface do repositório, que atribui eles decidem se retirar da entidade. Passar um objeto inteiro é melhor, assim você não precisa ter muitos parâmetros opcionais.


Pergunta 2: Agora os dois métodos farão parte da interface do repositório e não violam o princípio da responsabilidade única. A responsabilidade do repositório é fornecer operações CRUD para os Employeeobjetos, é isso que ele faz (além de Ler e Excluir, o CRUD se traduz em Criar e Atualizar). Obviamente, você pode dividir o repositório ainda mais tendo um EmployeeUpdateRepositorye assim por diante, mas isso raramente é necessário e uma única implementação geralmente pode conter todas as operações CRUD.


Pergunta 1: Você acabou com uma Employeeclasse simples que agora (entre outros atributos) terá id. Se o ID está preenchido ou vazio (ou null) depende se o objeto já foi salvo. No entanto, um ID ainda é um atributo que a entidade possui e a responsabilidade da Employeeentidade é cuidar de seus atributos, portanto, cuidar de seu ID.

Se uma entidade possui ou não um ID, normalmente não importa até que você tente fazer alguma lógica de persistência nela. Conforme mencionado na resposta à pergunta 5, é responsabilidade do repositório detectar que você não está tentando salvar uma entidade que já foi salva ou tentando atualizar uma entidade sem um ID.


Nota importante

Esteja ciente de que, embora a separação de preocupações seja grande, na verdade, projetar uma camada de repositório funcional é um trabalho bastante tedioso e, na minha experiência, é um pouco mais difícil de acertar do que a abordagem de registro ativo. Mas você terá um design muito mais flexível e escalável, o que pode ser uma coisa boa.

Andy
fonte
Hmm mesma que a minha resposta, mas não como 'edgey' coloca em tons
Ewan
2
@ Ewan Não diminui a votação da sua resposta, mas posso ver por que alguns podem ter. Ele não responde diretamente a algumas perguntas do OP e algumas de suas sugestões parecem infundadas.
Andy
1
Resposta agradável e abrangente. Acerta a unha na cabeça com a separação de preocupação. E gosto do aviso que indica a escolha importante a ser feita entre um design complexo perfeito e um bom compromisso.
Christophe
É verdade, sua resposta é superior
Ewan
quando criar um novo objeto de funcionário, não haverá valor para o ID. O campo id pode sair com valor nulo, mas fará com que o objeto empregado esteja em estado inválido ????
precisa
2

Primeiro, crie uma estrutura de funcionário contendo as propriedades do funcionário conceitual.

Em seguida, crie um banco de dados com a estrutura da tabela correspondente, por exemplo, mssql

Em seguida, crie um repositório de funcionários Para esse banco de dados EmployeeRepoMsSql com as várias operações CRUD necessárias.

Em seguida, crie uma interface IEmployeeRepo expondo as operações CRUD

Em seguida, expanda sua estrutura de funcionários para uma classe com um parâmetro de construção de IEmployeeRepo. Adicione os vários métodos Save / Delete etc necessários e use o EmployeeRepo injetado para implementá-los.

Quando se trata de Id, sugiro que você use um GUID que possa ser gerado via código no construtor.

Para trabalhar com objetos existentes, seu código pode recuperá-los do banco de dados por meio do repositório antes de chamar o método de atualização.

Como alternativa, você pode optar pelo modelo anêmico de objeto de domínio anêmico (mas na minha opinião superior), no qual você não adiciona os métodos CRUD ao seu objeto, e simplesmente passa o objeto ao repositório para ser atualizado / salvo / excluído

A imutabilidade é uma opção de design que depende dos seus padrões e estilo de codificação. Se você está indo tudo funcional, tente ser imutável também. Mas se você não tiver certeza de que um objeto mutável é provavelmente mais fácil de implementar.

Em vez de Create (), eu iria com Save (). Criar funciona com o conceito de imutabilidade, mas eu sempre acho útil poder construir um objeto que ainda não esteja 'salvo'. salvando no banco de dados.

***** código de exemplo

public class Employee
{
    public string Id { get; set; }

    public string Name { get; set; }

    private IEmployeeRepo repo;

    //with the OOP approach you want the save method to be on the Employee Object
    //so you inject the IEmployeeRepo in the Employee constructor
    public Employee(IEmployeeRepo repo)
    {
        this.repo = repo;
        this.Id = Guid.NewGuid().ToString();
    }

    public bool Save()
    {
        return repo.Save(this);
    }
}

public interface IEmployeeRepo
{
    bool Save(Employee employee);

    Employee Get(string employeeId);
}

public class EmployeeRepoSql : IEmployeeRepo
{
    public Employee Get(string employeeId)
    {
        var sql = "Select * from Employee where Id=@Id";
        //more db code goes here
        Employee employee = new Employee(this);
        //populate object from datareader
        employee.Id = datareader["Id"].ToString();

    }

    public bool Save(Employee employee)
    {
        var sql = "Insert into Employee (....";
        //db logic
    }
}

public class MyADMProgram
{
    public void Main(string id)
    {
        //with ADM don't inject the repo into employee, just use it in your program
        IEmployeeRepo repo = new EmployeeRepoSql();
        var emp = repo.Get(id);

        //do business logic
        emp.Name = TextBoxNewName.Text;

        //save to DB
        repo.Save(emp);

    }
}
Ewan
fonte
1
O Modelo de Domínio Anêmico tem muito pouco a ver com a lógica CRUD. É um modelo que, apesar de pertencer à camada de domínio, não possui funcionalidade e toda a funcionalidade é atendida por meio de serviços, para os quais esse modelo de domínio está sendo passado como parâmetro.
Andy
Exatamente, nesse caso, o repo é o serviço e as funções são as operações CRUD.
Ewan
@DavidPacker, você está dizendo que o Modelo de Domínio Anêmico é uma coisa boa?
Candied_orange 03/07
1
@CandiedOrange Não expressei minha opinião no comentário, mas não, se você decidir mergulhar seu aplicativo em camadas nas quais uma camada é responsável apenas pela lógica de negócios, estou com o Sr. Fowler que um modelo de domínio anêmico é de fato um antipadrão. Por que eu deveria precisar de um UserUpdateserviço com um changeUsername(User user, string newUsername)método, quando também posso adicionar o changeUsernamemétodo Userdiretamente à classe ? Criar um serviço para isso não faz sentido.
Andy
1
Acho que, neste caso, injetar o repositório apenas para colocar a lógica CRUD no modelo não é o ideal.
Ewan
1

Revisão do seu design

EmployeeNa realidade, você é um tipo de proxy para um objeto gerenciado persistentemente no banco de dados.

Portanto, sugiro pensar no ID como se fosse uma referência ao seu objeto de banco de dados. Com essa lógica em mente, você pode continuar seu design como faria com objetos que não são do banco de dados, o ID permitindo implementar a lógica de composição tradicional:

  • Se o ID estiver definido, você terá um objeto de banco de dados correspondente.
  • Se o ID não estiver definido, não há um objeto de banco de dados correspondente: Employeeainda pode ser criado ou poderia ter sido excluído.
  • Você precisa de algum mecanismo para iniciar o relacionamento para funcionários existentes e registros existentes do banco de dados que ainda não foram carregados na memória.

Você também precisa gerenciar um status para o objeto. Por exemplo:

  • quando um funcionário ainda não estiver vinculado a um objeto de banco de dados por meio de criação ou recuperação de dados, você não poderá executar atualizações ou exclusões
  • são os dados do empregado no objeto em sincronia com o banco de dados ou há alterações feitas?

Com isso em mente, poderíamos optar por:

class Employee
{
    ...
    Employee () {}       // Initialize an empty Employee
    Load(IDType ID) {}   // Load employee with known ID from the database
    bool Create() {}     // Create an new employee an set its ID 
    bool Update() {}     // Update the employee (can ID be changed?)
    bool Delete() {}     // Delete the employee (and reset ID because there's no corresponding ID. 
    bool isClean () {}   // true if ID empty or if all properties match database
}

Para poder gerenciar seu status do objeto de maneira confiável, você deve garantir um melhor encapsulamento, tornando as propriedades privadas e conceder acesso apenas por meio de getters e setters que os setters atualizam o status.

Suas perguntas

  1. Eu acho que a propriedade ID não viola o SRP. Sua única responsabilidade é se referir a um objeto de banco de dados.

  2. Seu funcionário como um todo não é compatível com o SRP, porque é responsável pelo vínculo com o banco de dados, mas também pela realização de alterações temporárias e por todas as transações que acontecem com esse objeto.

    Outro design poderia ser manter os campos alteráveis ​​em outro objeto que seria carregado apenas quando os campos precisassem ser acessados.

    Você pode implementar as transações do banco de dados no Employee usando o padrão de comando . Esse tipo de design também facilitaria a dissociação entre seus objetos de negócios (Employee) e seu sistema de banco de dados subjacente, isolando expressões e APIs específicas do banco de dados.

  3. Eu não adicionaria uma dúzia de parâmetros Create(), porque os objetos de negócios poderiam evoluir e dificultar a manutenção de tudo isso. E o código se tornaria ilegível. Você tem duas opções aqui: passar um conjunto minimalista de parâmetros (não mais que 4) que são absolutamente necessários para criar um funcionário no banco de dados e executar as alterações restantes via atualização, ou você passa um objeto. By the way, em seu projeto eu entendo que você já escolheu: my_employee.Create().

  4. A classe deve ser imutável? Veja a discussão acima: em seu projeto original no. Eu optaria por um ID imutável, mas não por um funcionário imutável. Um funcionário evolui na vida real (novo cargo, novo endereço, nova situação matrimonial, até novos nomes ...). Eu acho que será mais fácil e mais natural trabalhar com essa realidade em mente, pelo menos na camada da lógica de negócios.

  5. Se você considerar usar comandos para atualização e objetos distintos para (GUI?) Para reter as alterações desejadas, poderá optar pela abordagem antiga / nova. Em todos os outros casos, eu optaria por atualizar um objeto mutável. Atenção: a atualização pode acionar o código do banco de dados para garantir que, após uma atualização, o objeto ainda esteja realmente sincronizado com o banco de dados.

  6. Eu acho que buscar um funcionário da DB no construtor não é uma boa idéia, porque a busca pode dar errado e, em muitos idiomas, é difícil lidar com falhas na construção. O construtor deve inicializar o objeto (especialmente o ID) e seu status.

Christophe
fonte