Ir para conteúdo

POWERED BY:

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

lorena85

SOLID – Princípio da Responsabilidade Única

Recommended Posts

Conceitualmente, tanto faz, porém se Aplicação definir os Observadores, toda vez que aquele mesmo Subject precisar ser usado, você vai ter de especificar um ou mais observadores manualmente e já sabe: Don't Repeat Yourself.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Muito bom, valeu pela ajuda @Evandro Oliveira

 

agora não seria melhor fazer assim:


public function offsetSet($index, Logger $newval)
{
    parent::offsetSet($index, $newval);
}
evitando implementações!

 

 

 

 

Seria. Se o PHP permitisse sobrecarga de métodos. Como a interface ArrayAccess já define a assinatura de offsetSet, infelizmente esta abordagem não é possível.

 

Como assim o retorno dos métodos não é tipado em php??

Você citou que a classe ArrayObject garanti que o retorno do método vai retornar como objeto Logger, ela trata isso na entrada do método "offsetSet" ou na saída no método "offsetGet" essa tipagem??

Mais ou menos isso aqui que ele falou, ó:

A responsabilidade da classe é tratar os tipos na entrada do método, a do cliente é tratar os tipos retornados pelo método. Mesmo uma método "Set" pode retornar um tipo Exception ou uma string com erro conforme o caso.

 

O php não induz o tipo retornado, por exemplo a classe offsetGet pode retornar o índice do valor ou um tipo Null.

Quis dizer que você não pode forçar um tipo de retorno como é possível fazer com um tipo de argumento. Existe [inline]function (TipoDoArgumento $argumento) {}[/inline] mas não existe [inline]TipoDoRetorno function () {}[/inline].

De quem seria a responsabilidade de definir os observadores?

 

Por exemplo, o StockObserver relataria as vendas dos produtos, mas é a classe de venda que falaria: esses caras vão me observar ou eu definiria na hora de instancia-la ? Porque essa classe pode ter um StockObserver e um LoggerObserver.

 

Estou me confundindo muito?

Do meu entendimento de Observer, esta pergunta deveria ser feita da seguinte forma:

De quem seria a responsabilidade de informar ao Logger quais os assuntos que ele deve observar.

Afinal, um observador não é definido. Ele já existe e - literalmente - observa determinados assuntos aguardando por mudanças e toma as devidas "providências" quando notificado a respeito das mesmas.

 

Pra quem entende pelo menos um básico de UML, o diagrama do @Henrique Barcelos matou a pau.

 

Caros, venho notado uma série de possíveis implementações e, sinceramente, adorei o conteúdo do tópico. É, sem dúvida, um dos de mais alto nível técnico que pude presenciar recentemente. Isso me anima e me impede que bizarrices como esta me afastem em definitivo do fórum.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Do meu entendimento de Observer, esta pergunta deveria ser feita da seguinte forma:Afinal, um observador não é definido. Ele já existe e - literalmente - observa determinados assuntos aguardando por mudanças e toma as devidas "providências" quando notificado a respeito das mesmas.

 

Então eu poderia, por exemplo, chamar o attach() dentro do construtor da classe de usuario?

 

Escrevi um exemplo para treinar mesmo, esta meio porco devido a falta de tempo, mas se alguém puder olhar e comentar eu agradeceria.

 

Logger.class.php

<?php
interface Logger {
    public function write($message);
}

 

Observer.class.php

<?php
interface Observer {
    public function update($message);
}

 

AbstractLogger.class.php

<?php
abstract class AbstractLogger implements Observer, Logger {
    public abstract function update($message);
}

 

Subject.class.php

<?php
interface Subject {
    public function attach(Observer $observer);
    public function detach(Observer $observer);
    public function notify($message);
}

 

AbstractSubject.class.php

<?php
abstract class AbstractSubject implements Subject {
    private $observers = array();

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function detach(Observer $observer)
    {
        $key = array_search($observer, $this->observers, true);
        if($key !== false){
            unset($this->observers[$key]);
        }
    }

    public function notify($message)
    {
        if(count($this->observers) > 0){
            foreach($this->observers as $observer){
                $observer->update($message);
            }
        }
    }
}

 

FileLogger.class.php

<?php
class FileLogger extends AbstractLogger{
    public function write($message)
    {

    }

    public function update($message)
    {
        $fp = fopen('logs.log','a+');
        fwrite($fp, $message . PHP_EOL);
        fclose($fp);
    }
}

 

ConsoleLogger.class.php

<?php
class ConsoleLogger extends AbstractLogger{
    public function write($message)
    {
        echo $message . '<br>';
    }
    public function update($message)
    {
        $this->write($message);
    }
}

 

Usuario.class.php

<?php
class Usuario extends AbstractSubject{
    private $nome;
    private $usuario;
    private $email;
    private $senha;

    function __construct($nome, $email, $usuario, $senha)
    {
        $this->email = $email;
        $this->nome = $nome;
        $this->senha = $senha;
        $this->usuario = $usuario;
    }


    /**
     * @param mixed $email
     */
    public function setEmail($email)
    {
        $this->notify('Usuario ' . $this->getUsuario() . ' mudou seu email de ' . $this->getEmail() . ' para ' . $email);
        $this->email = $email;
    }

    /**
     * @return mixed
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * @param mixed $usuario
     */
    public function setUsuario($usuario)
    {
        $this->notify('Usuario ' . $this->getUsuario() . ' mudou seu nome de usuario de ' . $this->getUsuario() . ' para ' . $usuario);
        $this->usuario = $usuario;
    }

    /**
     * @return mixed
     */
    public function getUsuario()
    {
        return $this->usuario;
    }



    /**
     * @param mixed $nome
     */
    public function setNome($nome)
    {
        $this->notify('Usuario ' . $this->getUsuario() . ' mudou seu nome de ' . $this->getNome() . ' para ' . $nome);
        $this->nome = $nome;
    }

    /**
     * @return mixed
     */
    public function getNome()
    {
        return $this->nome;
    }

    /**
     * @param mixed $senha
     */
    public function setSenha($senha)
    {
        $this->notify('Usuario ' . $this->getUsuario() . ' mudou sua senha');
        $this->senha = $senha;
    }

    /**
     * @return mixed
     */
    public function getSenha()
    {
        return $this->senha;
    }



    public function __toString()
    {
        return (string) $this->getNome();
    }


}

 

No arquivo de teste, estou fazendo:

<?php
$user = new Usuario('Raul', 'raul@gmail.com', 'raul3k', '123');
$user->attach(new FileLogger());
$user->attach(new ConsoleLogger());
$user->setSenha('raul');
$user->setEmail('raul2@gmail.com');

A saida do log.txt e na tela ficou:

 

Usuario raul3k mudou sua senha

Usuario raul3k mudou seu email de raul@gmail.com para raul2@gmail.com

 

Alguma consideração, xingamento, elogio? Rs

Compartilhar este post


Link para o post
Compartilhar em outros sites

Então eu poderia, por exemplo, chamar o attach() dentro do construtor da classe de usuario?

Poder vc pode, mas aí você gera acoplamento, uma vez que a classe Usuario teria que conhecer algum Observer para criá-lo. Caso vc queira trocar esse Observer "padrão", vai ter que mexer na classe e isso não é o ideal.

 

Quanto a sua implementação, só altere o trecho abaixo:

<?php 
abstract class AbstractLogger implements Observer, Logger { 
    public function update($message) {
        $this->write($message);
    }
}

class FileLogger extends AbstractLogger {
    public function write($message)
    {
        $fp = fopen('logs.log','a+');
        fwrite($fp, $message . PHP_EOL);
        fclose($fp);
    }
}

class ConsoleLogger extends AbstractLogger{
    public function write($message)
    {
        echo $message . '<br>';
    }
}

Edit: Não tem muito a ver com o tópico, mas sugiro que você dê uma olhada na PSR-0 para usar a convenção de nomenclatura de classes e arquivos.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Outra forma também de fazer isso seria com um Mediator funcionando como uma "central" de eventos. Uma das vantagens é que você pode ter eventos variados.

 

A grande diferença dele é que ao invés de existir uma comunicação direta entre observer e subject, existe um "cara" no meio tomando conta, o que diminui o acoplamento entre os dois.

 

O mesmo código anterior só que agora usando um mediator:

<?php

class Mediator
{
    private $listeners = [];

    public function addListener($event, callable $listener)
    {
        if (! isset($this->listeners[$event]) {
            $this->listeners[$event] = [];
        }

        $this->listeners[$event][] = $listener;
    }

    public function dispatch($event, array $params = [])
    {
        if (! isset($this->listeners[$event]) {
            return;
        }

        foreach ($this->listeners[$event] $listeners) {
            foreach ($listeners as $listener) {
                call_user_func_array($listener, $params);
            }
        }
    }
}

class Usuario
{
    private $mediator;
    private $nome;
    private $usuario;
    private $email;
    private $senha;

    public function __construct(Mediator $mediator)
    {
        $this->mediator = $mediator;
    }

    public function setEmail($email)
    {
        $this->email = $email;
        $this->mediator->dispatch('user.email_changed', [$this]);
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function setUsuario($usuario)
    {
        $this->usuario = $usuario;
        $this->mediator->dispatch('user.username_changed', [$this]);
    }

    public function getUsuario()
    {
        return $this->usuario;
    }

    public function setNome($nome)
    {
        $this->nome = $nome;
        $this->mediator->dispatch('user.name_changed', [$this]);
    }

    public function getNome()
    {
        return $this->nome;
    }

    public function setSenha($senha)
    {
        $this->senha = $senha;
        $this->mediator->dispatch('user.password_changed', [$this]);
    }

    public function getSenha()
    {
        return $this->senha;
    }

    public function __toString()
    {
        return (string) $this->getNome();
    }
}

// Ao contrário da implementação com o observer, o logger nesse caso não importa,
// pois ele não dependerá mais do observer (e nem saberá que ele está sendo usado através de um mediator), já que um mediador vai controlá-lo.
// Esse é o porquê do mediator diminuir o acoplamento
$logger = new Logger(); 

// Criamos o nosso mediator, que terá os listeners que depois serão despachados
// por quem quer que seja
$mediator = new Mediator();

// Aqui nós declaramos os "listeners" para cada evento que quisermos controlar.
// Nesse caso, são usadas funções anônimas, mas poderia ser um método de classe ou
// o que você preferir, dependendo da sua forma de implementar o mediator.
$mediator->addListener('user.email_changed', function (Usuario $user) use ($logger) {
    $logger->log('O usuário mudou o email dele para' . $user->getEmail());
});
$mediator->addListener('user.username_changed', function (Usuario $user) use ($logger) {
    $logger->log('O usuário mudou o nome de usuário dele para' . $user->getUsuario());
});
$mediator->addListener('user.name_changed', function (Usuario $user) use ($logger) {
    $logger->log('O usuário mudou o nome dele para' . $user->getNome());
});
$mediator->addListener('user.password_changed', function (Usuario $user) use ($logger) {
    $logger->log('O usuário mudou a senha dele para' . $user->getSenha());
});

// Como o usuário é quem despacha os eventos, nós precisamos passar o mediator para ele
$user = new Usuario($mediator);

$user->setSenha('raul'); // realizará a troca de senha e despachará o evento "user.password_changed"
$user->setEmail('raul2@gmail.com'); // realizará a troca de email e despachará o evento "user.email_changed"
Como vimos aqui, o objetivo é o mesmo: poder alterar comportamentos do sistema em runtime, aumentando a flexibilidade.

Se você estiver interessado, existem mediators prontos e robustos como o Symfony Event Dispatcher e o Zend\EventManager, que implementam coisas interessantes, como prioridades para listeners.

 

Concluindo, você vai ver muito mais implementações de mediators do que de observers, principalmente em JavaScript e nos frameworks PHP modernos. Não há uma solução melhor ou pior, só para lembrar.

 

Edit: achei um vídeo de 2 minutos que explica Mediators perfeitamente:

Compartilhar este post


Link para o post
Compartilhar em outros sites

Seguindo a proposta da PSR-3, eu implementei rapidinho aqui este repositório:

https://github.com/henriquejpb/PSRLogger_Example

 

Com isso aqui:

<?php
require_once 'src/autoload.php';

use Psr\Log\FileLogger;
use Application\Service\User as UserService;
use Application\Entity\User;

$logger = new FileLogger("log.log");
$service = new UserService($logger);

$userHenrique = new User('Henrique');
$userJose = new User('José');

$service->addUser($userHenrique);
$service->addUser($userJose);
$service->removeUser($userJose);

Obtenho uma saída de log assim:

[2013-11-01 20:52:57] Info: Started user service
[2013-11-01 20:52:57] Info: Added user Henrique
[2013-11-01 20:52:57] Info: Added user José
[2013-11-01 20:52:57] Info: Removed user José

Compartilhar este post


Link para o post
Compartilhar em outros sites

Exatamente, o SRP pode ser violado numa classe com um método, assim como pode ser seguido numa classe com 30 métodos.

 

Um exemplo do mundo real de violação de SRP é quando há implementação de ActiveRecord junto com validação/filtro/eventos/logs, as famigeradas "Models" de alguns frameworks.

 

Enrico,

Em vários fóruns internacionais, e até mesmo aqui no imasters, essa é uma questão que sempre me chamou muito a atenção:

a validação e sanitização dos dados devem ficar na p**ra da camada controller ou na classe? Li discussões grandes mesmo. Coisa de xiitas contra sunitas. Na dúvida tenho colocado tudo na classe, mas percebo que algumas vezes tenho métodos de validação muito semelhantes em duas classes diferentes - o que me deixa furioso porque se trata de uma óbvia infração ao DRY. Se eu fizesse um arquivo de validação separado e chamasse ele no controller, passando os dados pra classe, certamente meu código ficaria menos...repetitivo.

 

Essa questão toda me leva a uma outra. Eu ia abrir um tópico diferente, mas é basicamente SOLID, então acho que é válido mantermos o tópico aberto:

 

Nas respostas anteriores ficou claro que os princípios do SOLID estão relacionados às classes e não aos métodos, certo?

Pergunto, porque tenho uma classe PDO/Database, com um método _connectWithMySQL($sql, $arg).

 

Essa classe recebe o parâmetro $sql que vem das demais classes Items, Users, etc (se é select, update, insert, drop), prepara e executa o comando. O parâmetro $arg determina o tipo de retorno que a classe que envia a solicitação vai ter (se é nenhum, FETCH_ALL, FETCH_ASSOC, COUNT, etc). Fiz um switch para todas as possibilidades que foram aparecendo. E sempre que surgiu uma necessidade nova de interação com o banco, apenas precisei adicionar um novo case no switch.

 

Essa gambiarra tem me ajudado pra car... ao longo do tempo, mas sempre fiquei na dúvida se esse argumento não era uma infração ao conceito do SOLID. Como se trata de um método, não me parece, agora. Ou é?

Compartilhar este post


Link para o post
Compartilhar em outros sites

a validação e sanitização dos dados devem ficar na p**ra da camada controller ou na classe?

 

Model não significa uma classe. É daí que surge toda a confusão feita pelos frameworks inspirados pelo Rails que basicamente incentivam uma godclass com todos os comportamentos do sistema tornando a manutenção um inferno. O problema deles é que eles usam o ActiveRecord (que já é uma "leve" violação do SRP) para coisas além da persistência de dados.

 

O controller serve apenas para converter a requisição HTTP que o usuário enviou para uma resposta HTTP.

 

Nas respostas anteriores ficou claro que os princípios do SOLID estão relacionados às classes e não aos métodos, certo?

 

Uma classe é constituída do quê? quem define os comportamentos delas?

 

Pergunto, porque tenho uma classe PDO/Database, com um método _connectWithMySQL($sql, $arg).

 

Bem, o ponto da orientação a objetos é justamente permitir um alto nível de abstração. Fazendo um método desses, você está criando um acoplamento ao mysql. Isso se relaciona com o OCP e com o DIP.

 

Use polimorfismo para solucionar isso:

interface Database {
    // Métodos que um SGBD precisa
}

class MySql implements Database {}
class SQLite implements Database {}
class PostgreSQL implements Database {}

class DatabaseConnection {
    private $database;

    public function __construct(Database $database) {
        $this->database = $database;
    }
}

Ou simplesmente use um ORM como o Doctrine.

 

Essa classe recebe o parâmetro $sql que vem das demais classes Items, Users, etc (se é select, update, insert, drop), prepara e executa o comando. O parâmetro $arg determina o tipo de retorno que a classe que envia a solicitação vai ter (se é nenhum, FETCH_ALL, FETCH_ASSOC, COUNT, etc).

 

Isso dá um cheiro de Primitive Obsession.

 

Fiz um switch para todas as possibilidades que foram aparecendo. E sempre que surgiu uma necessidade nova de interação com o banco, apenas precisei adicionar um novo case no switch.

 

:( . Essa é uma violação ao OCP, um dos princípios S.O.L.I.D., que diz que nossas classes devem ser abertas à extensão, mas fechadas à edição.

 

Para resolver esse tipo de problema, novamente, use polimorfismo. Se você pensar em interfaces e abstração, você já começa a ter um bom design de código.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Essa é uma violação ao OCP, um dos princípios S.O.L.I.D., que diz que nossas classes devem ser abertas à extensão, mas fechadas à edição.

 

Mas é que meu sistema eu havia desenvolvido em programação procedural e tinha uns gaps de segurança e umas gambiarras absurdas e complexas. Mas funcionava (funciona, ainda) perfeitamente desde 2010. Mas resolvi fazer uma coisa menos amadora e enfiei a cara nos livros e tutoriais de OO (não apenas PHPOO). E estou reconstruindo tudo.

Como o PHP.net já deixou claro que vai descontinuar o mysql_*, passei a adotar o PDO. Mas ainda é algo novo pra mim. Então na verdade, a classe permanece "aberta à edição", porque ainda não terminei ela.

Ainda assim, como sempre adiciono itens no switch, não seria uma extensão?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Ainda assim, como sempre adiciono itens no switch, não seria uma extensão?

 

Não, isso é uma edição. Você tem que abrir a classe e editá-la para adicionar um novo tipo de comportamento.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Eu já tive muito dúvida a esse respeito também. Não existia um consenso, um padrão a ser seguido, os esquerdistas defendiam que toda a lógica deveria estar no Controller e o direitistas que toda verificação deveria estar na Model, que o Controller deveria ser magro (Slim COntroller).

 

De saco cheio dessa indecisão, assumi o que era mais lógico de um ponto de vista mais amplo: Tem coisas que devem ser verificadas no Controller e tem coisas que devem ser verificadas na Model. Ponto!

 

O Controller recebe a requisição e os dados do usuário. Antes de serem usados, os dados precisam ser limpos, sanitizados, filtrados e validados.

 

Não acho certo, por exemplo, um método que visa buscar um usuário pelo seu ID, ser responsável por remover espaços, barras, aspas e etc. O motivo de existência dele é listar usuário por um ID e IDs sempre são numéricos, então, pra mim, essa é a ÚNICA coisa que esse método deveria verificar.

 

 

class Controller {
 
    public function userProfile() {
 
        $data = someSanitizingFunction( $_POST );
 
        try {
 
            $user = new User( new ConnectionClass( /** ... */ ) );
 
            try {
 
                $userData = $user -> getUserById( (int) $data['id'] ) -> fetch();
 
           } catch( LogicException ) {
 
                // Dom something!
           }
 
        } catch( ConnectionException $e ) {
 
            // Do something!
        }
    }
}
 
class User {
 
    private $conn;
 
    public function __construct( Connection $connection ) {
 
        $this -> conn =& $connection;
    }
 
    public function getUserById( $id ) {
 
        if( ! is_int( $id ) || $id == 0 ) {
            throw new LogicException( 'User\'s IDs must be a non-zero number' );
        }
 
        return $this -> conn -> query(
 
            sprintf( 'SELECT * FROM `users` WHERE id = %s', $id )
        );
    }
}

Escrevi de cabeça, lógico e evidente que não é exatamente assim que se faz u se deve fazer mas é só pra ilustrar mesmo.

 

Aqui o is_int() vai verificar se não aquele cast forçado para int não foi esquecido pelo desenvolvedor (acontece) e nem que aquela conversão resulte em zero, caso algum espírito de porco passe uma string no lugar de um número.

 

A "Model" não deve conhecer funções / método de classe de tratamento de strings, filtragem de resultados, validação de lógica... Isso é tarefa de quem recebe os dados, quem controla o fluxo mas mais importante a forma como esses dados são utilizados.

Compartilhar este post


Link para o post
Compartilhar em outros sites

A "Model" não deve conhecer funções / método de classe de tratamento de strings, filtragem de resultados, validação de lógica... Isso é tarefa de quem recebe os dados, quem controla o fluxo mas mais importante a forma como esses dados são utilizados.

Há controvérsias...

 

A lógica da aplicação/regras de negócio devem ir obrigatoriamente na Model. Filtragem de resultados é uma regra de negócio, logo, deve ir na model.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Quando eu disse filtrados, me referia aos dados vindos do usuário.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Como já foi dito, controllers validam o que você pode fazer. Como você pode e o que você deve fazer é responsabilidade das models.

 

Principalmente num ambiente web, controllers não devem conhecer as models. A única coisa que controllers sabem é quais models associar à view que será retornada.

 

View usuario.html, aqui está o modelo que você deve apresentar:

new User($db, $request->queryString['id']);

Percebam que, num ambiente bem modelado, [inline]User::__construct[/inline] dispara uma exception quando algum dos seus argumentos não é esperado. A controller não precisa validar [inline]$request->queryString[id][/inline], a controller precisa retornar um erro caso o mesmo exista. Fim.

 

Filtragem e sanitização podem (o que não quer dizer que devam) ficar a critério dos componentes que vão processar a requisição. Se vai ser dentro ou fora da controller, vai de cada implementação. Quem decide se o parâmetro atende ou não ao esperado é a model.

Compartilhar este post


Link para o post
Compartilhar em outros sites

Principalmente num ambiente web, controllers não devem conhecer as models. A única coisa que controllers sabem é quais models associar à view que será retornada.

Novamente, há controvérsias...

Essa é só uma possível implementação.

 

Uma pergunta sobre essa implentação inclusive me surgiu agora: como disparar eventos na Model que não resultam em uma atualização da View?

 

Eu costumo utilizar Controllers como intermediários entre Views e Models, recebem a requisição, obtêm os dados das Models e "configuram" a View para exibir esses dados.

 

Ex.:

// Num controller:
public function indexAction(Request $req) {
    try {
        // Exibe 30 resultados da página atual...
        $data = $this->getModel('User')->getPage((int) $req->getQuery('pg'), 30);
    } catch (PageNumberOutOfRangeException $e) {
        return new ResponseRedirect('?pg=1'); // Redireciona, mas também poderia exibir uma página de erro...
    }

    $view = new TemplateView('app.user.index');
    $view->userList = $data;

    $res = new Response(Response::OK);
    $res->appendBody($view);
    return $res;
}

Compartilhar este post


Link para o post
Compartilhar em outros sites

×

Informação importante

Ao usar o fórum, você concorda com nossos Termos e condições.