Estou tentando criar um programa para gerenciar funcionários. No entanto, não consigo descobrir como projetar a Employee
classe. Meu objetivo é ser capaz de criar e manipular dados de funcionários no banco de dados usando um Employee
objeto.
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.
- O
ID
de um funcionário é fornecido pelo banco de dados; portanto, se eu usar o objeto para descrever um novo funcionário, ainda não haveráID
armazenamento, enquanto um objeto que representa um funcionário existente terá umID
. 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 ...). - O
Create
método deve criar um funcionário no banco de dados, enquanto oUpdate
eDelete
deve atuar em um funcionário existente (Novamente, SRP ...). - Quais parâmetros o método 'Criar' deve ter? Dezenas de parâmetros para todos os dados do funcionário ou talvez um
Employee
objeto? - A classe deve ser imutável?
- Como vai
Update
funcionar? 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). - Qual seria a responsabilidade do construtor? Quais seriam os parâmetros necessários? Buscaria dados de funcionários do banco de dados usando um
id
parâ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.
Employee
objeto 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 doEmployee
não pode buscar dados do db anymore, para que responda 6. #Update
um funcionário ou atualiza um registro de funcionário? VocêEmployee.Delete()
faz ou fazBoss.Fire(employee)
?Respostas:
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:
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 aEmployee
entidade, pode retornar uma lista deEmployee
objetos, aceita umEmployee
objeto quando deseja atualizar um funcionário, pode retornar um únicoEmployee
atravé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 aEmployee
entidade, pode retornar uma lista deEmployee
objetos, aceita umEmployee
objeto quando você deseja atualizar um funcionário, pode retornar um únicoEmployee
por 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:
É 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
interface
palavra - 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
Employee
classe 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
Employee
classe 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
Employee
classe, comochangeUsername
,markAsDeceased
, que irá manipular os dados daEmployee
classe apenas na RAM e, em seguida, você poderia introduzir um método comoregisterDirty
do 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 ocommit
mé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
create
método será agoraregisterNew
. Se não, provavelmente eu chamaria issosave
. 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 (sejaregisterNew
ousave
) aceite oEmployee
objeto 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
Employee
objetos, é 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 umEmployeeUpdateRepository
e 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
Employee
classe simples que agora (entre outros atributos) terá id. Se o ID está preenchido ou vazio (ounull
) depende se o objeto já foi salvo. No entanto, um ID ainda é um atributo que a entidade possui e a responsabilidade daEmployee
entidade é 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.
fonte
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
fonte
UserUpdate
serviço com umchangeUsername(User user, string newUsername)
método, quando também posso adicionar ochangeUsername
métodoUser
diretamente à classe ? Criar um serviço para isso não faz sentido.Revisão do seu design
Employee
Na 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:
Employee
ainda pode ser criado ou poderia ter sido excluído.Você também precisa gerenciar um status para o objeto. Por exemplo:
Com isso em mente, poderíamos optar por:
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
Eu acho que a propriedade ID não viola o SRP. Sua única responsabilidade é se referir a um objeto de banco de dados.
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.
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()
.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.
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.
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.
fonte