Exemplo prático de Orientação a Objetos (php), diferenças e vantagens em relação à um código Estruturado
#SouDev, focado em #FrontEnd “atualmente”.
Já trabalhei com backend também, desde a modelagem das Entidades, até as interfaces e controllers com ajax.
Uma das minhas maiores dificuldades enquanto estava estudando sobre Orientação a Objetos, era entender de fato o que é um objeto.
E o que a palavra orientação tem a ver com eles. Afinal, o que é Orientação a Objetos?
Não basta sair criando classes, instanciando coisas, ou colocando todo o nosso código estruturado dentro de métodos public function foo()!
Isso não é Orientação a Objetos.
Precisamos ter em mente diversos conceitos de programação, e entender que a granulidade de nossos objetos(SRP), nos guiará a um código melhor, enxuto e menos custoso.
Exemplo Prático
Precisei recentemente, ler diversos feeds de redes sociais, e mostrar os 4~5~7 últimas atividades da timeline/posts/vídeos/fotos…
Eu já tinha cada código isolado mais ou menos pronto:
-> Ler rss do WordPress com php
..
Ainda precisava ler do Flickr, do Facebook, e do Youtube.
Simplesmente, pegar cada rotina dessa e jogar na aplicação, me levaria a ter um código macarrônico/redundante:
Ok, a classe acima “faz” oque eu precisava, mas está incorreta. De uma forma visual e bem simples, note as áreas que circulei.
Uma “god class”, precisa ser urgentemente refatorada!
E ainda faltam o Facebook, o Flickr, o Youtube…
Refatorando
As repetições e inconsistências estão concentradas no loop que itera o objeto, e nas rotinas de testar se já existe ou criar um novo arquivo de cache.
<?php /** * @file View.class.php * @date 2012-05-04 * @author William Moraes */ class View { public function draw( iSource $source, $limit=5 ) { $data = $source->getData(); $i = 0; $html = ''; foreach( $data AS $item ){ if( $i==$limit ) break; if( $source->criteria( $item ) ){ $html .= $source->html( $item ); $i++; } } return $html; } }//View
E o cache, que também não deveria ser responsabilidade de cada “método” twitter(), wordpress()..
<?php /** * @file Cache.class.php * @date 2012-05-04 * @author William Moraes */ class Cache { private $path = '../cache/'; private $view; public function setPath( $path ) { $this->path = $path; } public function __construct( iSource $source, View $view ) { $this->source = $source; $this->view = $view; } public function isCached( $url, $file, $limit=5 ) { $this->source->setURL( $url ); $cached_file = $this->path.$file; if( is_file( $cached_file ) ) return file_get_contents( $cached_file ); $contents = $this->view->draw( $this->source, $limit ); file_put_contents( $cached_file, trim( $contents ) ); return $contents; } }//Cache
Note que para “dar certo” e fazer sentido, eu criei um contrato: A interface iSource:
<?php /** * @file iSource.class.php * @date 2012-05-04 * @author William Moraes */ interface iSource { public function setURL( $url ); public function html( $data ); public function getData(); public function criteria( $item ); }
E então agora, cada “objeto”, afinal Twitter é uma coisa, Facebook é uma outra, e Flickr é outra ainda completamente diferente. Logo, objetos diferentes, podem implementar esse contrato, e garantir compatibilidade com as classes Cache e View.
Timeline do Twitter
<?php include_once 'iSource.class.php'; /** * @file Twitter.class.php * @date 2012-05-04 * @author William Moraes * @usage $twitter = new Twitter( $reader ); $twitter->setURL( 'http://twitter.com/statuses/user_timeline/locaweb.json?count=5' ); echo $view->draw( $twitter ); */ class Twitter implements iSource { private $reader; private $url; public function __construct( Reader $reader ) { $this->reader = $reader; } public function setURL( $url ) { $this->url = $url; } /** * @function html */ public function html( $item ) { $html = "\t".'<li><img src="'.$item->user->profile_image_url.'" alt="'.$item->user->screen_name.'" title="'.$item->user->screen_name.'" /> '; $html .= $this->makeLinks( $item->text ); $html .= '</li>'.PHP_EOL; return $html; } /** * @function getData */ public function getData() { return $this->reader->getJSON( $this->url ); } /** * @function criteria */ public function criteria( $item ) { return true; } /** * @function makeLinks */ private function makeLinks( $text ) { return preg_replace( Array( '/(http:\/\/[\w\.\/]+)/', '/[^\w]@([\w]+)/', '/(#[\w]+)/' ), Array( '<a href="$1" title="$1" rel="external">$1</a>', '<a href="http://twitter.com/#!/$1" title="$1" rel="external">@$1</a>', '<a href="http://twitter.com/#!/search/$1" title="$1" rel="external">$1</a>' ), $text ); } }//Twitter
Note a simplicidade do método Twitter:html();
Diferente do método LeitorRss:twitter(), agora só oque “realmente” interessa está ali. Afinal, oque mudava entre: LeitorRss:twitter() e LeitorRss:wordpress(), era a forma com que eu montava cada LI, e não o loop em si.
Timeline do Facebook
É simples também agora, plugar a class Facebook para ler a timeline:
<?php include_once 'iSource.class.php'; /** * @file Facebook.class.php * @date 2012-05-04 * @author William Moraes * @usage $facebook = new Facebook( $reader ); $facebook->setURL( 'https://graph.facebook.com/locaweb/posts&access_token=SEU_ACCESS_TOKEN&method=get' ); echo $view->draw( $facebook ); */ class Facebook implements iSource { private $reader; private $url; public function __construct( Reader $reader ) { $this->reader = $reader; } public function setURL( $url ) { $this->url = $url; } /** * @function html */ public function html( $item ) { $ts = strtotime( $item->created_time ); $date = date( 'd/m/Y à\s H:m', $ts ); $pieces = explode('_', $item->id ); return '<li>'.substr( $item->message, 0, 140 ).'.. postado dia <a href="https://www.facebook.com/locaweb/posts/'.$pieces[1].'" rel="external">'.$date.'</a></li>'; } /** * @function getData */ public function getData() { $data = $this->reader->getJSON( $this->url ); return $data->data; } /** * @function criteria */ public function criteria( $item ) { return isset( $item->message ); } }//Facebook
Usando:
<?php header ('Content-type: text/html; charset=utf-8'); date_default_timezone_set('America/Sao_Paulo'); include 'class/Reader.class.php'; include 'class/Twitter.class.php'; include 'class/Facebook.class.php'; include 'class/Wordpress.class.php'; include 'class/Statusblog.class.php'; include 'class/Flickr.class.php'; include 'class/View.class.php'; include 'class/Cache.class.php'; $reader = new Reader(); $view = new View(); $twitter = new Twitter( $reader ); $cache = new Cache( $twitter, $view ); echo $cache->isCached( 'http://twitter.com/statuses/user_timeline/locaweb.json?count=10', 'twitter-timeline.txt' ); $facebook = new Facebook( $reader ); $cache = new Cache( $facebook, $view ); echo $cache->isCached( 'https://graph.facebook.com/locaweb/posts&access_token=XXX&method=get', 'facebook-timeline.txt' ); $wordpress = new WordPress( $reader ); $cache = new Cache( $wordpress, $view ); echo $cache->isCached( 'http://feeds.feedburner.com/bloglocaweb', 'rss_blog.txt' ); /* cache status blog */ $statusblog = new Statusblog( $reader ); $cache = new Cache( $statusblog, $view ); echo $cache->isCached( 'http://statusblog.locaweb.com.br/feed', 'rss_statusblog.txt', 7 );
E enfim, o último participante:
Class Reader
Eu não vi sentido em colocar o cURL, o json_decode(), o file_get_contents()… dentro de cada uma das classes que representam as Mídias Sociais.
Por esse motivo, criei um outro objeto, especializado em “ler”, seja da forma que for, e retornar dados em forma de StandardClass.
<?php /** * @file Reader.class.php * @date 2012-05-04 * @author William Moraes */ class Reader { public function getXML( $url ) { $file = $this->curlFile( $url ); return simplexml_load_string( $file, 'SimpleXMLElement', LIBXML_NOCDATA ); } public function getJSON( $url ) { $result = $this->curlFile( $url ); return json_decode( $result ); } private function getContents( $url ) { return file_get_contents( $url ); } private function curlFile($url, $timeout=0) { $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $url ); //curl_setopt ($ch, CURLOPT_HEADER, 1); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $timeout ); $content = curl_exec( $ch ); curl_close( $ch ); return $content; } }//Reader
Eu disse não à herança
Geralmente, temos uma certa tendência a querer colocar esses métodos em uma class Abstrata, e então Twitter, Facebook, WordPress, extenderem essa class.
Porém, do ponto de vista da Orientação a Objetos, isso não é bacana. Afinal, nossos objetos usam pq precisam os métodos curlFile, getJSON, mas não são isso.
O objeto Twitter não deve ter a responsabilidade de ir buscar o jSON, nem de fazer o cache, nem de interar um loop.
Apenas deve saber oque fazer com os dados já mastigados, e assim por diante.
Vantagem
Uma vantagem, é que essa class Reader pode ser utilizada em outro projeto, sozinha. Desacoplei o código dela do restante da aplicação.
Extends pode e deve ser usado, mas na hora correta
Dentro deste mesmo projeto, eu precisava retornar dados, de mais de um blog WordPress. E para cada blog, havia um html diferente. Digamos que uma “especialização” do que a class WordPress fazia. Logo, aqui sim, coube usar herança:
<?php include_once 'iSource.class.php'; /** * @file Statusblog.class.php * @date 2012-05-04 * @author William Moraes * @usage $Statusblog = new Statusblog( $reader ); $Statusblog->setURL( 'http://feeds.feedburner.com/BlogLocaweb14elwSalvador' ); echo $view->draw( $Statusblog ); */ class Statusblog extends WordPress { /** * @overwrite parent::html() * @function html */ public function html( $item ) { return '<li> <a href="'.$item->link.'">'.$item->title.'</a> <div class="category">Categoria: '.$item->category.'</div> </li>'.PHP_EOL; } }//Facebook
Concluindo
Agora sim, eu tenho um projeto Orientado a Objetos. Um objeto conversando com outro, poucas dependências sólidas e bem resolvidas.
É isso. O que acham ?
criticas? sugestões? dúvidas?