Salvando um objeto por meio de um método próprio ou de outra classe?

38

Se eu quiser salvar e recuperar um objeto, devo criar outra classe para lidar com isso, ou seria melhor fazer isso na própria classe? Ou talvez misturando os dois?

O que é recomendado de acordo com o paradigma OOD?

Por exemplo

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

Versus. (Eu sei que o seguinte é recomendado, no entanto, isso me parece um pouco contra a coesão da Studentclasse)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

Que tal misturá-los?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }
Ahmad
fonte
3
Você deve fazer alguma pesquisa sobre o padrão Active Record, como esta questão ou um presente
Eric Rei
@gnat, eu pensei perguntando o que é melhor é opinião com base, em seguida, acrescentou "prós e contras" e não tem nenhum problema para removê-lo, mas na verdade eu quero dizer sobre o paradigma OOD que é recomendado
Ahmad
3
Não há resposta correta. Depende de suas necessidades, preferências, como sua classe será usada e onde você vê o seu projeto. A única coisa correta de OO é que você pode salvar e recuperar como objetos.
Dunk

Respostas:

34

Princípio da responsabilidade única , separação de preocupações e coesão funcional . Se você ler esses conceitos, a resposta é: Separe-os .

Um motivo simples para separar a Studentclasse "DB" (ou StudentRepository, seguir convenções mais populares) é permitir que você altere suas "regras de negócios", presentes na Studentclasse, sem afetar o código responsável pela persistência e vice- versa.

Esse tipo de separação é muito importante, não apenas entre regras de negócios e persistência, mas entre as muitas preocupações do seu sistema, para permitir que você faça alterações com um impacto mínimo em módulos não relacionados (mínimo porque às vezes é inevitável). Ajuda a construir sistemas mais robustos, mais fáceis de manter e mais confiáveis ​​quando ocorrem mudanças constantes.

Ao mesclar regras de negócios e persistência, uma classe única como no seu primeiro exemplo ou com DBuma dependência Student, você está juntando duas preocupações muito diferentes. Pode parecer que eles pertencem um ao outro; eles parecem coesos porque usam os mesmos dados. Mas eis o seguinte: a coesão não pode ser medida apenas pelos dados compartilhados entre os procedimentos, você também deve considerar o nível de abstração em que eles existem. De fato, o tipo ideal de coesão é descrito como:

Coesão funcional é quando partes de um módulo são agrupadas porque todas elas contribuem para uma única tarefa bem definida do módulo.

E, claramente, a execução de validações por um Studenttempo também persistente não forma "uma única tarefa bem definida". Então, novamente, regras de negócios e mecanismos de persistência são dois aspectos muito diferentes do sistema, que por muitos princípios de bom design orientado a objetos, devem ser mantidos separados.

Eu recomendo a leitura sobre Arquitetura Limpa , assistindo a essas palestras sobre o Princípio de Responsabilidade Única (onde um exemplo muito semelhante é usado) e assistindo a essa palestra sobre Arquitetura Limpa . Esses conceitos descrevem as razões por trás dessas separações.

MichelHenrich
fonte
11
Ah, mas a Studentclasse deve ser devidamente encapsulada, sim? Como uma classe externa pode gerenciar a persistência do estado privado? O encapsulamento deve ser equilibrado com o SoC e o SRP, mas simplesmente escolher um sobre o outro sem pesar cuidadosamente as trocas provavelmente está errado. Uma solução possível para esse dilema é usar acessadores privados de pacotes para o código de persistência usar.
amon
1
@ amon Concordo que não é tão simples, mas acredito que é uma questão de acoplamento, não de encapsulamento. Por exemplo, você pode tornar a classe Aluno abstrata, com métodos abstratos para quaisquer dados necessários à empresa, que são implementados pelo repositório. Dessa forma, a estrutura de dados do banco de dados fica completamente oculta por trás da implementação do repositório, portanto é encapsulada até na classe Student. Mas, ao mesmo tempo, o repositório está profundamente acoplado à classe Student - se qualquer método abstrato for alterado, a implementação também precisará ser alterada. É tudo sobre trocas. :)
MichelHenrich
4
A alegação de que o SRP facilita a manutenção de programas é duvidosa. O problema que o SRP cria é que, em vez de ter uma coleção de classes bem nomeadas, em que o nome diz tudo o que a classe pode ou não fazer (em outras palavras, fácil de entender o que leva à fácil manutenção), você acaba com centenas / milhares de classes que as pessoas precisavam usar um dicionário de sinônimos para escolher um nome único, muitos nomes são semelhantes, mas não exatamente, e classificar todo esse joio é uma tarefa árdua. IOW, impossibilidade de manutenção, IMO.
Dunk
3
@ Dunk, você fala como se as aulas fossem a única unidade de modularização disponível. Eu já vi e escrevi muitos projetos após o SRP, e concordo que seu exemplo acontece, mas apenas se você não fizer uso correto de pacotes e bibliotecas. Se você separar responsabilidades em pacotes, como o contexto está implícito, você poderá nomear as classes livremente novamente. Então, para manter um sistema como esse, basta se aprofundar no pacote certo antes de procurar a classe que você precisa alterar. :)
MichelHenrich
5
Os princípios do @Dunk OOD podem ser declarados como: "Em princípio, isso é melhor por causa de X, Y e Z". Isso não significa que deve sempre ser aplicado; as pessoas precisam ser pragmáticas e ponderar as compensações. Em grandes projetos, os benefícios são tais que geralmente superam a curva de aprendizado de que você fala - mas é claro que essa não é a realidade para a maioria das pessoas e, portanto, não deve ser aplicada com tanta intensidade. No entanto, mesmo no SRP de aplicativos mais leves, acho que podemos concordar que separar a persistência das regras de negócios sempre gera benefícios além do seu custo (exceto, talvez, no código de descarte).
MichelHenrich
10

Ambas as abordagens violam o princípio da responsabilidade única. Sua primeira versão atribui à Studentclasse muitas responsabilidades e a vincula a uma tecnologia específica de acesso ao banco de dados. A segunda leva a uma grande DBturma que será responsável não apenas pelos alunos, mas por qualquer outro tipo de objeto de dados em seu programa. EDIT: sua terceira abordagem é a pior, pois cria uma dependência cíclica entre a classe DB e a Studentclasse.

Portanto, se você não for escrever um programa de brinquedos, não use nenhum deles. Em vez disso, use uma classe diferente como a StudentRepositorypara fornecer uma API para carregar e salvar, desde que você implemente o código CRUD por conta própria. Você também pode considerar usar uma estrutura ORM , que pode fazer o trabalho duro por você (e a estrutura normalmente aplicará algumas decisões nas quais as operações de Carregar e Salvar devem ser colocadas).

Doc Brown
fonte
Obrigado, modifiquei minha resposta, e a terceira abordagem? de qualquer forma eu acho que aqui a questão é sobre camadas distintas make
Ahmad
Algo também me sugere usar StudentRepository em vez de DB (não sei por quê! Talvez de novo seja uma responsabilidade única), mas ainda não consigo entender por que repositório diferente para cada classe? se for apenas uma camada de acesso a dados, há mais funções utilitárias estáticas e podem estar juntas, não? apenas talvez, então seria tornou-se tão sujo e cheio de classe (sorry for my Inglês)
Ahmad
1
@ Ahmad: existem algumas razões e alguns prós e contras a considerar. Isso pode preencher um livro inteiro. O raciocínio por trás disso se resume à testabilidade, manutenção e capacidade de evoluir a longo prazo de seus programas, especialmente quando eles têm um ciclo de vida duradouro.
Doc Brown
Alguém já trabalhou em um projeto em que houve uma mudança significativa na tecnologia DB? (sem uma reescrita maciça?) Eu não tenho. Em caso afirmativo, seguir o SRP, conforme sugerido aqui para evitar ser vinculado a esse banco de dados, ajudou significativamente?
user949300
@ user949300: Acho que não me expressei claramente. A turma do aluno não fica vinculada a uma tecnologia específica do banco de dados, fica vinculada a uma tecnologia específica de acesso ao banco de dados; portanto, espera algo como um banco de dados SQL para persistência. Manter a turma do Aluno completamente inconsciente do mecanismo de persistência permite uma reutilização muito mais fácil da turma em diferentes contextos. Por exemplo, em um contexto de teste ou em um contexto em que os objetos são armazenados em um arquivo. E isso é algo que eu realmente fiz no passado com muita frequência. Não é necessário alterar toda a pilha de banco de dados do seu sistema para obter benefícios com isso.
Doc Brown
3

Existem alguns padrões que podem ser usados ​​para persistência de dados. Existe o padrão de Unidade de Trabalho , existe o padrão de Repositório , existem alguns padrões adicionais que podem ser usados ​​como a Fachada Remota, e assim por diante.

A maioria deles tem fãs e críticos. Muitas vezes, trata-se de escolher o que parece ser o melhor para o aplicativo e mantê-lo (com todas as suas vantagens e desvantagens, ou seja, não usar os dois padrões ao mesmo tempo ... a menos que você esteja realmente certo disso).

Como uma observação lateral: no seu exemplo, o RetrieveStudent, AddStudent deve ser métodos estáticos (porque eles não dependem da instância).

Outra maneira de ter métodos de salvar / carregar em classe é:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

Pessoalmente, eu usaria essa abordagem apenas em aplicativos bastante pequenos, possivelmente ferramentas para uso pessoal ou onde eu possa prever com segurança que não terei casos de uso mais complicados do que apenas salvar ou carregar objetos.

Também pessoalmente, consulte o padrão da Unidade de Trabalho. Quando você o conhece, é realmente bom em casos pequenos e grandes. E é suportado por muitas estruturas / APIs, para nomear EntityFramework ou RavenDB, por exemplo.

Gerino
fonte
2
Sobre sua observação: Embora, conceitualmente, exista apenas um banco de dados enquanto o aplicativo está em execução, isso não significa que você deve tornar estático os métodos RetriveStudent e AddStudent. Repositórios geralmente são feitos com interfaces para permitir que os desenvolvedores alternem implementações sem afetar o restante do aplicativo. Isso pode até permitir que você distribua seu aplicativo com suporte para bancos de dados diferentes, por exemplo. Essa mesma lógica, é claro, se aplica a muitas outras áreas além da persistência, como, por exemplo, a interface do usuário.
precisa saber é o seguinte
Você está definitivamente certo, eu fiz muitas suposições com base na pergunta original.
Gerino
obrigado, eu acho melhor abordagem é usar camadas como mencionado no padrão Repository
Ahmad
1

Se é um aplicativo muito simples, onde o objeto está mais ou menos vinculado ao armazenamento de dados e vice-versa (ou seja, pode ser considerado uma propriedade do armazenamento de dados), é recomendável ter um método .save () para essa classe.

Mas acho que isso seria excepcional.

Em vez disso, geralmente é melhor deixar a classe gerenciar seus dados e sua funcionalidade (como um bom cidadão de OO) e externalizar o mecanismo de persistência para outra classe ou conjunto de classes.

A outra opção é usar uma estrutura de persistência que defina persistência declarativamente (como nas anotações), mas isso ainda está externalizando a persistência.

Roubar
fonte
-1
  • Defina um método nessa classe que serializa o objeto e retorna uma sequência de bytes que você pode colocar em um banco de dados ou arquivo ou qualquer outra coisa
  • Tenha um construtor para esse objeto que usa uma sequência de bytes como entrada

Sobre o RetrieveAllStudents()método, seu sentimento é correto, provavelmente é extraviado, porque você pode ter várias listas distintas de alunos. Por que não simplesmente manter a (s) lista (s) fora da Studentclasse?

philfr
fonte
Você sabe que eu vi essa tendência em algum framework PHP, por exemplo, no Laravel, se você conhece. O modelo está vinculado ao banco de dados e pode até retornar uma série de objetos.
Ahmad