Henrique Barcelos 290 Denunciar post Postado Março 29, 2013 Citar Tudo bem que seja um assunto discutível, mas pattern significa padrão, modelo, exemplo. Na minha opinião ele deveria então no mínimo ter avisado que se tratava de uma variação do RDG. Variação sobre o quê? Qual é a forma padrão do padrão? Entenda que quando falamos de padrões esse conceito de "padrão" não existe. Segundo Cristopher Alexander - arquiteto - pai dos padrões de projeto: Citar A beleza de se utilizar padrões é o fato de você poder resolver diversos problemas parecidos sem repetir qualquer solução uma única vez. Enrico disse: O próprio GOF e o P of EAA (clássicos dos patterns) tem patterns "ronc ronc", como Singleton, Registry, Active Record, etc. não há nada 100% e repito, aprender errado é bom pois você aprende o que não fazer. Discordo do "ronc ronc", cada um tem o seu lugar e a sua aplicação. Mantras do tipo "singleton é ruim porque é global" são muito bonitinhos na teoria, mas na prática é outra história. Por muitas vezes eu tive que recorrer a soluções nada convencionais aos meus problemas, que pareciam fugir do escopo de qualquer técnica bem conhecida, mas é exatamente isso que um bom programador tem que fazer: pensar "fora da caixa". Não é porque um certo guru uma vez disse que tal padrão é ruim, que eu deveria utilizar tal padrão para resolver certo tipo de problema que eu vou fazer isso SEMPRE E SEM CONTESTAR. No dia em que eu achar que devo usar um active record, por qualquer razão, eu vou utilizar. Vou citar um exemplo: Pense que você desenvolverá um sistema embarcado que tem como característica uma pouca disponibilidade de memória. Vou tentar criar o sistema utilizando o menor número possível de objetos para evitar stack overflow na memória do bichinho. Dessa forma, além de não haver NENHUM problema, é o que se DEVE fazer. Lançar mão de padrões como Singleton, LightWeight, Register e o famigerado Active Record fará (hipoteticamente) que o meu sistema utilize menos memória? Ótimo, serão exatamente esses que vou utilizar. Mantras de gurus servem muito mais para impedir que os inexperientes programadores se rendam às "facilidades" de acesso global, métodos estáticos e afins, mas todos esses gurus utilizam (e não tão infrequentemente) o que dizem abominar, pois se aquilo existe, é porque há uma razão para isso. O único mantra que é 100% verdade, que você deve repetir sempre é: Citar Padrão de projeto não é receita de bolo. Sempre que surge uma discussão como essa, eu acabo tendo que utilizar a frase acima. Como programador, você é o senhor do seu código. Faça da forma que achar melhor. Se um dia você vir que aquilo não está bom, mude, refatore, refaça (se houver tempo hábil para isso). Compartilhar este post Link para o post Compartilhar em outros sites
Enrico Pereira 299 Denunciar post Postado Março 29, 2013 Mas estamos falando de PHP, não de um sistema embarcado, e se quiser redução de memória, veja Flyweight e Prototype. Facilidade de acesso global? Eu diria desorganização, lentidão e intestabilidade. Singleton é ruim, nunca necessário, é um padrão estúpido, que mascara a orientação a objetos criando um objeto global. Registry é outro (mapa de singletons), uma boa alternativa é o Service Locator. Métodos estáticos tem sim uma utilização: criação de objetos. Compartilhar este post Link para o post Compartilhar em outros sites
Henrique Barcelos 290 Denunciar post Postado Março 29, 2013 Citar Mas estamos falando de PHP, não de um sistema embarcado, e se quiser redução de memória, veja Flyweight e Prototype. Você pode estar falando de PHP, eu estou falando de padrões, que são independentes de linguagem. Citar Facilidade de acesso global? Eu diria desorganização, lentidão e intestabilidade. Exatamente por isso criam-se esses mantras que você repete tão insistentemente: impedir que se adquira certos vícios. Repito mais uma vez: TUDO tem uma utilidade, se não tivesse, não existiria. Algumas práticas caem em desuso justamente por se mostrarem mais nocivas do que benéficas na maioria dos casos, mas se um dia isso for necessário, seja por questões de tempo, seja porque você está lidando com um sistema que tem um legado e não pode ser profundamente alterado, você vai parar e falar pro cliente: Citar Isso tudo tá uma porcaria, eu teria feito de outro jeito, a solução não vai ser elegante e blablablablabla. Ou você vai resolver o problema com a solução "feia"? Se tem uma coisa que aprendi em 4 anos estudando engenharia é que na maioria dos casos vai valer a máxima do engenheiro: Citar O mais simples que funcione. Compartilhar este post Link para o post Compartilhar em outros sites
Enrico Pereira 299 Denunciar post Postado Março 29, 2013 Primeiramente, não disse que padrões são dependentes de linguagem, mas sim que você não teria muita das vezes que se preocupar com memória e até citei dois padrões úteis para isso. O singleton, nesse caso seria pior, pois teria acesso global, mais lento. O fato de ser um mantra não invalida o mesmo. O singleton só tem utilidade para fazer abobrinha. Vou direto ao ponto: - Singleton viola o SRP, além da classe cuidar da responsabilidade dela, ela cuida de seu ciclo de vida. - Singleton é uma parede contra testes unitários, quase sempre usamos o método resetInstance (POG!). - Singleton é algo que não necessitamos, criamos código sem necessidade e pior, criamos muita das vezes um problema. - Singleton esconde as dependências de um objeto, piorando a manutenção. - Singleton não funciona, você pode acabar com ele usando Reflection (quebrando a confiança). - Singleton gera acoplamento. - Singleton torna herança complicada algumas vezes. - Singleton consome maior memória, pois acessa estado global. - Singleton tira o controle sobre a instância. - Singleton é uma gambiarra por si só. É como você ter um martelo e querer usá-lo como furadeira. - Singleton vai justamente contra a simplicidade. Não estava falando sobre um sistema legado, se o caos já existe e não é possível consertar, continue com o caos (triste, mas...), é algo óbvio. Compartilhar este post Link para o post Compartilhar em outros sites
felipe_zmm 0 Denunciar post Postado Março 29, 2013 Em 29/03/2013 at 13:54, Henrique Barcelos disse: Variação sobre o quê? Qual é a forma padrão do padrão? Entenda que quando falamos de padrões esse conceito de "padrão" não existe. Se você tem vinte livros de design patterns que ensinam de forma x e outro que ensina de forma y, foi criado um padrão do padrão e a forma y fugiu desse padrão. Você mesmo disse que era esse caso um mau exemplo do livro. Se é um mau exemplo algum motivo tem. Mas vejam, eu já entendi que não devo me prender aos padrões como se eles fossem uma receita fixa que eu não pudesse adaptar às minhas necessidades. Compartilhar este post Link para o post Compartilhar em outros sites
Henrique Barcelos 290 Denunciar post Postado Março 29, 2013 Citar - Singleton consome maior memória, pois acessa estado global. Não faz sentido nenhum o fato de acessar um estado global ocupar mais memória. Global ou não global, o espaço ocupado é o mesmo, a diferença é que está garantido que a qualquer tempo t, só existirirá uma única instância do objeto. Em linguagens com garbage collection, é difícil precisar exatamente quando uma área de memória será liberada. Entenda que não estou defendendo o uso desenfreado de Singleton, pois isso, como você mesmo apontou, possui uma série de implicações. Enfim, isso já é fugir do escopo do tópico. Não vou rebater mais as suas afirmações. *** Citar Se você tem vinte livros de design patterns que ensinam de forma x e outro que ensina de forma y, foi criado um padrão do padrão e a forma y fugiu desse padrão. Exatamente porque existe essa LIBERDADE, o autor pode realizar as adaptações que preferir. Eu nunca li o livro, teria que fazê-lo para dar um parecer. Citar Você mesmo disse que era esse caso um mau exemplo do livro. Se é um mau exemplo algum motivo tem. Falei que era um mau exemplo se o autor realmente afirma que é possível existir um RDG sem um TDG associado. Compartilhar este post Link para o post Compartilhar em outros sites
Enrico Pereira 299 Denunciar post Postado Março 29, 2013 Grandes livros erram, esse livro provavelmente possui uma edição nova ou uma correção (errata) sobre isso. Erros em livro é como bug em software, sempre tem. Tenho quase certeza que o autor não fez de má fé, foi um erro, somos humanos, erramos. A questão do estado global gastar mais memória, é porque procurar algo em um escopo global é mais lento e requer maior processamento. Pense que eu quero pedir um papel e estou pedindo para as pessoas que estão online no fórum, e no outro caso, eu peço à qualquer membro do fórum (incluindo os off-lines) o mesmo papel. Qual vai ser mais rápido? o primeiro, óbvio, pois eu tenho um escopo reduzido. Compartilhar este post Link para o post Compartilhar em outros sites
Henrique Barcelos 290 Denunciar post Postado Março 29, 2013 Citar A questão do estado global gastar mais memória, é porque procurar algo em um escopo global é mais lento e requer maior processamento. Pense que eu quero pedir um papel e estou pedindo para as pessoas que estão online no fórum, e no outro caso, eu peço à qualquer membro do fórum (incluindo os off-lines) o mesmo papel. Qual vai ser mais rápido? o primeiro, óbvio, pois eu tenho um escopo reduzido. A única difença entre dados globais e não-globais é a sua localização em memória. Sua analogia é inválida, você não parece entender a organização em memória de um programa em execução. Dentro de um mesmo processo, acessar uma posição de memória X e uma Y custa temporalmente o mesmo tanto: um cálculo aritmético para encontrar o offset da posição e o tempo de acesso à memória RAM ou ao cache. Isso se deve ao fato de que para o sistema operacional não existe o conceito de funções, escopos, etc. dentro de um programa em execução. Compartilhar este post Link para o post Compartilhar em outros sites
felipe_zmm 0 Denunciar post Postado Março 30, 2013 É claro que o autor não fez de má fé. Pesquisei a errata e não existe correção e a edição que eu tenho é a última. De qualquer forma não estou dizendo que o livro não presta pra nada, aprendi coisas com ele. Voltando ao foco do tópico, estive estudando o pattern Factory Method e ficaram algumas dúvidas: abstract class Fabrica { abstract public function fabricar(); } class FabricaTablet extends Fabrica { public function fabricar() { return new Tablet(); } } class FabricaTelevisao extends Fabrica { public function fabricar() { return new Televisao(); } } interface Produto { public function addEstoque($numItens); public function removerEstoque($numItens); } class Tablet implements Produto { private $sistema; public function __construct($sistema) { $this->sistema = $sistema; } public function getEstoque() { return $this->estoque; } public function getSistema() { return $this->sistema; } public function setSistema($sistema) { $this->sistema = $sistema; } public function addEstoque($numItens) { if ((int)$numItens > 0) { $this->estoque += (int)$numItens; } else { throw new Exception('A quantidade a ser adicionada precisa ser maior que zero!'); } } public function removerEstoque($numItens) { if ((int)$numItens > 0) { $this->estoque -= (int)$numItens; echo "Retirando {$numItens} tablets do estoque."; } else { throw new Exception('A quantidade a ser removida precisa ser maior que zero!'); } } } class Televisao implements Produto { private $voltagem; public function __construct() { $this->voltagem = $voltagem; } public function getEstoque() { return $this->estoque; } public function getVoltagem() { return $this->voltagem; } public function setVoltagem($voltagem) { if (($voltagem == 110) || ($voltagem == 220)) { $this->voltagem = $voltagem; } else { throw new Exception('A voltagem precisa ser 110 ou 220!'); } } public function addEstoque($numItens) { if ($numItens > 0) { $this->estoque += $numItens; } else { throw new Exception('A quantidade a ser adicionada precisa ser maior que zero!'); } } public function removerEstoque($numItens) { if ($numItens > 0) { $this->estoque = $numItens; echo "Retirando {$numItens} televisões do estoque."; } else { throw new Exception('A quantidade a ser removida precisa ser maior que zero!'); } } } Se eu entendi o porque de uma classe abstrata Factory, seria para que o objeto que for instanciar o "produto" não precise conhecer exatamente qual tipo de produto vai ser utilizado. É mais ou menos isso ? Então abstraindo um banco de dados com esse pattern eu poderia criar MySQL Factory, OracleFactory, PostgreFactory e etc sem que os objetos que fazem o acesso ao banco saibam qual SGBD está sendo utilizado. Viajei ou peguei a idéia mesmo que parcialmente ? Compartilhar este post Link para o post Compartilhar em outros sites
Enrico Pereira 299 Denunciar post Postado Março 30, 2013 Perfeito, pegou a ideia até de forma completa, o Factory Method é exatamente isso. A wikipédia tem uma implementação estupidamente simples de Factory Method usando PHP (http://en.wikipedia.org/wiki/Factory_method_pattern) <?php /* factory and car interfaces */ interface CarFactory { public function makeCar(); } interface Car { public function getType(); } /* concrete implementations of the factory and car */ class SedanFactory implements CarFactory { public function makeCar() { return new Sedan(); } } class Sedan implements Car { public function getType() { return 'Sedan'; } } Ele define uma interface para um carro (o carro concreto) e para o factory (a fábrica). Aparentemente isso é mais burocrático do que simplesmente adcionar a interface e as classes concretas, mas o Factory Method vai além da abstração, ele encapsula a classe concreta, imagine que a classe Sedan mudasse de nome, poderia causar erros, mas como a instância é criada pela factory, logo há um encapsulamento. Quem for depender da factory, simplesmente não vai precisar ter conhecimento de quem é a classe concreta, apenas a interface. Pois nesse caso uma CarFactory, teria que retornar uma Car no método makeCar(), mas isso não é muito visível no PHP, pela ausência de tipagem de retorno. Se tivesse seria algo como: interface CarFactory { public Car function makeCar(); } Eu forçaria o retorno do método makeCar para ser do tipo Car, mas infelizmente o PHP não possui tipagem de retorno (ainda, um dia espero). Por isso disse que aprender em Java é mais fácil, pois vc teria noção completa do diagrama, mas basta o entendimento disso que ficaria igual no PHP. --- Henrique, tem razão eu me confundi totalmente com Javascript, onde há vários escopos e variáveis globais realmente custam mais processamento. Meio que finalizando a discussão sobre Singleton, tente no máximo não usar, talvez injetar as dependências seja mais burocrático, sendo **talvez** menos produtivo, mas lembre-se que código ruim mata e os outros contra-argumentos sobre singleton são válidos. Vamos prosseguir ao tópico. Compartilhar este post Link para o post Compartilhar em outros sites
felipe_zmm 0 Denunciar post Postado Abril 1, 2013 Bom, dando andamento ao estudo do Factory Method eu resolvi tentar fazer umas classes pra conexão com banco de dados. Seguem: abstract class Fabrica { abstract public function fabricar(); } class FabricaMySqlCon extends Fabrica { public function fabricar() { return new MySqlCon('localhost', 'loja', 'root'); } } class FabricaPgSqlCon extends Fabrica { public function fabricar() { return new PgSqlCon('localhost', 'loja', 'postgres', 'postgres'); } } abstract class BdCon { protected $host; protected $banco; protected $usuario; protected $senha; abstract public function conectar(); } class MySqlCon extends BdCon { public function __construct($host, $banco, $usuario, $senha = null) { $this->host = $host; $this->banco = $banco; $this->usuario = $usuario; $this->senha = $senha; } public function conectar() { return new PDO("mysql:host={$this->host};dbname={$this->banco}", $this->usuario, $this->senha); } } class PgSqlCon extends BdCon { public function __construct($host, $banco, $usuario, $senha = null) { $this->host = $host; $this->banco = $banco; $this->usuario = $usuario; $this->senha = $senha; } public function conectar() { return new PDO("pgsql:host={$this->host};dbname={$this->banco};user={$this->usuario};password={$this->senha}"); } } E utilizando: function __autoload($classe) { require_once("{$classe}.class.php"); } $fabrica = new FabricaMySqlCon(); $bd = $fabrica->fabricar(); $con = $bd->conectar(); $rs = $con->query('SELECT id, nome FROM produtos'); while ($linha = $rs->fetch()) { echo $linha['id'] . ' - ' . $linha['nome'] . '<br />'; } Bom, eu não sei se estou no caminho certo e por isso queria que me corrigissem e me dessem dicas de onde posso melhorar. Algo que me incomoda bastante é que não vejo onde estou ganhando com essas classes todas. Não seria muito diferente de simplesmente mudar o DSN direto na hora de instanciar o PDO. Talvez eu precise de mais abstração, como por exemplo o comando SELECT que costuma variar entre os SGBD's, para que tudo comece a fazer mais sentido na minha cabeça. Compartilhar este post Link para o post Compartilhar em outros sites
felipe_zmm 0 Denunciar post Postado Abril 1, 2013 O que me deixa um pouco confuso com isso tudo é que o fato de estar utilizando PDO já traz uma certa flexibilidade. Se ele não existisse eu teria classes que usam mysql_ ou pg_ e assim por diante. Faria mais sentido abstrair essas funcionalidades em classes distintas. Compartilhar este post Link para o post Compartilhar em outros sites
Henrique Barcelos 290 Denunciar post Postado Abril 1, 2013 Para esse caso não tem ganho nenhum, como você mesmo falou, só precisa mudar o DSN. A ideia é como no post #72, só faz sentido se você for criar wrappers para as funções "nativas". Compartilhar este post Link para o post Compartilhar em outros sites
felipe_zmm 0 Denunciar post Postado Abril 1, 2013 Fiz umas alterações nas classes (insônia é tenso): abstract class AbstractBd { protected $host; protected $banco; protected $usuario; protected $senha; abstract public function listarTodos($tabela, $limite = 10000, $offset = 0); abstract public function buscar($tabela, array $aWhere, $limite = 10000, $offset = 0); } class MySql extends AbstractBd { private $pdo; public function __construct($host, $banco, $usuario, $senha = null) { $this->host = $host; $this->banco = $banco; $this->usuario = $usuario; $this->senha = $senha; $this->pdo = new PDO("mysql:host={$this->host};dbname={$this->banco}", $this->usuario, $this->senha); } public function listarTodos($tabela, $limite = 10000, $offset = 0) { $rs = $this->pdo->query("SELECT * FROM {$tabela} LIMIT {$offset}, {$limite}"); return $rs->fetchAll(PDO::FETCH_OBJ); } public function buscar($tabela, array $aWhere, $limite = 10000, $offset = 0) { end($aWhere); $ultimo = key($aWhere); $where = ''; foreach ($aWhere as $chave => $valor) { $where .= $chave . ' ' . $valor; if ($ultimo != $chave) { $where .= ' AND '; } } $rs = $this->pdo->query("SELECT * FROM {$tabela} WHERE {$where} LIMIT {$offset}, {$limite}"); return $rs->fetchAll(PDO::FETCH_OBJ); } } class PgSql extends AbstractBd { private $pdo; public function __construct($host, $banco, $usuario, $senha = null) { $this->host = $host; $this->banco = $banco; $this->usuario = $usuario; $this->senha = $senha; $this->pdo = new PDO("pgsql:host={$this->host};dbname={$this->banco};user={$this->usuario};password={$this->senha}"); } public function listarTodos($tabela, $limite = 10000, $offset = 0) { $rs = $this->pdo->query("SELECT * FROM {$tabela} LIMIT {$limite} OFFSET {$offset}"); return $rs->fetchAll(PDO::FETCH_OBJ); } public function buscar($tabela, array $aWhere, $limite = 10000, $offset = 0) { end($aWhere); $ultimo = key($aWhere); $where = ''; foreach ($aWhere as $chave => $valor) { $where .= $chave . ' ' . $valor; if ($ultimo != $chave) { $where .= ' AND '; } } $rs = $this->pdo->query("SELECT * FROM {$tabela} WHERE {$where} LIMIT {$limite} OFFSET {$offset}"); return $rs->fetchAll(PDO::FETCH_OBJ); } } Acho que dessa forma faz mais sentido, já que a forma de utilizar LIMIT e OFFSET é diferente em MySQL e Postgre (apesar de o MySql também aceitar a forma do Postgre). Então eu teria uma classe dessa pra cada SGBD. Mas não seria um trabalho muito grande pra tão pouco ? Talvez num sistema grande isso faça sentido, mas não sei se uma solução dessas é compatível com um sistema pequeno/médio. Compartilhar este post Link para o post Compartilhar em outros sites
Evandro Oliveira 331 Denunciar post Postado Abril 1, 2013 Em 26/03/2013 at 17:27, felipe_zmm disse: A não ser que vc não esteja considerando os getters e setters como métodos.Eu, particularmente, não considero. Uma classe que contenha apenas getters/setters nada mais é que um container com, possível, validação. Em 26/03/2013 at 17:27, felipe_zmm disse: Se puder me explicar eu agradeço. Em 15/11/2002 at 02:00, Martin Fowler disse: The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other.Data mapper é uma camada de software que separa objetos na memória (tempo de execução) do banco de dados (persistência). Sua responsabilidade é transferir dados entre os dois isolando-os um do outro Na sua mesma implementação, mais adiante, veremos a necessidade de verificar se um usuário está logado if (!$user->isLoggedIn()) { throw new UnauthenticatedException('You must login first'); }Veja que - exceto em implementações de login único - não armazenamos no banco se um usuário está logado ou não. Essa verificação ficaria por conta dos objetos run-time, não cabendo ao mapper/db este método. De igual modo, quando implementamos - dentro da classe User - um mecanismo de autenticação, amarramos o sistema aquele mecanismo (no nosso exemplo, verificação no DB). Obviamente, verificar as credenciais não é e nem deve ser responsabilidade da classe User e, na maioria dos casos, nem do mapper. Na descrição, Fowler implementa como exemplos as ações de bancos de dados (inserir, editar, remover). Isso desacopla as tarefas de persistência do objeto de sistema e especifica as tarefas de persistência de acordo com o "alvo" qual o mapper aponta. Há uma relação de composição entre o Objeto e o Mapper. Não há motivos para existir um mapper sem o de-para Mapeando usuário para um DB MySQL User Mostrar conteúdo oculto class User { private $id; private $name; private $email; public function setId($id) { $this->id = intval($id, 10); } public function setName($name) { $this->id = strval($name); } public function setEmail($email, EmailValidator $validator = null) { if (!is_null($validator)) { $validator->validate($email); } $this->email = $email; } public function getId($id) { return $this->id; } public function getName($name) { return $this->name; } public function getEmail($email) { return $this->email; } public function isLoggedIn() { return false; } } abstract class UserMapper { protected $user; public function __construct(User $user) { $this->user = $user; } } class MySqlUserMapper extends UserMapper { const TABLE = 'user'; const QUERY_DATA = 'replace into :table values (:id, :name, :email, :password)'; const QUERY_SELECT = 'select id, name, email, password from :table where id = :id or email = :email'; const QUERY_DELETE = 'delete from :table where id = :id'; protected $connection; public function __construct(User $user, PDO $connection) { parent::__construct($user); $this->connection = $connection; if (is_null($user->getId())) { $this->create(); } } private function create() { $stmt = $this->connection->prepare(self::QUERY_DATA); $stmt->bindColumn $data = array( 'table' => self::TABLE, 'id' => null, 'name' => $this->user->getName(), 'email' => $this->user->getEmail(), 'password' => $this->user->getPassword() ); $stmt->execute($data); $this->user->setId($this->conn->lastInsertId()); } Veja que eu me valho da instrução [inline]replace[/inline] que não é SQL padrão e também confio no resultado retornado por lastInsertId. Outra implementação necessitaria de verificar estas abordagens e realizar as alterações necessárias. Contudo, qualquer outra implementação para qualquer outro sistema de persistência, não interferiria na implementação da classe User. Mantendo o objetivo final do pattern. Se você acaba com uma classe que seja apenas um container, pode haver um problema de modelagem do sistema, não da escolha do pattern em si. Em 26/03/2013 at 17:27, felipe_zmm disse: Quanto à conectar no banco na classe ControleLogin não sei o que fazer exatamente. ControleLogin deve validar o login contra alguma coisa e utilizando algum mecanismo. ControleLogin valida usuários num banco de dados MySQL, sob o Schema 'meusistema', dentro da tabela 'usuario'. Sendo o Campo de email único e a senha recebida deve gerar o mesmo hash md5 armazenado: select nome, email, senha from meusistema.usuario where email = '$email' and senha = MD5('$senha')Voltamos à questão da granularidade. Veja que ControleLogin precisa de - até o momento - duas "ferramentas": Um sistema de armazenamento e um algoritmo de validação. Uma abordagem possível seria: class ControleLogin { public function __construct(PDO $connection, Validator $algorithm) { $this->conn = $connection; $this->algo = $algorithm; } public function validate(User $user) { $stmt = $this->conn->prepare('select nome, email, senha from usuario where email = :email and senha =: senha'); $params = array( 'email' => $user->getEmail(), 'senha' => $this->algo->generateHash($user->getProvidedPasswd()) ); $stmt->execute($params); // [...] }Outra possível abordagem seria o próprio validador receber o "armazenador de dados". Dessa forma, [inline]ControleLogin[/inline], receberia apenas o validador e passaria o usuário para que este verifique. Esta delegação garante o princípio de responsabilidade única, garante uma modularidade e uma granularidade viável. Podemos acoplar e intercambiar componentes sem afetar o funcionamento geral da aplicação. Em 26/03/2013 at 17:27, felipe_zmm disse: Antes eu chamava a conexão no método validarUsuario, mas me disseram pra não fazer isso. Passei pro construtor, mas essa parece também não ser a melhor opção, já que a classe possui um único método que utiliza o banco de dados. Então o que fazer ? Conectar no banco fora da classe antes de utilizar o método validarUsuario ? O que pensei em fazer é no construtor da classe BD passar as configurações de conexão, resolvendo assim o problema de fixar as configurações e adicionar um método conectar e outro desconectar. Resolveria ? Pense em 1. responsabilidade: Não é de responsabilidade do validador conectar em banco algum. E se eu quiser validar contra o arquivo de senhas do linux (/etc/passwd) ???? 2. Reusabilidade: Eu não posso aproveitar esta conexão quando o usuário validado quiser alterar seu email no sistema??? Em 26/03/2013 at 19:45, felipe_zmm disse: 1 - Minha classe BD se tornou inútil (acho que ela já era antes), então parei de usá-la e simplesmente instancio um objeto PDO e passo-o como parâmetro:Aqui você já passou a demonstrar traços de que estava entendendo melhor como as coisas funcionam... Em 26/03/2013 at 19:45, felipe_zmm disse: 2 - Devo utilizar o autoload nos arquivos de classes ou da forma como fiz, no próprio script que instancia as classes ? Algo que me deixa confuso é que com o paradigma OO eu tenho a impressão de que devo deixar de lado as práticas anteriores, e me veio a dúvida se posso incluir essa função autoload em um arquivo e chamá-lo por include. https://github.com/pear/pear-core/blob/master/PEAR/Autoloader.phphttps://github.com/composer/composer/blob/master/src/Composer/Autoload/ClassLoader.php https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md Basicamente, no seu arquivo de bootstrap - o arquivo que será efetivamente executado e será responsável por utilizar as classes e instâncias, você pode verificar a existência/registro do autoloader utilizado e, caso necessário, registrá-lo: function meuAutoLoader($className) { include "./$className.php"; } in_array('meuAutoLoader', spl_autoload_functions()) or spl_autoload_register('meuAutoLoader');Outra alternativa é a diretiva auto_prepend_file, que você pode utilizar para incluir seu arquivo de configurações e autoloader. Em 26/03/2013 at 19:45, felipe_zmm disse: 3 - Quanto a criar uma classe para iniciar e destruir sessão, esta não seria um pouco "inútil" ? Eu teria um método iniciarSessao que apenas chama session_start (é claro que posso fazer outras verificações) e um outro método destruirSessão que a limpa e destrói. Me parece uma classe sem propósito real de certa forma. Talvez eu tenha entendido de forma errada a sugestão.Tão inútil que passou a ser parte do core. Em 26/03/2013 at 21:05, felipe_zmm disse: Então se eu entendi, o motivo de uma classe para gerenciamento de sessão é mais para manter minhas classes testáveis ?Não. Implementação prática Em 27/03/2013 at 04:13, felipe_zmm disse: O problema de passar o PDO no construtor da classe ControleLogin é que apenas um método o utiliza (pelo menos por enquanto). Só se eu colocar como parâmetro opcional. O que não sei é se existe alguma restrição teórica quanto a isso.Experimente diversas abordagens e verifique qual se adapta melhor. Tente variar o cenário, tente pensar em que outras circunstâncias ControleLogin precisaria de um sistema de persistência. Em 27/03/2013 at 20:06, felipe_zmm disse: public function usuarioLogado() { session_start(); return (isset($_SESSION['login'])) && (isset($_SESSION['validado'])); }[ Essa é a função que eu chamo nas páginas restritas, e dependendo de seu retorno o usuário é redirecionado para a página de login. Para utilizar isso, na hora de instanciar o objeto eu teria que instanciar e passar um PDO sendo que isso não faz o menor sentido no contexto desse método. Por isso coloquei o PDO como opcional no construtor. Talvez seja um problema de projeto da classe, mas não sei como resolvê-lo. Estar logado é uma situação, um estado das instâncias de Usuario. Movendo a responsabilidade para o real responsável você desacopla a confusa necessidade de um componente desnecessário ;) $usuario->logado() or die('você não está logado!'); Em 27/03/2013 at 20:06, felipe_zmm disse: Uma coisa que está me preocupando um pouco é o seguinte: class UsuarioMapper { public function incluir(Usuario $usuario) { $qry = "INSERT INTO {$this->tabela}(id, usuario, senha, email, nome, sobrenome, status, data_cadastro) " . "VALUES(null, :login, :senha, :email, :nome, :sobrenome, '1', CURDATE())"; try { $stmt = $this->bd->prepare($qry); $stmt->bindParam(':login', $usuario->getLogin()); $stmt->bindParam(':senha', $usuario->getSenha()); $stmt->bindParam(':email', $usuario->getSenha()); $stmt->bindParam(':nome', $usuario->getNome()); $stmt->bindParam(':sobrenome', $usuario->getSobrenome()); $stmt->execute(); $ultimoId = $this->bd->lastInsertId(); $usuario->setId($ultimoId); return true; } catch (PDOException $e) { echo $e->getMessage(); return false; } } As minhas consultas SQL estão fixadas no código da classe Mapper, não sei se isso é correto. Fico pensando na reutilização disso. Talvez fosse melhor passar as consultas como parâmetro ? Mas por outro lado isso aumenta a complexidade na utilização da classe. Não sei, talvez eu esteja meio paranoico quando aos conceitos de manter as coisas simples, reutilização e etc. As consultas são específicas de cada mapper. Como o exemplo que eu passei anteriormente. A única alteração que eu faria no seu caso seria movê-las para propriedades e/ou constantes, ao invés de variáveis criadas e destruídas a cada chamada do método. Em 28/03/2013 at 01:28, felipe_zmm disse: Tudo que eu faço nos últimos dias é ler patterns, estudar patterns e praticar patterns. O problema está na divergência de informações que eu encontro por aí. No começo deste tópico eu citei que estava lendo o famoso livro "PHP - Programando com Orientação a Objetos" do Pablo Dall'Oglio. Se tudo que eu ando lendo na internet está correto, eu gastei um tempo absurdo lendo esse livro e aprendendo conceitos errados. Um exemplo são os patterns Table Data Gateway e Row Data Gateway. Aqui mesmo no fórum existe um tópico do Henrique Barcelos que explica e exemplifica os dois, e segundo ele não existe RDG sem TDG. No livro está exatamente o contrário, além de usar um RDG sem TDG ele instancia PDO dentro da classe RDG. Onde estaria o desacoplamento nisso ? E o dependency injection ? Isso é o que mais está me frustrando nessa tentativa de aprender a programar OO da forma correta. Se eu não posso confiar em absolutamente nada que leio, nem em um livro até bem conceituado, vou seguir em frente como ? No mais, Enrico, agradeço toda sua atenção e paciência. Talvez minhas dúvidas pareçam bobas pra quem já entende, mas realmente é difícil porque tenho encontrado muitas divergências. Em 26/03/2013 at 15:14, Evandro Oliveira disse: [...] São orientações, conselhos. Não normas. Não há verdade absoluta. Não há um conceito que abranja todos os cenários possíveis da programação. [...] Em 28/03/2013 at 02:09, felipe_zmm disse: Sobre os testes unitários eu já ouvir falar bastante do PHPUnit, mas não sei exatamente o que ele faz.Testes unitários garantem o funcionamento esperado do seu código no nível mais atômico da programação. É uma forma de - principalmente - evitar regressões e que novas implementações não quebrem o que já funciona. http://blog.thiagobelem.net/aprendendo-tdd-ou-desenvolvimento-orientado-a-testes/ Em 28/03/2013 at 02:09, felipe_zmm disse: Mas talvez eu esteja com um medo de errar excessivo mesmo, querendo fazer tudo certo logo de cara e isso obviamente não vai acontecer. É uma característica minha mesmo, ser perfeccionista e não querer errar. Citar Perguntou o discípulo ao mestre: - Como nos tornamos sábios? O mestre respondeu: - Boas escolhas. - E como fazemos boas escolhas? - Experiência. Acrescentou o mestre. - E como adquirimos experiência? insistiu o discípulo. - Más escolhas. Desculpe o post extremamente defasado. Estive atualizando meu sistema (chegada eminente do Ubuntu 13.04) aliado ao feriado me deixaram um pouco sem tempo semana passada. Lerei os posts pendentes e retornarei uma nova resposta o mais breve possível. Compartilhar este post Link para o post Compartilhar em outros sites
Evandro Oliveira 331 Denunciar post Postado Abril 1, 2013 Em 29/03/2013 at 01:22, felipe_zmm disse: É bom em termos. No meu caso talvez seja útil porque eu resolvi pesquisar mais, buscar outras explicações. Agora imagina o tanto de gente que leu esse livro e está aplicando o pattern da forma errada. A forma errada foi, por muitos anos, a única alternativa disponível. Veja que o PHP está desativando as funções mysql_*. Pense na quantidade de livros, artigos e tutoriais que estarão errados depois desta efetivação. Pense na quantidade de registros que já estão errados ensinando Orientação a Objetos em php da forma errada simplesmente porque abordam a versão 4. Eu aprendi assim!. Em 29/03/2013 at 14:37, Enrico Pereira disse: Mas estamos falando de PHP, não de um sistema embarcado, e se quiser redução de memória, veja Flyweight e Prototype. Uma implementação incorreta de Flyweight consome mais memória do que reduz. Sem contar que este nem é o objetivo do padrão. Em 29/03/2013 at 14:37, Enrico Pereira disse: Facilidade de acesso global? Eu diria desorganização, lentidão e intestabilidade. Singleton é ruim, nunca necessário, é um padrão estúpido, que mascara a orientação a objetos criando um objeto global. Registry é outro (mapa de singletons), uma boa alternativa é o Service Locator. Métodos estáticos tem sim uma utilização: criação de objetos. Já discutimos sobre isso, mas acho que não ficou muito claro =\. Acho que falhei no exemplo ou em expressar meu ponto de vista. Segue uma segunda alternativa: class Hash { public function md5($input) { if (!function_exists('md5')) { throw new NotImplementedException('MD5 is unavailable'); } return md5($input); } public function crc32($input) { if (!function_exists('crc32')) { throw new NotImplementedException('Cannot generate CRC32 sum.'); } return crc32($input); } public function sha1($input) { if (!function_exists('sha1')) { throw new NotImplementedException('Algorithm SHA1 is not implemented'); } return sha1($input); } public static function generate($input, $algorithm) { if (!method_exists(__CLASS__, $algorithm)) { throw new InvalidArgumentException("Unknown algorithm \"{$algorithm}\""); } return call_user_func(array(__CLASS__, $algorithm), $input); } } Obviamente, outra possível implementação seria cada algoritmo ser uma subclasse de Hash. Mas, em nenhuma das duas, há um bom motivo para que estes geradores sejam instâncias. Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton viola o SRP, além da classe cuidar da responsabilidade dela, ela cuida de seu ciclo de vida. Basta mover o método [inline]getInstance[/inline] para outra classe. Registry surgiu daí. Pra mim, é inviável e puro capricho. Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton é uma parede contra testes unitários, quase sempre usamos o método resetInstance (POG!). Duble os métodos, não as classes/instâncias. Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton é algo que não necessitamos, criamos código sem necessidade e pior, criamos muita das vezes um problema.Qual? Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton esconde as dependências de um objeto, piorando a manutenção.Com Singleton, perdemos apenas o método construtor. Toda e qualquer injeção de dependência pode ser feita de outra forma. Getters/Setters ou passando os dependentes no método em questão. Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton não funciona, você pode acabar com ele usando Reflection (quebrando a confiança).Qualquer coisa pode ser modificada utilizando Reflection. Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton gera acoplamento.Da classe com ela mesma???? Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton torna herança complicada algumas vezes. Se o objetivo é ter uma única instância, por que raios eu quereria ter subclasses? Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton tira o controle sobre a instância. Pelo contrário. Singleton toma para si o controle da instância. Especifique um cenário onde isso é um problema... Em 29/03/2013 at 16:27, Enrico Pereira disse: - Singleton é uma gambiarra por si só. É como você ter um martelo e querer usá-lo como furadeira. - Singleton vai justamente contra a simplicidade. Acusações sem base apenas para dizer que o padrão é ruim. Em 01/04/2013 at 04:27, felipe_zmm disse: Bom, dando andamento ao estudo do Factory Method eu resolvi tentar fazer umas classes pra conexão com banco de dados. Seguem: Mostrar conteúdo oculto abstract class Fabrica { abstract public function fabricar(); } class FabricaMySqlCon extends Fabrica { public function fabricar() { return new MySqlCon('localhost', 'loja', 'root'); } } class FabricaPgSqlCon extends Fabrica { public function fabricar() { return new PgSqlCon('localhost', 'loja', 'postgres', 'postgres'); } } abstract class BdCon { protected $host; protected $banco; protected $usuario; protected $senha; abstract public function conectar(); } class MySqlCon extends BdCon { public function __construct($host, $banco, $usuario, $senha = null) { $this->host = $host; $this->banco = $banco; $this->usuario = $usuario; $this->senha = $senha; } public function conectar() { return new PDO("mysql:host={$this->host};dbname={$this->banco}", $this->usuario, $this->senha); } } class PgSqlCon extends BdCon { public function __construct($host, $banco, $usuario, $senha = null) { $this->host = $host; $this->banco = $banco; $this->usuario = $usuario; $this->senha = $senha; } public function conectar() { return new PDO("pgsql:host={$this->host};dbname={$this->banco};user={$this->usuario};password={$this->senha}"); } } E utilizando: function __autoload($classe) { require_once("{$classe}.class.php"); } $fabrica = new FabricaMySqlCon(); $bd = $fabrica->fabricar(); $con = $bd->conectar(); $rs = $con->query('SELECT id, nome FROM produtos'); while ($linha = $rs->fetch()) { echo $linha['id'] . ' - ' . $linha['nome'] . '<br />'; } Bom, eu não sei se estou no caminho certo e por isso queria que me corrigissem e me dessem dicas de onde posso melhorar. Algo que me incomoda bastante é que não vejo onde estou ganhando com essas classes todas. Não seria muito diferente de simplesmente mudar o DSN direto na hora de instanciar o PDO. Talvez eu precise de mais abstração, como por exemplo o comando SELECT que costuma variar entre os SGBD's, para que tudo comece a fazer mais sentido na minha cabeça. Em 01/04/2013 at 08:31, Henrique Barcelos disse: Para esse caso não tem ganho nenhum, como você mesmo falou, só precisa mudar o DSN. A ideia é como no post #72, só faz sentido se você for criar wrappers para as funções "nativas". Henrique já respondeu. Quero apenas complementar: Veja que PDO já deixou a abstração ao máximo possível. Não haveria motivo para os devs simplesmente "Ah, vamos parar por aqui e deixar o pessoal implementar daqui pra frente."... Você chegou à conclusão óbvia: Em 01/04/2013 at 04:27, felipe_zmm disse: Não seria muito diferente de simplesmente mudar o DSN direto na hora de instanciar o PDO. É a pior bobagem que programadores PHP tentam fazer quando iniciam em OOP: "Criar a classe banco" Compartilhar este post Link para o post Compartilhar em outros sites
Henrique Barcelos 290 Denunciar post Postado Abril 1, 2013 Só pra exemplificar, meu factory de conectores é este aqui: :seta: https://github.com/henriquejpb/HydraFramework/blob/master/library/Hydra/Db.php Simplesmente vefirica se uma dada classe existe e retorna uma instância. Compartilhar este post Link para o post Compartilhar em outros sites
felipe_zmm 0 Denunciar post Postado Abril 1, 2013 Em 01/04/2013 at 15:24, Evandro Oliveira disse: Veja que eu me valho da instrução [inline]replace[/inline] que não é SQL padrão e também confio no resultado retornado por lastInsertId. Outra implementação necessitaria de verificar estas abordagens e realizar as alterações necessárias. Contudo, qualquer outra implementação para qualquer outro sistema de persistência, não interferiria na implementação da classe User. É nisso que estou tentando chegar. Separar e abstrair as funcionalidades de forma que se amanhã eu trocar o meio de armazenar os dados não seja necessário sair alterando o código em tudo que é canto. Acho que o exemplo que você deu com a classe MySqlUserMapper é uma solução bacana, mas eu continuo sem saber se eu estou entendendo o propósito da coisa direito. Na prática não existe muita diferença entre fazer uma classe PostgreUserMapper e alterar a classe MySqlUserMapper. Só se no futuro eu voltar a usar MySql e a classe já vai estar lá, seria apenas mudar a instância. Acho que isso tudo cai no conceito de DRY e de reutilização do código. Em 01/04/2013 at 15:24, Evandro Oliveira disse: Experimente diversas abordagens e verifique qual se adapta melhor. Tente variar o cenário, tente pensar em que outras circunstâncias ControleLogin precisaria de um sistema de persistência. Eu fiz alterações nessa classe, e acabou ficando assim: class ControleLogin { private $sessao; private $pdo; private $login; private $senha; public function __construct(PDO $pdo, Sessao $sessao) { $this->pdo = $pdo; $this->sessao = $sessao; } public function autenticarLogin($login, $senha, $tabela) { $this->login = $login; $this->senha = $this->gerarHash($senha); $qry = "SELECT COUNT(id) FROM {$tabela} " . "WHERE login = :login AND senha = :senha AND status = '1'"; $stmt = $this->pdo->prepare($qry); $stmt->bindParam(':login', $this->login); $stmt->bindParam(':senha', $this->senha); $stmt->execute(); if ($stmt->fetchColumn() == 1) { $this->sessao->setarValor('login', $this->login); $this->sessao->setarValor('validado', 1); return true; } } public function logout() { $this->sessao->destruirSessao(); } private function gerarHash($senha) { return sha1($senha); } } Bom, ainda tenho o problema de ter que instanciar um PDO pra utilizar o método logout, que não faz uso do mesmo. A sua idéia de ter uma classe Validator é interessante (logo no começo do tópico outro usuário tinha dado essa idéia, mas acho que eu ainda não conseguia enxergar a utilidade disso), porque eu desacoplaria o PDO de ControleLogin, mas eu teria o que na classe ? Um método apenas ? Isso faz sentido ? O que me desanima um pouco às vezes é que eu pareço estar andando em círculos sem evoluir muito. Compartilhar este post Link para o post Compartilhar em outros sites
Henrique Barcelos 290 Denunciar post Postado Abril 2, 2013 Citar Bom, ainda tenho o problema de ter que instanciar um PDO pra utilizar o método logout, que não faz uso do mesmo Eu ia até te falar isso antes, mas pensei que você fosse mesmo chegar sozinho a isso. Não vou nem entrar no mérito de você restringir seu controle de login ao banco de dados para manter a simplicidade. Antes de mais nada, pra que servem as propriedades $login e $senha da classe, se você os passa como parâmetros no método login? Como deve ser o seu raciocínio quanto aos parâmetros necessários no construtor? Responda a seguinte pergunta: que tipo de dependência é INDISPENSÁVEL para o funcionamento dos métodos da sua classe? Observe que dos 3 métodos existentes, apenas 1 utiliza a instância de PDO, logo, ela não é tão essencial assim, logo, você não deveria impor que para instanciar ControleLogin você precisa de um objeto PDO. A única dependência forte é a classe de sessão. Veja: class ControleLogin { private $sessao; public function __construct(Sessao $sessao) { $this->sessao = $sessao; } public function autenticarLogin(PDO $pdo, $tabela, $login, $senha) { $senha = $this->gerarHash($senha); $qry = "SELECT COUNT(id) FROM {$tabela} " . "WHERE login = :login AND senha = :senha AND status = '1'"; $stmt = $pdo->prepare($qry); $stmt->bindParam(':login', $this->login); $stmt->bindParam(':senha', $this->senha); $stmt->execute(); if ($stmt->fetchColumn() == 1) { $this->sessao->setarValor('login', $login); $this->sessao->setarValor('validado', 1); return true; } else { return false; } } public function logout() { $this->sessao->destruirSessao(); } private function gerarHash($senha) { return sha1($senha); } } Problema resolvido, não? Mas a classe tem outros problemas. Exemplo: você restringe o uso de sha1 como método de critografia, mas e se quiser alterar? Se por exemplo, existem 2 tipos de autenticação diferente no seu sistema, um usa md5 e o outro sha1, como você faria? Compartilhar este post Link para o post Compartilhar em outros sites
Evandro Oliveira 331 Denunciar post Postado Abril 2, 2013 Em 01/04/2013 at 21:18, felipe_zmm disse: Na prática não existe muita diferença entre fazer uma classe PostgreUserMapper e alterar a classe MySqlUserMapper. Só se no futuro eu voltar a usar MySql e a classe já vai estar lá, seria apenas mudar a instância. Acho que isso tudo cai no conceito de DRY e de reutilização do código. Perfeito! Em 01/04/2013 at 21:18, felipe_zmm disse: Bom, ainda tenho o problema de ter que instanciar um PDO pra utilizar o método logout, que não faz uso do mesmo. A sua idéia de ter uma classe Validator é interessante (logo no começo do tópico outro usuário tinha dado essa idéia, mas acho que eu ainda não conseguia enxergar a utilidade disso), porque eu desacoplaria o PDO de ControleLogin, mas eu teria o que na classe ? Um método apenas ? Isso faz sentido ? Novamente, responsabilidades. Responda: Qual a responsabilidade do ControleLogin??? Tornar determinado usuário logado, correto? Como isso é feito? É iniciada uma Sessão para determinado usuário e, pelo menos deveria ser, definido o estado do usuário como logado. Em que circunstância ControleLogin deve realizar sua tarefa? Quando as credenciais fornecidas passarem em determinada validação. Isolamos as responsabilidades e esclarecemos as dependências: [inline]ControleLogin[/inline] depende de [inline]User[/inline] para obter as credenciais e torná-lo logado. Precisa, também, de uma [inline]Session[/inline] para guardar o usuário logado entre as requisições. Por fim, sua tarefa apenas será executada se as credenciais passarem por um [inline]Validator[/inline]. Novamente, a forma de implementação depende da abordagem. Todos os exemplos que demonstrarei a seguir são válidos e expõem pontos positivos e negativos: // Mecanismo de validação definido em uma classe Validator // Mecanismo e sessão são constantes $controle = new ControleLogin(new Validator(new PDO), new Session); $controle->login($user); // Mecanismo de persistência definido no controleLogin $controle = new MySqlControleLogin(new Validator, new Session); $controle->login($user); // Mecanismo de validação definido no controleLogin $controle = new MD5ControleLogin(new PDO, new Session); $controle->login($user); // Validação e persistência independentes // Sessão independente $controle = new ControleLogin(new PDO, new Validator); $controle->login($user, new Session); Voltamos à palavrinha-chave granularidade Em 01/04/2013 at 21:18, felipe_zmm disse: Na prática não existe muita diferença entre fazer uma classe PostgreUserMapper e alterar a classe MySqlUserMapper. [...] isso tudo cai no conceito de DRY e de reutilização do código. Tudo depende do quanto você quer poder trocar. Não quero poder trocar nem o banco de dados, nem a sessão, nem a validação // O método construtor de ControleLogin será // responsável por criar uma instância de PDO, // executar as consultas e criar a sessão $controle = new ControleLogin; $controle->login($user); Isso gera um alto acoplamento, mas foi decidido assim. Sabe-se que será assim e suas consequências. Quanto menor o acoplamento, mais serão as partes que podem ser trocadas. Em 01/04/2013 at 21:18, felipe_zmm disse: O que me desanima um pouco às vezes é que eu pareço estar andando em círculos sem evoluir muito. A evolução é visível e notável. Basta voltar ao seu primeiro post, comparar aquele código com a sua versão atual e explicar para você mesmo porque de cada modificação. Você estará esclarecendo e expondo a quantidade de conceitos que já foram aborados e entendidos num período de 8 dias. Compartilhar este post Link para o post Compartilhar em outros sites