To understand the tree, you need to look at the forest. To understand a forest you need to look at the trees. Clients normaly want to see the forest, but developers like to develop trees. A big step taken by the agile process is to start moving the developers to see the forest, not only the trees that compose that forest.
When I was trying to learn how to draw, the good teachers kept telling me to do things from the outer to the inner. TDD and BDD brought a broader vision to this. But I still have a feeling that we need to start from a little bit before and we also need to have a beeter picture of the tree.
The standart bdd tools, using a strict grammar is nice. But, how do we see the results? Are they results at all?
No, they are the specifications. For tests we have results and for specification we have what? I think in examples and these can be working or not workin. So, how do I take a good look at the specifications and see what parts are working, incomplete, not working....
I have not seen yet this being addressed by any tool. Colored test results from a little piece of specification, as most tools will produce, won´t give me the forest view. Forest have leaves, flowers, stems and other important things to be noticed to understand the forest. I think we should think more in writing specs to be read and understood by humans than by continuous integration tools. They should be extremely easy to navigate, search and write. Images are essential in some place, they need to have a way to be added.
So I think there is a gap to be filled.... that´s why I´ve started PSpec.
Mostrando postagens com marcador php. Mostrar todas as postagens
Mostrando postagens com marcador php. Mostrar todas as postagens
quarta-feira, 22 de setembro de 2010
quarta-feira, 30 de junho de 2010
Doctrine - Operações em lote ou "batch updates" - algums dicas
O Doctrine não é uma ferramenta feita para operações em lote, ele é um ORM. Mas tem alguns momentos que já temos o método na nossa camada de negócio e queremos chama-lo um monte de vezes para executar alguma manutenção no banco de dados ou algo assim.
Nestes momentos nem sempre é necessário reescrever tudo em um sql otimizado, em casos mais simples ou não tão críticos, podemos usar o que já temos. Porém, fazer esse tipo de operação no Doctrine requer alguns cuidados que temos aprendido com o tempo. Coloco aqui algumas dicas para quem quiser pular a fase do aprendizado pelo erro. Mais abaixo também coloco um exemplo de código para o tão famigerado e entretanto tão usado copy and paste.
Dicas:
1 - Faça um looping iniciando e "commitando" a transação. Isso aumenta em muito a velocidade de insert/update no banco. A quantidade de registros atualizados a cada iteração vai ter que ser definida na tentativa e erro, mas quanto mais simples a operação, maior a quantidade de itens que podem ser incluidos. Vale perceber que mesmo que a transação funcione, se forem muitos itens, o resultado final pode ficar mais lento mesmo assim.
2 - Sempre chame o método free() passando o parâmetro true para seus objetos instanciados pelo doctrine para que ele libere a memória usada por aquele objeto. Eu chamo também um unset() logo em seguida.
3 - Se você usar uma action no symfony (isso é, se é que você usa o symfony), ele tem uma mania de querer repetir a operação depois que termina. Por algum motivo que ainda não descobri, ele faz um redirect ou forward para a mesma action, então eu coloco aquele die() no final. Sei que ideal seria uma task rodando do shell, mas faço assim pq acho mais simples, principalmente para operações que vão rodar uma vez só. Se for algo que vai ser ativado pelo cron, aí não tem como fugir das tasks.
Para recortar e colar (divirta-se):
Nestes momentos nem sempre é necessário reescrever tudo em um sql otimizado, em casos mais simples ou não tão críticos, podemos usar o que já temos. Porém, fazer esse tipo de operação no Doctrine requer alguns cuidados que temos aprendido com o tempo. Coloco aqui algumas dicas para quem quiser pular a fase do aprendizado pelo erro. Mais abaixo também coloco um exemplo de código para o tão famigerado e entretanto tão usado copy and paste.
Dicas:
1 - Faça um looping iniciando e "commitando" a transação. Isso aumenta em muito a velocidade de insert/update no banco. A quantidade de registros atualizados a cada iteração vai ter que ser definida na tentativa e erro, mas quanto mais simples a operação, maior a quantidade de itens que podem ser incluidos. Vale perceber que mesmo que a transação funcione, se forem muitos itens, o resultado final pode ficar mais lento mesmo assim.
2 - Sempre chame o método free() passando o parâmetro true para seus objetos instanciados pelo doctrine para que ele libere a memória usada por aquele objeto. Eu chamo também um unset() logo em seguida.
3 - Se você usar uma action no symfony (isso é, se é que você usa o symfony), ele tem uma mania de querer repetir a operação depois que termina. Por algum motivo que ainda não descobri, ele faz um redirect ou forward para a mesma action, então eu coloco aquele die() no final. Sei que ideal seria uma task rodando do shell, mas faço assim pq acho mais simples, principalmente para operações que vão rodar uma vez só. Se for algo que vai ser ativado pelo cron, aí não tem como fugir das tasks.
Para recortar e colar (divirta-se):
function executeRecalculateAllRatings() {
set_time_limit(3600);
$min = 0;
$increment = 1000;
$last = 50000;
$count = 0;
while ($min <= $last) { echo "<>NEW TRANSACTION - $min <>";
Doctrine_Manager::connection()->beginTransaction();
$comments = CommentTable::build()->findByIdRange($min, $min + $increment);
foreach($comments as $comment) {
$res = RatingLogTable::build()->calculateCommentRatingTotal($comment['id']);
$comment->setRating($res[0],$res[1]);
$comment->save();
$comment->free(true);
unset($comment);
echo $count++ . ' - ';
}
$min = $min + $increment;
Doctrine_Manager::connection()->commit();
}
die();
}
Marcadores:
batch,
doctrine,
memory,
php,
phpdoctrine
sexta-feira, 18 de junho de 2010
PHP - Para fazer refactoring faz diferença se um método é públicos ou privados?
Para muitos o fato de um atributo ou método ser público ou privado pode nada ter a ver com refactoring de código. Afinal de contas isso tem mais a ver com o fato dessa classe poder ser acessada de fora ou não do objeto em questão. Então, a princípio, é melhor deixar aquele método público (padrão do php), afinal de contas deixando ele público, ele poderá ser usado por muitas outras classes, correto?
ERRADO!
Toda vez que você cria um método e declara ele como sendo público você está aumentando a interface do seu objeto. Com isso o seu sistema fica mais complexo.
E o que isso tem a ver com refactoring?
Para quem refatora código normalmente (todos fazem isso, correto? ), isso é bem fácil de entender. Se eu tenho um método público, estou dizendo que este método pode estar sendo chamado de qualquer lugar no sistema. Ou seja, se eu precisar alterar o meu método, vou precisar revisar o sistema inteiro para saber se alguém usa aquele método. E para uma linguagem dinâmica como o php, isso não é nenhum pouco fácil de fazer usando uma IDE.
Tudo que é dificil de fazer parece que atrai aquela conhecida de muitos programadores: preguiça! E a preguiça gera "ctrl+c ctrl+v". Resultado: código duplicado, dificuldade de fazer refactoring, problemas na implementação de novas regras. Mais erros, mais custo de manutenção, etc... Você deve conhecer esta novela!
Já se a pessoa que declarou o método colocou uma simples palavra antes dele (protected ou private), aí fica bem mais fácil de saber se o método está sendo usado no sistema, afinal de contas ele só poderá ser usado naquela classe ou filhas. Assim a preguiça fica no canto dela, o método é refatorado e o código fica limpo, fácil de mover, etc... espero que conheça esta história também! Se não conhece, a hora é agora. Sucesso!
ERRADO!
Toda vez que você cria um método e declara ele como sendo público você está aumentando a interface do seu objeto. Com isso o seu sistema fica mais complexo.
E o que isso tem a ver com refactoring?
Para quem refatora código normalmente (todos fazem isso, correto? ), isso é bem fácil de entender. Se eu tenho um método público, estou dizendo que este método pode estar sendo chamado de qualquer lugar no sistema. Ou seja, se eu precisar alterar o meu método, vou precisar revisar o sistema inteiro para saber se alguém usa aquele método. E para uma linguagem dinâmica como o php, isso não é nenhum pouco fácil de fazer usando uma IDE.
Tudo que é dificil de fazer parece que atrai aquela conhecida de muitos programadores: preguiça! E a preguiça gera "ctrl+c ctrl+v". Resultado: código duplicado, dificuldade de fazer refactoring, problemas na implementação de novas regras. Mais erros, mais custo de manutenção, etc... Você deve conhecer esta novela!
Já se a pessoa que declarou o método colocou uma simples palavra antes dele (protected ou private), aí fica bem mais fácil de saber se o método está sendo usado no sistema, afinal de contas ele só poderá ser usado naquela classe ou filhas. Assim a preguiça fica no canto dela, o método é refatorado e o código fica limpo, fácil de mover, etc... espero que conheça esta história também! Se não conhece, a hora é agora. Sucesso!
sábado, 29 de maio de 2010
Que tal uma validação de dados "universal" para a comunidade php?
Validação de dados é um tema que sempre gera alguma controvérsia. Alguns acham que ela tem que ser feita na action, outros em uma camada de serviço ou de aplicação e outros no modelo. A maioria das pessoas também concorda que ela deva ser feita no browser. Neste texto pretendo examinar um pouco este assunto e sugerir a necessidade de se ter um componente de validação de dados para php como um projeto independente de qualquer framework ou camada.
Esses dias li um artigo interessante que diz que ao mesmo tempo que a comunidade ruby está caminhando para um único padrão forte de framework com muita convenção sobre conficuração, a comunidade php está caminhando para retirar a "mágica" dos códigos focando em padrões de projeto e OO, ter mais de um framework robusto e buscar ter interoperabilidade entre os frameworks. Achei isso muito bom. Pela minha experiência vejo que esses comportamentos "mágicos" dos frameworks costumam embaralhar muito a cabeça dos programadores mais jóvens.
Nesse rumo também vemos que algumas ferramentas estão amadurecendo muito e buscando encontrar o seu foco principal, removendo tudo aquilo que foge a esse foco. O Doctrine 2, por exemplo, que é um projeto que venho estudando, não vem com componente de validação integrado na versão 2. No início fiquei bem chateado com isso pois gostava muito de definir já algumas restrições no yml e já ter essa validação prontinha pra mim. Mais depois consegui perceber que o foco de um ORM não é esse e acho que se cada ferramente se manter no seu foco ela tem mais sucesso de prosperar.
Além disso se examinarmos um pouco mais, como já falei antes, dependendo da arquitetura uns acham melhor ter a validação no modelo, outros no JS, outros na action, etc... Não quero nesse texto ficar discutindo o que é melhor. Eu mesmo acho que isso varia de sistema pra sistema. Mas se percebemos bem essas necessidades vemos que a comunidade se beneficiará muito de ter uma ferramenta de validação de dados como sendo uma biblioteca completamente isolada. Assim ela poderá ser utilizada com qualquer framework e em qualquer camada. Claro que podem ser definidos adaptadores para integrar essa ferramenta em um lugar ou outro, mas o cerne dela deve ser "puro".
O meu pensamento é que se tivermos um arquivo de definição de dados, ele possa ser lido e usado para validar tanto um entidade na camada de modelo quanto um formulário no javascript. Com isso criamos um padrão de formato de dados que pode ser uma ótima contribuição nessa filosofia de ter ferramentas que podem ser usados nos diversos frameworks e camadas.
Algumas coisas eu acho interessante que essa ferramenta tenha:
http://groups.google.com/group/json-schema/web/json-schema-proposal-working-draft
http://tools.ietf.org/html/draft-zyp-json-schema-02
Mas com certeza existem outros. O que quero ressaltar aqui é exatamente a necessidade da existência de uma ferramenta como essa. Tendo algo assim separado de frameworks, a comunidade pode juntar esforços para contribuir melhorando cada vez mais os algorítmos de validação e ampliando cada vez mais os tipos de dados disponíveis.
Esses dias li um artigo interessante que diz que ao mesmo tempo que a comunidade ruby está caminhando para um único padrão forte de framework com muita convenção sobre conficuração, a comunidade php está caminhando para retirar a "mágica" dos códigos focando em padrões de projeto e OO, ter mais de um framework robusto e buscar ter interoperabilidade entre os frameworks. Achei isso muito bom. Pela minha experiência vejo que esses comportamentos "mágicos" dos frameworks costumam embaralhar muito a cabeça dos programadores mais jóvens.
Nesse rumo também vemos que algumas ferramentas estão amadurecendo muito e buscando encontrar o seu foco principal, removendo tudo aquilo que foge a esse foco. O Doctrine 2, por exemplo, que é um projeto que venho estudando, não vem com componente de validação integrado na versão 2. No início fiquei bem chateado com isso pois gostava muito de definir já algumas restrições no yml e já ter essa validação prontinha pra mim. Mais depois consegui perceber que o foco de um ORM não é esse e acho que se cada ferramente se manter no seu foco ela tem mais sucesso de prosperar.
Além disso se examinarmos um pouco mais, como já falei antes, dependendo da arquitetura uns acham melhor ter a validação no modelo, outros no JS, outros na action, etc... Não quero nesse texto ficar discutindo o que é melhor. Eu mesmo acho que isso varia de sistema pra sistema. Mas se percebemos bem essas necessidades vemos que a comunidade se beneficiará muito de ter uma ferramenta de validação de dados como sendo uma biblioteca completamente isolada. Assim ela poderá ser utilizada com qualquer framework e em qualquer camada. Claro que podem ser definidos adaptadores para integrar essa ferramenta em um lugar ou outro, mas o cerne dela deve ser "puro".
O meu pensamento é que se tivermos um arquivo de definição de dados, ele possa ser lido e usado para validar tanto um entidade na camada de modelo quanto um formulário no javascript. Com isso criamos um padrão de formato de dados que pode ser uma ótima contribuição nessa filosofia de ter ferramentas que podem ser usados nos diversos frameworks e camadas.
Algumas coisas eu acho interessante que essa ferramenta tenha:
- Extensibilidade. Esse é um dos pontos principais. Deve ser muito fácil acrescentar um novo tipo de dados e também a classe ou método para validar este tipo. Além disso pode ser útil ter um sistema para registrar listeners que recebem eventos durante o processo de validação extendendo ou sobreescrevendo algum comportamento.
- Facilidade de configuração. Geralmente as mensagens de erro lançados por estas ferramentas já atendem nos casos mais simples. Então é necessário poder trocar as mensagens de erro de cada validação e também poder capturar um erro e sobreescrever a mensagem para casos específicos.
- Trabalhar com objetos/arrays multidimensionais. Esse é um dos pontos onde a maioria das ferramentas do "mercado de software livre" não atende. Acho importanto poder dizer que dentro do dado que eu quero validar pode ter um array e que cada item "nome" desse array pode ter no máximo 15 caracteres.
- Ter um formato que pode ser compartilhado entre servidor e browser. É um fato que o servidor tem que ter validação para termos os dados íntegros. Também é um fato que uma validação no JS melhora a experiência do usuário e diminui o trafego entre cliente e servidor. E muitas vezes a validação nos dois pontos é a mesma. Por isso é importante que a definição da validação possa ser usada pelo browser em uma api semelhante à usada no servidor.
http://groups.google.com/group/json-schema/web/json-schema-proposal-working-draft
http://tools.ietf.org/html/draft-zyp-json-schema-02
Mas com certeza existem outros. O que quero ressaltar aqui é exatamente a necessidade da existência de uma ferramenta como essa. Tendo algo assim separado de frameworks, a comunidade pode juntar esforços para contribuir melhorando cada vez mais os algorítmos de validação e ampliando cada vez mais os tipos de dados disponíveis.
Marcadores:
dados,
json,
json schema,
php,
validação
quarta-feira, 19 de maio de 2010
Alterando uma informação automáticamente antes do update no doctrine 2. (com annotations)
1 - Definimos que a classe ouve os callbacks:
/**
* @Entity
* @HasLifecycleCallbacks
*/
2 - Definimos um método para ser atualizado antes do update.
/**
* @PrePersist @PreUpdate
*/
3 - Lembrar de mandar construir os modelos novamente.
/**
* @Entity
* @HasLifecycleCallbacks
*/
2 - Definimos um método para ser atualizado antes do update.
/**
* @PrePersist @PreUpdate
*/
function updateUpdatedAt() {
$this->updatedAt = new DateTime();
}
3 - Lembrar de mandar construir os modelos novamente.
quarta-feira, 17 de março de 2010
Desacoplando para Testar - Injeção de dependência, inversão de controle e outras palavras bonitas.
Objetivo: Ensinar uma maneira de se estruturar uma aplicação para ela ser mais fácil de testar. Reduzir o tempo dos testes. Simplificar os testes usando mocks de maneiras não intrusivas.
Uns dos pontos principais para o teste de software é:
Não vou entrar de jeito nenhum na discussão do que é mock e o que é um stub. Vou melhorar este termo e traduzir. Digamos que podemos usar um "orêia". Por exemplo, se temos alguma parte do software que acessa um serviço externo, provavelmente isso não vai acontecer em menos de 0,1 segundo e isso é MUITO demorado para um teste. Você não acha? Imagine que ao final de um projeto pequeno para médio você terá em torno de 1000 testes.
Bom, então como fazemos para o nosso teste não demorar tanto? Usamos um mock, quer dizer stub, ou melhor: um "orêia". O nosso orêia vai ser usado no lugar do serviço original e fingir que executa o serviço. Ele pode até retornar algo estático ou que esteja em memória para simular o componente que ele está substituindo.
Por exemplo, digamos que no método inserirTexto() usamos o serviço do yahoo de extrair palavras chaves. Para isso usamos uma classe nossa onde encapsulamos a chamada a esse serviço: a classe PalavraChaveExtractorTabajaras. então teremos algo assim:
Mas como vamos fazer no nosso método inserirTexto para poder dizer que queremos usar o ôreia (Mock)?
Temos que ter um jeito de fazer com que o sistema use o "PalavraChaveExtractorTabajaras" em um ambiente de produção e use o mock nos testes. Já sei, que tal...
Bom, uma outra alternativa é criar um builder para o extractor assim:
E então?
Então o melhor que já encontrei até agora para poder ter estes serviços variando de acordo com ambiente e não precisar saber de nada disso no código que usa o serviço é passar esta responsabilidade para uma outra classe. Esta classe vai gerenciar esse processo de instanciar e entregar os "serviços" de acordo com o ambiente que estamos. Essa classe geralmente é chamada de serviceContainer e já existem algumas implementações interessantes para ela em php, como por exemplo esta:
http://components.symfony-project.org/dependency-injection/trunk/book/03-Service-Container
Apesar de ser muito boa esta implementação, para um projeto que fizemos agora eu preferi escrever algo mais simples e que funcionou tão bem quanto, inclusive com algum ganho de performance, economizando em torno de 1m a cada requisição.
Porque o nome ServiceContainer ?
Porque a classe é um container que contém serviços. Por serviços podemos entender os objetos que implementam serviços como o de palavras chave do nosso exemplo, ou serviços de envio de email, ou até mesmo um repositório de objetos que se responsabiliza por cuidar da persistência de um determinado objeto.
Assim no nosso código podemos ter algo como:
O código fica um pouco mais extenso, mas continua claro. Estou pegando o serviço responsável por processar 'keywords'. Não sei qual serviço ele é, mas tudo bem, polimorfismo é isso aí.
Desta maneira apenas o container centraliza a responsabilidade de definir em qual ambiente estamos. Também ele vai instanciar o objeto de acordo com o ambiente. Um outro benefício que ganhamos disso é que se precisarmos trocar serviço que usamos poderemos intanciar ele e configurar também de outra maneira que não seja a que usavamos antes.
O SimpleServiceContainer
Com base na mesma interface do container do symfony, eu criei esse aí mais simplezinho:
Por padrão ele usa as definições do SimpleServiceContainer_all. Esse eu tenho que criar obrigatóriamente. Depois eu poso extender dele de acordo com o ambiente que eu quiser. Por exemplo, se eu chamo o ambiente de desenvolvimento de test eu posso criar as minhas classe da seguinte maneira:
Assim desta maneira, quando eu crio o meu serviço com:
Eu vou receber o mock para o ambiente de testes e o normal para quando eu não estiver rodando os meus testes. Pode parecer simples, mas faz toda a diferença.
Veja:
Uns dos pontos principais para o teste de software é:
- testar algo coeso, não atrapalhando um teste com funcionalidades de outras bibliotecas
- ser rápido
Não vou entrar de jeito nenhum na discussão do que é mock e o que é um stub. Vou melhorar este termo e traduzir. Digamos que podemos usar um "orêia". Por exemplo, se temos alguma parte do software que acessa um serviço externo, provavelmente isso não vai acontecer em menos de 0,1 segundo e isso é MUITO demorado para um teste. Você não acha? Imagine que ao final de um projeto pequeno para médio você terá em torno de 1000 testes.
Bom, então como fazemos para o nosso teste não demorar tanto? Usamos um mock, quer dizer stub, ou melhor: um "orêia". O nosso orêia vai ser usado no lugar do serviço original e fingir que executa o serviço. Ele pode até retornar algo estático ou que esteja em memória para simular o componente que ele está substituindo.
Por exemplo, digamos que no método inserirTexto() usamos o serviço do yahoo de extrair palavras chaves. Para isso usamos uma classe nossa onde encapsulamos a chamada a esse serviço: a classe PalavraChaveExtractorTabajaras. então teremos algo assim:
function inserirTexto($texto) {
$this->palavrasChaves = PalavraChaveExtractorTabajaras::extrair($texto);
$this->gravarNoBanco();
}
Beleza, então esse método extrai as palavras chaves e grava tudo no banco. Hum... e agora como vamos fazer para chamar esse método de um teste? Imaginem que temos o seguinte teste:function testDeveInserirTextoNoBanco() {
$texto = $this->getTextoFixture();
$conteudo = FabricaDoConteudo::criaUmNovoAiPorFavor();
$conteudo->inserirTexto($texto);
$conteudoNoBanco = RepositorioDoConteudo::lerPeloTexto($texto);
$this->assertTrue((boolean)$conteudoNoBanco,"O conteúdo deve ser gravado no banco.");
}
Mas como vamos fazer no nosso método inserirTexto para poder dizer que queremos usar o ôreia (Mock)?
Temos que ter um jeito de fazer com que o sistema use o "PalavraChaveExtractorTabajaras" em um ambiente de produção e use o mock nos testes. Já sei, que tal...
if($ambiente == 'test') {
$this->palavrasChaves = PalavraChaveExtractorTabajarasMock::extrair($texto);
}
else {
$this->palavrasChaves = PalavraChaveExtractorTabajaras::extrair($texto);
}
Funcionar funciona, mas ainda não é algo muito bonito. Se eu quiser colocar outro ambiente vou ter que adicionar mais um if. Além disso, neste meu método, o ideal é que eu escreva o código de maneira fluente sem ter que me preocupar com testes. Já pensou ter que colocar estes ifs em todo canto. Vai ficar muito sujo.Bom, uma outra alternativa é criar um builder para o extractor assim:
class PalavraChaveExtractorTabajaras {
static function build() {
// retorna de acordo com o ambiente
}
Eu teria que alterar a declaração do método "extrair", de estática para dinâmica, mas funciona melhorzinho, mas ainda assim eu acrescento código a minha classe que não tem a ver com ela. A minha classe não deve saber em que ambiente eu estou. Isso deve ficar para as classes das camadas mais altas, da aplicação ou do framework que eu estiver usando.E então?
Então o melhor que já encontrei até agora para poder ter estes serviços variando de acordo com ambiente e não precisar saber de nada disso no código que usa o serviço é passar esta responsabilidade para uma outra classe. Esta classe vai gerenciar esse processo de instanciar e entregar os "serviços" de acordo com o ambiente que estamos. Essa classe geralmente é chamada de serviceContainer e já existem algumas implementações interessantes para ela em php, como por exemplo esta:
http://components.symfony-project.org/dependency-injection/trunk/book/03-Service-Container
Apesar de ser muito boa esta implementação, para um projeto que fizemos agora eu preferi escrever algo mais simples e que funcionou tão bem quanto, inclusive com algum ganho de performance, economizando em torno de 1m a cada requisição.
Porque o nome ServiceContainer ?
Porque a classe é um container que contém serviços. Por serviços podemos entender os objetos que implementam serviços como o de palavras chave do nosso exemplo, ou serviços de envio de email, ou até mesmo um repositório de objetos que se responsabiliza por cuidar da persistência de um determinado objeto.
Assim no nosso código podemos ter algo como:
function inserirTexto($texto) {
$extrator = SimpleServiceContainer::getInstance()->getService('keywords');
$this->palavrasChaves = $extrator->extrair($texto);
$this->gravarNoBanco();
}O código fica um pouco mais extenso, mas continua claro. Estou pegando o serviço responsável por processar 'keywords'. Não sei qual serviço ele é, mas tudo bem, polimorfismo é isso aí.
Desta maneira apenas o container centraliza a responsabilidade de definir em qual ambiente estamos. Também ele vai instanciar o objeto de acordo com o ambiente. Um outro benefício que ganhamos disso é que se precisarmos trocar serviço que usamos poderemos intanciar ele e configurar também de outra maneira que não seja a que usavamos antes.
O SimpleServiceContainer
Com base na mesma interface do container do symfony, eu criei esse aí mais simplezinho:
class SimpleServiceContainer {
public $parameters;
static $containers = array();
protected $services = array();
protected $env;
/**
* @return SimpleServiceContainer
*/
static function getContainer($env) {
if(!array_key_exists($env,self::$containers)) {
$class = 'SimpleServiceContainer_'.$env;
if(!class_exists($class)) {
$class = 'SimpleServiceContainer_all';
}
self::$containers[$env] = new $class($env);
}
return self::$containers[$env];
}
static function getInstance() {
return self::getContainer(ConfigSystem::get('environment'))
}
function init() {}
protected function __construct($env) {
$this->env = $env;
$this->init();
}
/**
* @return SimpleServiceContainer
*/
function setParameter($name,$value) {
$this->parameters[$name] = $value;
return $this;
}
function getParameter($name) {
return $this->parameters[$name];
}
function getService($serviceName) {
if(!isset($this->services[$serviceName])) {
$this->createService($serviceName);
}
return $this->services[$serviceName];
}
private function createService($serviceName) {
$methodName = 'createService_'.$serviceName;
if(!method_exists($this, $methodName)) {
throw new ServiceBuilderNotConfiguredException(
'this container does not have a builder for the service called "'
.$serviceName."', the method should be called ".$methodName
);
}
$this->services[$serviceName] = $this->$methodName();
}
public function getInstantiatedServiceNames() {
return array_keys($this->services);
}
}
Por padrão ele usa as definições do SimpleServiceContainer_all. Esse eu tenho que criar obrigatóriamente. Depois eu poso extender dele de acordo com o ambiente que eu quiser. Por exemplo, se eu chamo o ambiente de desenvolvimento de test eu posso criar as minhas classe da seguinte maneira:
class SimpleServiceContainer_all extends SimpleServiceContainer {
protected function createService_keywords() {
return PalavraChaveExtractorTabajaras::build();
}
}
class SimpleServiceContainer_test extends SimpleServiceContainer_all {
protected function createService_keywords() {
return PalavraChaveExtractorTabajarasMock::build();
}
}
Assim desta maneira, quando eu crio o meu serviço com:
$extrator = SimpleServiceContainer::getInstance()->getService('keywords');
Eu vou receber o mock para o ambiente de testes e o normal para quando eu não estiver rodando os meus testes. Pode parecer simples, mas faz toda a diferença.
Veja:
- No meu código eu não digo como instanciar o serviço, pois o container funciona como uma factory. Assim qualquer outro componente que tenha a mesma interface pode ser substituido por ambiente eu até em um refactoring futuro sem impacto nas camadas que o usam.
- Isso diminui o acoplamento das classes que usam o serviço com o serviço em sí.
- Quando eu rodo meus testes o sistema automáticamente instancia o serviço de acordo com o ambiente de maneira transparente e me manda um componente que eu posso usar só sabendo a sua interface.
- Isso permite eu usar mocks transparentemente nos testes e também em outros ambientes e até substituí-los futuramente por outras classes quando estas forem desenvolvidas.
- Eu gosto desta implementação pois também instancio os objetos em puro php, sem a necessidade de um xml ou algo assim.
- Os objetos são instanciados apenas quando necessito deles.
- Os objetos dos serviços são por padrão instanciados uma vez só e usados em diferentes partes do sistema. Claro que isso pode não ser o comportamento desejado, mas pela simplicidade da implementação é fácil trocar este comportamento.
Marcadores:
arquitetura para testes,
classe,
injeção de dependência,
php,
service container,
testes
terça-feira, 14 de abril de 2009
Lendo um diretório com SPL
$r = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(dirname(__FILE__)),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($r as $file) {
echo $file.'
';
}
quinta-feira, 5 de março de 2009
Criando uma estrutura de arvore para o jsTree
public function createTreeStructure($dir) {
$ar = array(
'attributes'=>array('id'=>'node-root'),
'data'=>'TESTS',
'children'=>$this->createChildren($dir)
);
return $ar;
}
public function createChildren($dir) {
static $n=0;
$it = new RecursiveDirectoryIterator($dir);
$arAll = array();
foreach($it as $file) {
if(strpos($file,'.svn') !== false) continue;
$ar = array(
'attributes'=>array('id'=>'node-'.$n),
'data'=>$file->getFilename()
);
if($file->isDir()) {
$ar['children'] = $this->createChildren($file);
}
$arAll[] = $ar;
}
return $arAll;
}
Assinar:
Postagens (Atom)