Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Efekt motyla – czyli jak można pracować z syste...

Efekt motyla – czyli jak można pracować z systemami legacy w PHPie

Jako programiści jesteśmy bardzo dobrzy w wprowadzaniu dużej entropii (chaosu) w tworzonych przez nas aplikacjach. Często dochodzimy do tzw “efektu motyla” gdzie wprowadzenie zmiany w jednym miejscu systemu powoduje katastrofalne skutki w innym miejscu. W mojej prezentacji na podstawie przykładów z projektów legacy nad którymi miałem “przyjemność” pracować chciałbym podzielić się kilkoma technikami które pomagają zapanować nad chaosem i docelowo pozwalają zmniejszyć entropie w naszym kodzie. Powiemy sobie między innymi o rzecach takich jak ACL (Anti-Corruption Layer) o tym jak anemia może nam się odbić czykawką oraz o tym że zasady oraz wzorce między innymi DRY (don't repeat yourself) trzeba używać z głową.

Leszek Prabucki

February 22, 2016
Tweet

More Decks by Leszek Prabucki

Other Decks in Programming

Transcript

  1. Co powoduje efekt motyla w kodzie? - duża ilość linii

    kodu oraz warunków logicznych - logika biznesowa rozmyta w różnych warstaw aplikacji - kod nastawiony na modyfikacje - upośledzona abstrakcja - upośledzone DRY - brak testów
  2. 1. Opakowywanie kodu legacy 2. Proxy na poziomie serwera HTTP

    3. Modyfikacje kodu legacy (testy + refactoring) 4. Dodawanie nowego kodu oraz jego intergracja z legacy Chicken Little sposoby migracji
  3. <?php namespace AppBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use

    Symfony\Component\HttpFoundation\Response; class DefaultController extends Controller { /** * @Route("/{path}", name="legacy", requirements={"path" = ".*"}) */ public function legacyAction() { ob_start(); require_once __DIR__.'/../../../web/index.php'; $content = ob_get_clean(); return new Response($content); } } Opakowywanie kodu legacy
  4. <?php class fwManagePersonAdd extends definition { public function showPage() {

    if ($_POST['add']) { foreach ($_POST as $k => $v) { $ins[$k] = portal::safe($v); } $errors = cPerson::checkErrors($ins); if (!$errors) { $ins['user'] = $this->u->getLogin(); $person = cPerson::add($ins); for ($i = 1; $i <= 10; $i++) { if ($_POST['trade' . $i] && $_POST['subtrade' . $i] && $_POST['time' . $i]) { $tins['trade'] = $_POST['trade' . $i]; $tins['subtrade'] = $_POST['subtrade' . $i]; $tins['time'] = $_POST['time' . $i]; $tins['user'] = $this->u->getId(); $person->addTrade($tins); } } if (is_uploaded_file($_FILES['cv']['tmp_name'])) { $name = portal::upload($_FILES['cv']); $person->addFile($name, cPerson::fileCv); } header("Location: /?a=managePersonDetail&id={$person->getId()}"); die(); } else { foreach ($errors as $k => $v) { $arr['error_' . $k] = i18n_const::getPersonAddErrors($v); } $arr['main_error'] = 'block'; } } return t::instance()->render_once('managePersonAdd/main', $arr); } } Modyfikacje kodu legacy
  5. <?php class DefineContactPersonInformationTest extends PHPUnit_Framework_TestCase { /** * @var \Behat\Mink\Mink

    */ private $mink; public function setUp() { $connection = new \Doctrine\DBAL\Driver\PDOConnection( 'mysql:host=localhost;dbname=testdb', 'username', 'password' ); $connection->exec('DELETE FROM person_trade'); $connection->exec('DELETE FROM person'); $session = new \Behat\Mink\Session(new \Behat\Mink\Driver\GoutteDriver()); $this->mink = new \Behat\Mink\Mink(['default' => $session]); $this->mink->setDefaultSessionName('default'); } /** * @test */ function shouldAllowToDefineContactPersonInformation() { $session = $this->mink->getSession(); $session->visit('http://localhost/index.php?a=managePersonAdd'); $currentPage = $session->getPage(); $currentPage->fillField('name', 'Leszek'); $currentPage->fillField('surname', 'Prabucki'); $currentPage->attachFileToField('cv', __DIR__.'/pathtocv.pdf'); $currentPage->pressButton('Dodaj'); $currentPage = $session->getPage(); $assertSession = $this->mink->assertSession(); $assertSession->addressMatches('#a=managerPersonDetails#'); $this->assertEquals('Leszek', $currentPage->findField('name')->getValue()); $this->assertEquals('Prabucki', $currentPage->findField('surname')->getValue()); } } Modyfikacje kodu legacy - test first
  6. <?php class fwManagePersonAdd extends definition { public function showPage() {

    $request = \Symfony\Component\HttpFoundation\Request::createFromGlobals(); if ($errors = $this->validate($request)) { $errors['main_error'] = 'block'; $content = t::instance()->render_once('managePersonAdd/main', [ 'errors' => $errors ]); return new \Symfony\Component\HttpFoundation\Response($content); } $ins = $request->request->all(); $ins['user'] = $this->u->getLogin(); $person = cPerson::add($ins); if ($cv = $request->files->get('cv', false)) { $name = portal::upload($cv); $person->addFile($name, cPerson::fileCv); } return new \Symfony\Component\HttpFoundation\RedirectResponse( sprintf('/?a=managePersonDetail&id=%d', $person->getId()) ); } private function validate(\Symfony\Component\HttpFoundation\Request $request) { $errors = cPerson::checkErrors($request->request->all()); $errors = array_map(function ($error) { return i18n_const::getPersonAddErrors($error); }, $errors); return $errors; } } Modyfikacje kodu legacy
  7. <?php class fwManagePersonAdd extends definition { public function showPage() {

    if ($_POST['add']) { //... $request = \Symfony\Component\HttpFoundation\Request::createFromGlobals(); portal::getCommandBus()->handle( new \Cocoders\HRAgencyContactList\UseCase\DefineContactPersonInformation\Command( $request->request->get('name'), $request->request->get('surname'), $this->u->getId() ) ); header("Location: /?a=managePersonDetail&id={$person->getId()}"); die(); } else { //.. } } return t::instance()->render_once('managePersonAdd/main', $arr); } } Command Bus usage:
  8. <?php class portal { private static $kernel; public static function

    getCommandBus() { return static::getKernel()->getService('tactician.commandbus'); } private static function getKernel() { if (self::kernel) { return self::$kernel; } $kernel = new LegacyKernel(); $request = Request::createFromGlobals(); $request->attributes->set('isLegacy', true); $kernel->boot(); $container = $kernel->getContainer(); $container->enterScope('request'); $container->get('request_stack')->push($request); $container->set('request', $request); self::$kernel = $kernel; return self::$kernel; } } Shared kernel:
  9. <?php declare(strict_types=1); namespace Cocoders\HRAgencyContactList\UseCase; use Cocoders\HRAgencyContactList\ContactPersonInformationCatalogue; use Cocoders\HRAgencyContactList\ContactPersonInformation; final class

    DefineContactPersonInformation { private $catalogue; public function __construct(ContactPersonInformationCatalogue $catalogue) { $this->catalogue = $catalogue; } public function execute( DefineContactPersonInformation\Command $command, DefineContactPersonInformation\Responder $responder ) { $id = ContactPersonInformation\Id::generate(); $contactPersonInformation = new ContactPersonInformation( $id, ContactPersonInformation\FullName::fromFirstAndSurName( $command->getFirstName(), $command->getSurName() ), $command->getCreatorId() ); $this->catalogue->add($contactPersonInformation); $responder->contactPersonInformationDefined($id); } } Command handler:
  10. <?php declare(strict_types=1); namespace Cocoders\HRAgencyContactList\UseCase; use Cocoders\HRAgencyContactList\ContactPersonInformationCatalogue; use Cocoders\HRAgencyContactList\ContactPersonInformation; final class

    DefineContactPersonInformation { private $catalogue; public function __construct(ContactPersonInformationCatalogue $catalogue) { $this->catalogue = $catalogue; } public function execute( DefineContactPersonInformation\Command $command, DefineContactPersonInformation\Responder $responder ) { $id = ContactPersonInformation\Id::generate(); $contactPersonInformation = new ContactPersonInformation( $id, ContactPersonInformation\FullName::fromFirstAndSurName( $command->getFirstName(), $command->getSurName() ), $command->getCreatorId() ); $this->catalogue->add($contactPersonInformation); $responder->contactPersonInformationDefined($id); } } Użycie repozytorium:
  11. <?php declare(strict_types=1); namespace Cocoders\DoctrineAdapter\HRAgencyContactList; use Doctrine\Common\Persistence\ObjectManager; use Cocoders\HRAgencyContactList\ContactPersonInformationCatalogue as ContactPersonInformationCatalogueInterface;

    final class ContactPersonInformationCatalogue implements ContactPersonInformationCatalogueInterface { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } public function findCreatedBy(ContactPersonInformation\CreatorId $creatorId): array { return $this ->manager ->getRepository(ContactPersonInformation::class) ->findBy(['creatorId' => (string) $creatorId]) ; } public function add(ContactPersonInformation $contactPersonInformation) { $this->manager->persist($contactPersonInformation); } } Repo w Doctrine:
  12. <?php declare(strict_types=1); namespace Cocoders\AntiCorruptionLayer\HRAgencyContactList; use Cocoders\HRAgencyContactList\ContactPersonInformationCatalogue as ContactPersonInformationCatalogueInterface; use Doctrine\DBAL\Connection;

    final class ContactPersonInformationCatalogue implements ContactPersonInformationCatalogueInterface { private $catalogue; private $legacyConnection; public function __construct( ContactPersonInformationCatalogueInterface $catalogue, Connection $legacyConnection ) { $this->catalogue = $catalogue; $this->legacyConnection = $legacyConnection; } //... public function add(ContactPersonInformation $contactPersonInformation) { $maxLegacyPersonId = $this->legacyConnection->fetchColumn('SELECT max(id) FROM person'); $contactPersonInformationId = $contactPersonInformation->getId(); if ($contactPersonInformationId->isLegacy() && $contactPersonInformation->getId() > $maxLegacyPersonId) { throw new \LogicException('Cannot add new legacy strucutre'); } if ($contactPersonInformationId->isLegacy()) { $this->legacyConnection->update( 'person', [ 'name' => $contactPersonInformation->getFirstName(), 'surname' => $contactPersonInformation->getSurName() ], [ 'id' => (string) $contactPersonInformationId ] ); } $this->catalogue->add($contactPersonInformation); } Anti-Corruption Layer repo: