uma classe só pode ter uma única razão para mudar
class Report {
public function fetch();
public function asHtml();
public function asJson();
}
Especialize suas classes dividindo-as até que tenham somente um objetivo
class Report {
public function fetch();
}
class ReportWriter {
public function asHtml(Report $report);
public function asJson(Report $report);
}
entidades devem ser abertas para extensão mas fechadas para modificação
É possível implementar uma nova funcionalidade em seu sistema somente adicionando novas classes ao invés de modificar as existentes?
class ReportExporter {
public function toCsv(Report $report) {
$tempFile = $this->createNewFile();
$handler = fopen($tempFile, 'w');
foreach ($report as $row) {
fputcsv($handler, $row);
}
fclose($handler);
return $tempFile;
}
public function toXml(Report $report) { /* ... */ }
}
Crie estruturas polimórficas.
Em PHP, utilize interfaces!
class ReportExporter {
public function export(Report $report, WriterInterface $writer) {
$writer->init();
foreach ($report as $row) {
$writer->write($row);
}
return $writer->finish();
}
}
interface WriterInterface {
public function init();
public function write($row);
public function finish();
}
class CsvWriter implements WriterInterface { /* ... */ }
class XmlWriter implements WriterInterface { /* ... */ }
// Em meu sistema, posso trocar o Writer sem medo!
$reportExporter->export($report, new CsvWriter());
$reportExporter->export($report, new XmlWriter());
objetos em um programa deveriam ser substituíveis por instâncias de seus subtipos sem alterar a exatidão do programa
Meu sistema precisa funcionar corretamente ao trocar o tipo do Writer
$writer = new CsvWriter();
$writer->write(/* ... */);
$writer = new FileWriter();
$writer->write(/* ... */);
Pré-condições não podem ser fortalecidas em uma subclasse
class DatabaseAdapter {
public function query(Statement $statement);
}
class MyPdoStatement extends Statement {
/* ... */
}
class PdoDatabaseAdapter extends DatabaseAdapter {
// Errado!
public function query(MyPdoStatement $statement);
}
Pós-condições não podem ser enfraquecidas em uma subclasse
class Retangulo {
public function setAltura($altura) {
$this->altura = (float) $altura;
}
public function setLargura($largura) {
$this->largura = (float) $largura;
}
public function getAltura() {
return $this->altura;
}
public function getLargura() {
return $this->largura;
}
}
function area(Retangulo $retangulo) {
return $retangulo->getAltura() * $retangulo->getLargura();
}
$retangulo = new Retangulo();
$retangulo->setAltura(5);
$retangulo->setLargura(4);
if (area($retangulo) != 20) {
throw new UnexpectedValueException("Algo não está certo");
}
class Quadrado extends Retangulo {
public function setAltura($altura) {
$this->altura = (float) $altura;
$this->largura = (float) $altura;
}
public function setLargura($largura) {
$this->altura = (float) $largura;
$this->largura = (float) $largura;
}
}
O que acontece ao executar o mesmo código do cálculo de área, mas passando um objeto Quadrado
?
function area(Retangulo $retangulo) {
return $retangulo->getAltura() * $retangulo->getLargura();
}
$quadrado = new Quadrado();
$quadrado->setAltura(5);
$quadrado->setLargura(4);
if (area($quadrado) != 20) {
throw new UnexpectedValueException("Algo não está certo");
}
Herança não deve ser utilizada quando uma subclasse restringe a liberdade imposta na classe principal, mas sim quando adiciona mais detalhes.
Ela só é necessária quando há similaridade de comportamento. Senão, use composição.
é preferível ter várias interfaces mais específicas do que uma genérica
interface AveInterface {
public function bicar();
public function voar();
}
Mas nem toda ave voa!
class Pinguim implements AveInterface {
public function bicar() { /* ... */ }
public function voar() {
return false;
}
}
Segrege suas interfaces ao invés de criar uma que atenda todos os casos
interface AveInterface {
public function bicar();
}
interface VoadorInterface {
public function voar();
}
class Papagaio implements AveInterface, VoadorInterface { /* ... */ }
class Pinguim implements AveInterface { /* ... */ }
class Esquilo implements VoadorInterface { /* ... */ }
dependa de abstrações (interfaces) ao invés de classes concretas
PSR-15
php-fig.org/psr/psr-15
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\JsonResponse;
class ActionHandler implements RequestHandlerInterface {
public function handle(ServerRequestInterface $request) : ResponseInterface {
return new JsonResponse(['status' => true]);
}
}
Depender de interfaces nos fornece grande flexibilidade para mudar partes do sistema sem afetar o funcionamento do todo
Design by contract
public function minify(array $files) {
foreach ($files as $file) {
if (!is_file($file)) {
throw new RuntimeException("{$file} não é válido");
}
switch (pathinfo($file, PATHINFO_EXTENSION)) {
case 'js':
// ...
break;
case 'html':
// ...
break;
case 'css':
// ...
break;
}
}
}
public function minify(array $files) {
foreach ($files as $file) {
$this->parseFile($file);
}
}
protected function parseFile($file) {
if (!is_file($file)) {
throw new RuntimeException("{$file} não é válido");
}
$this->getParserForExtension(pathinfo($file, PATHINFO_EXTENSION))
->parse($file);
}
protected function getParserForExtension($extension) {
if (isset($this->parsersByExtension[$extension])) {
return $this->parsersByExtension[$extension];
}
throw new DomainException("Nenhum minificador para {$extension} foi encontrado");
}
public function login($username, $password) {
$row = $this->dbh->fetch('SELECT * FROM user WHERE username = ?', $username);
if (!empty($row)) {
if ($row['active']) {
if (password_verify($password, $row['password'])) {
$_SESSION['loggedIn'] = true;
header('Location: /dashboard.php');
return true;
} else {
header('Location: /login.php?error=invalid');
}
} else {
header('Location: /login.php?error=inactive');
}
} else {
$attempts = $this->dbh->fetch('SELECT attempts FROM login_attempts WHERE username = ?', $username);
if ($attempts >= self::MAX_LOGIN_ATTEMTPS) {
$this->dbh->query('UPDATE login_attempts SET attempts = attempts + 1 WHERE username = ?', $username);
header('Location: /login.php?error=blocked');
} else {
header('Location: /login.php?error=invalid');
}
}
return false;
}
Early return
public function login($username, $password) {
$user = $this->repository->findByUsername($username);
if (empty($user)) {
$this->checkLoginAttempts($user);
return false;
}
return $this->checkUser($user);
}
protected function checkUser(User $user) {
if (!$user->active) {
header('Location: /login.php?error=inactive');
return false;
}
if (!password_verify($password, $user->password)) {
header('Location: /login.php?error=invalid');
return false;
}
$this->doLogin($user);
retun true;
}
protected function doLogin(User $user) {
$_SESSION['loggedIn'] = true;
header('Location: /dashboard.php');
}
protected function checkLoginAttempts(User $user) {
$attempts = $this->repository->touchLoginAttemptsByUser($user);
if ($attempts >= self::MAX_LOGIN_ATTEMTPS) {
header('Location: /login.php?error=blocked');
return;
}
header('Location: /login.php?error=invalid');
}
public function parse($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email inválido');
}
if (!EmailValidator::checkExistingDomain($email)) {
throw new InvalidArgumentException('Email inválido');
}
// ...
}
Se houver comportamento, crie um objeto!
class Email {
public function __construct($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email inválido');
}
// outras validações
}
}
class Example {
public function parse(Email $email) {
// ...
}
}
Se a classe possui uma coleção, ela não deve ter outras propriedades
class FilterApplier {
protected $data = [];
protected $index;
public function add(callable $filter);
public function remove(callable $filter);
public function run();
}
Crie collections!
class FilterApplier {
public function __construct(FilterCollection $filterCollection);
public function run();
}
class FilterCollection implements Iterator {
protected $data = [];
public function add(callable $filter);
public function remove(callable $filter);
}
Se você precisa passar por diversos objetos para chamar um método, significa que você precisa saber muito sobre como eles funcionam
class Controller {
public function run() {
if ($this->getRequest()->getHeaders()->get('Accept') == 'application/json') {
header('Content-type: application/json');
return json_encode(['status' => true]);
}
}
}
Law of Demeter e Tell, Don't Ask
class Controller {
public function run() {
if ($this->getRequest()->needsJson()) {
header('Content-type: application/json');
return json_encode(['status' => true]);
}
}
}
class Request {
public function needsJson() {
return $this->getHeaders()->needsJson();
}
}
class Headers {
public function needsJson() {
return $this->get('Accept') === 'application/json';
}
}
class Login {
public function login(User $user) {
// ...
$attempts = $this->loginAttempts->getByUser($user);
$this->loginAttempts->setByUser($user, ++$attempts);
if ($attempts >= self::MAX_LOGIN_ATTEMTPS) {
$this->loginAttempts->blockUserLogin($user);
}
}
}
“Tell, Don't Ask”
class Login {
public function login(User $user) {
// ...
$this->loginAttempts->touchUser($user);
}
}
github.com/squizlabs/PHP_CodeSniffer
Plugin de Object Calisthenics para o PHPCSVerifica violações do código de acordo com um conjunto de regras
Automaticamente corrige algumas violações no código
Equivalente à ferramenta PMD do Java, analisa código para detectar diversas anomalias, como:
Complexidade determinada de acordo com o número de pontos de decisão (if
, while
, for
e case
) + 1
public function minify(array $files) { // 1
foreach ($files as $file) { // 2
if (!is_file($file)) { // 3
throw new RuntimeException("{$file} não é válido");
}
switch (pathinfo($file, PATHINFO_EXTENSION)) {
case 'js': // 4
// ...
break;
case 'html': // 5
// ...
break;
case 'css': // 6
// ...
break;
}
}
}
Graus de Complexidade
1-4
— baixa 5-7
— moderada8-10
— alta 11+
— muito alta