Slide 1

Slide 1 text

GETTING OUT OF TROUBLE WITH ANTI-PATTERNS

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

WE ARE HIRING!

Slide 6

Slide 6 text

GETTING OUT OF TROUBLE WITH ANTI-PATTERNS

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Design patterns are common ways of solving familiar problems in software engineering

Slide 9

Slide 9 text

https://laracasts.com/series/design-patterns-in-php

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Anti-patterns are common ways of solving familiar problems in software engineering…

Slide 12

Slide 12 text

…which have an unintended negative result

Slide 13

Slide 13 text

We Keep Making The Same Mistakes

Slide 14

Slide 14 text

Spotting anti-patterns helps you identify where things have gone wrong, and puts you back on the right path

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

SOFTWARE DEVELOPMENT ANTI-PATTERNS

Slide 20

Slide 20 text

SOFTWARE ARCHITECTURE ANTI-PATTERNS

Slide 21

Slide 21 text

PROJECT MANAGEMENT ANTI-PATTERNS

Slide 22

Slide 22 text

I thought you’d already fixed that bug?

Slide 23

Slide 23 text

CUT & PASTE PROGRAMMING

Slide 24

Slide 24 text

The same bug recurs throughout the system despite many local fixes Symptoms

Slide 25

Slide 25 text

Lines of code increase more than productivity Symptoms

Slide 26

Slide 26 text

Excessive software maintenance costs Symptoms

Slide 27

Slide 27 text

Organisational emphasis is on short-term payoff rather than long-term investment Typical Causes

Slide 28

Slide 28 text

Development speed is considered a top marker of productivity Typical Causes

Slide 29

Slide 29 text

Lack of abstraction / poor understanding of inheritance, composition, or other development strategies Typical Causes

Slide 30

Slide 30 text

People who, unfamiliar with a new technology, copy & paste examples from stack overflow and just modify to their needs Typical Causes

Slide 31

Slide 31 text

Code Mining

Slide 32

Slide 32 text

$ composer require --dev sebastian/phpcpd

Slide 33

Slide 33 text

$ php vendor/bin/phpcpd . --exclude-regexps=vendor

Slide 34

Slide 34 text

Found 35 clones with 1873 duplicated lines in 44 files: .../campaign_groups/lib/ManageCampaignGroups.php:258-462 (204 lines) .../campaign/lib/ManageCampaigns.php:335-539

Slide 35

Slide 35 text

Found 35 clones with 1873 duplicated lines in 44 files: .../campaign_groups/lib/ManageCampaignGroups.php:258-462 (204 lines) .../campaign/lib/ManageCampaigns.php:335-539

Slide 36

Slide 36 text

Found 35 clones with 1873 duplicated lines in 44 files: .../campaign_groups/lib/ManageCampaignGroups.php:258-462 (204 lines) .../campaign/lib/ManageCampaigns.php:335-539

Slide 37

Slide 37 text

Found 35 clones with 1873 duplicated lines in 44 files: .../campaign_groups/lib/ManageCampaignGroups.php:258-462 (204 lines) .../campaign/lib/ManageCampaigns.php:335-539

Slide 38

Slide 38 text

Found 35 clones with 1873 duplicated lines in 44 files: .../campaign_groups/lib/ManageCampaignGroups.php:258-462 (204 lines) .../campaign/lib/ManageCampaigns.php:335-539

Slide 39

Slide 39 text

Found 35 clones with 1873 duplicated lines in 44 files: .../campaign_groups/lib/ManageCampaignGroups.php:258-462 (204 lines) .../campaign/lib/ManageCampaigns.php:335-539

Slide 40

Slide 40 text

Refactor

Slide 41

Slide 41 text

Pluck Reusable Components

Slide 42

Slide 42 text

/** * filters out campaigns. * * @param ArrayManipulation $campaigns * @param array $filtersSpec * * @throws Exception */ public static function filterCampaignsList(ArrayManipulation $campai $filterRules = array(); foreach ($filtersSpec as $filter) { if ($filter['name'] == '-custom-ysdb') { $operator = $filter['comparator']; $expectedValue = $filter['value']; $campaignFbIds = self::pluckFbCampaignIds($campaigns->getValue if (!$campaignFbIds) { continue; } // get yesterday spend. used in `yday spnd/dly bdgt` filter $spec = DWEndpointSpec::create() ->setIds($campaignFbIds) ->addFields(array('spend')) ->setDateFrom((new DateTime())->modify('-1 DAY')) ->setDateTo(new DateTime()) campaign ManageCampaignGroups.php campaign_groups

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

/** * @param $filtersArray * * @return array */ public static function parseFiltersArray($filtersArray) { $pre = array(); $post = array(); $preFiltersNames = array( 'statusIds' => 'statusIds', 'name' => 'name', 'budget' => 'budget', 'objective' => 'objective', 'spend_cap' => 'spend_cap' ); $in_array_match = function (&$value, $array) { foreach ($array as $pattern => $newValue) { if (preg_match('/^' . $pattern . '$/', $value)) { $value = $newValue; return true; } } return false; }; campaign ManageCampaignGroups.php campaign_groups

Slide 45

Slide 45 text

ManageCampaigns.php /** * @param $filtersArray * * @return array */ public static function parseFiltersArray($filtersArray) { $pre = array(); $post = array(); $preFiltersNames = array( 'dimensionTag_\d+' => 'dimensionTag', 'statusIds' => 'statusIds', 'bidTypes' => 'bidTypes', 'campaignGroupIds' => 'campaignGroupIds', 'name' => 'name', 'budget' => 'budget', 'optimisationGoal' => 'optimisationGoal', 'billingEvent' => 'billingEvent', '-custom-dayparting' => '-custom-dayparting', '-custom-dimension' => '-custom-dimension', ); $in_array_match = function (&$value, $array) { foreach ($array as $pattern => $newValue) { if (preg_match('/^' . $pattern . '$/', $value)) { $value = $newValue; return true; campaign campaign_groups

Slide 46

Slide 46 text

/** * @param $filtersArray * * @return array */ public static function parseFiltersArray($filtersArray) { $pre = array(); $post = array(); $preFiltersNames = array( 'dimensionTag_\d+' => 'dimensionTag', 'statusIds' => 'statusIds', 'bidTypes' => 'bidTypes', 'bidModel' => 'bidModel', 'campaignIds' => 'campaignIds', 'campaignGroupIds' => 'campaignGroupIds', 'uploadId' => 'uploadId', 'creative_title' => 'creative_title', 'creative_body' => 'creative_body', 'name' => 'name', 'maxBid' => 'maxBid', '-custom-dimension' => '-custom-dimension', ); $in_array_match = function (&$value, $array) { foreach ($array as $pattern => $newValue) { if (preg_match('/^' . $pattern . '$/', $value)) { $value = $newValue; campaign campaign_groups campaign_ads ManageAds.php

Slide 47

Slide 47 text

abstract class CampaignManagement { abstract protected static function getFilters(): array; public static function parseFiltersArray(): array { $pre = array(); $post = array(); $preFiltersNames = static::getFilters(); $in_array_match = function (&$value, $array) { foreach ($array as $pattern => $newValue) { if (preg_match('/^' . $pattern . '$/', $value)) { $value = $newValue; return true; } } return false; }; foreach ($filtersArray as $filter) { if ($in_array_match($filter['name'], $preFiltersNames)) { campaign campaign_groups campaign_ads CampaignManagement.php Management

Slide 48

Slide 48 text

abstract class CampaignManagement { abstract protected static function getFilters(): array; public static function parseFiltersArray(): array { $pre = array(); $post = array(); $preFiltersNames = static::getFilters(); $in_array_match = function (&$value, $array) { foreach ($array as $pattern => $newValue) { if (preg_match('/^' . $pattern . '$/', $value)) { $value = $newValue; return true; } } return false; }; foreach ($filtersArray as $filter) { if ($in_array_match($filter['name'], $preFiltersNames)) { campaign campaign_groups campaign_ads CampaignManagement.php Management

Slide 49

Slide 49 text

abstract class CampaignManagement { abstract protected static function getFilters(): array; public static function parseFiltersArray(): array { $pre = array(); $post = array(); $preFiltersNames = static::getFilters(); $in_array_match = function (&$value, $array) { foreach ($array as $pattern => $newValue) { if (preg_match('/^' . $pattern . '$/', $value)) { $value = $newValue; return true; } } return false; }; foreach ($filtersArray as $filter) { if ($in_array_match($filter['name'], $preFiltersNames)) { campaign campaign_groups campaign_ads CampaignManagement.php Management

Slide 50

Slide 50 text

class ManageAdsPage extends CampaignManagement { protected static function getFilters(): array { return array( 'dimensionTag_\d+' => 'dimensionTag', 'statusIds' => 'statusIds', 'bidTypes' => 'bidTypes', 'bidModel' => 'bidModel', 'campaignIds' => 'campaignIds', 'campaignGroupIds' => 'campaignGroupIds', 'uploadId' => 'uploadId', 'creative_title' => 'creative_title', 'creative_body' => 'creative_body', 'name' => 'name', 'maxBid' => 'maxBid', '-custom-dimension' => '-custom-dimension', ); } } campaign campaign_groups campaign_ads ManageAds.php Management

Slide 51

Slide 51 text

class ManageAdsPage extends CampaignManagement { protected static function getFilters(): array { return array( 'dimensionTag_\d+' => 'dimensionTag', 'statusIds' => 'statusIds', 'bidTypes' => 'bidTypes', 'bidModel' => 'bidModel', 'campaignIds' => 'campaignIds', 'campaignGroupIds' => 'campaignGroupIds', 'uploadId' => 'uploadId', 'creative_title' => 'creative_title', 'creative_body' => 'creative_body', 'name' => 'name', 'maxBid' => 'maxBid', '-custom-dimension' => '-custom-dimension', ); } } campaign campaign_groups campaign_ads ManageAds.php Management

Slide 52

Slide 52 text

class ManageAdsPage extends CampaignManagement { protected static function getFilters(): array { return array( 'dimensionTag_\d+' => 'dimensionTag', 'statusIds' => 'statusIds', 'bidTypes' => 'bidTypes', 'bidModel' => 'bidModel', 'campaignIds' => 'campaignIds', 'campaignGroupIds' => 'campaignGroupIds', 'uploadId' => 'uploadId', 'creative_title' => 'creative_title', 'creative_body' => 'creative_body', 'name' => 'name', 'maxBid' => 'maxBid', '-custom-dimension' => '-custom-dimension', ); } } campaign campaign_groups campaign_ads ManageAds.php Management

Slide 53

Slide 53 text

$ php vendor/bin/phpcpd . --exclude-regexps=vendor

Slide 54

Slide 54 text

Found 31 clones with 1426 duplicated lines in 42 files:

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Code Review

Slide 57

Slide 57 text

We’re not really sure what that code does

Slide 58

Slide 58 text

But it works so we just leave it alone We’re not really sure what that code does

Slide 59

Slide 59 text

LAVA FLOW

Slide 60

Slide 60 text

Undocumented, complex, important-looking functions / classes / segments unrelated to the system architecture Symptoms

Slide 61

Slide 61 text

Unjustifiable variables & code fragments distributed throughout system Symptoms

Slide 62

Slide 62 text

Lots of ‘TODO / to complete’ comments Symptoms

Slide 63

Slide 63 text

Large blocks of commented-out code with no explanation Symptoms

Slide 64

Slide 64 text

R&D code finds its way into production Typical Causes

Slide 65

Slide 65 text

Lone-wolf working on the code Typical Causes

Slide 66

Slide 66 text

Shifting goals of the software project Typical Causes

Slide 67

Slide 67 text

Lack of architectural forethought / design Typical Causes

Slide 68

Slide 68 text

Dig In To The Problem

Slide 69

Slide 69 text

actions.class.php actions getParameter('uploadId'); $clientId = $this->getUser()->getAttribute('clientSessionId'); $inlineBuilderUpload = Doctrine::getTable('InlineBuilderUpload')- >findOneByIdAndClientId($uploadId, $clientId); if ($inlineBuilderUpload instanceof InlineBuilderUpload) { # We only allow to process builds from the last step of the builde it (saved) if (in_array($inlineBuilderUpload->getStatus(), array(InlineBuilderUpload::STATUS_EDITING,InlineBuilderUpload::STATUS_SA $inlineBuilderUpload->setStatus(InlineBuilderUpload::STATUS_IN_Q $inlineBuilderUpload->save(); } } /* $bulkUpload = $inlineBuilderUpload->loadBulkUpload(); $conn = sfContext::getInstance()->getDatabaseManager()->getDatabase( >getDoctrineConnection();

Slide 70

Slide 70 text

actions.class.php actions } } /* $bulkUpload = $inlineBuilderUpload->loadBulkUpload(); $conn = sfContext::getInstance()->getDatabaseManager()->getDatabase('d >getDoctrineConnection(); try { $conn->beginTransaction(); $bulkUpload->saveIntoDB(); // Commit the transaction when done $conn->commit(); $inlineBuilderUpload->setStatus(InlineBuilderUpload::STATUS_PROCESSE $inlineBuilderUpload->save(); } catch (Exception $e) { // Rollback if transaction fail $conn->rollback(); $bulkError = new BulkError(); $bulkError->setClass('BulkDB'); $bulkError->setFunction('BulkDB.saveIntoDB'); $bulkError->setType(BulkError::TYPE_ERROR); $bulkError->setMessage($e->getMessage());

Slide 71

Slide 71 text

actions.class.php actions $bulkError->setFunction('BulkDB.saveIntoDB'); $bulkError->setType(BulkError::TYPE_ERROR); $bulkError->setMessage($e->getMessage()); $bulkError->setObject('inlineBuilderUpload'); $bulkError->setAccountId($inlineBuilderUpload->getAccountId()); $bulkError->save(); $inlineBuilderUpload->setStatus(InlineBuilderUpload::STATUS_ERROR); $inlineBuilderUpload->save(); } */ $this->redirect('/inlinebuilder/uploads'); } public function executeDownloadOLD(sfWebRequest $request) { // ... } public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } }

Slide 72

Slide 72 text

Refactor

Slide 73

Slide 73 text

Detective Work

Slide 74

Slide 74 text

$ git blame actions.class.php

Slide 75

Slide 75 text

a315cb4a (Jordi 2014-07-01) /* 9dedbd66 (Jordi 2014-06-30) $conn = sfContext::getInstance()->getDatabaseManager()-> 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) try { 9dedbd66 (Jordi 2014-06-30) $conn->beginTransaction(); 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) $bulkUpload->saveIntoDB(); 144398e4 (Jordi 2014-06-30) 9dedbd66 (Jordi 2014-06-30) $conn->commit(); 9dedbd66 (Jordi 2014-06-30)

Slide 76

Slide 76 text

a315cb4a (Jordi 2014-07-01) /* 9dedbd66 (Jordi 2014-06-30) $conn = sfContext::getInstance()->getDatabaseManager()-> 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) try { 9dedbd66 (Jordi 2014-06-30) $conn->beginTransaction(); 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) $bulkUpload->saveIntoDB(); 144398e4 (Jordi 2014-06-30) 9dedbd66 (Jordi 2014-06-30) $conn->commit(); 9dedbd66 (Jordi 2014-06-30)

Slide 77

Slide 77 text

a315cb4a (Jordi 2014-07-01) /* 9dedbd66 (Jordi 2014-06-30) $conn = sfContext::getInstance()->getDatabaseManager()-> 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) try { 9dedbd66 (Jordi 2014-06-30) $conn->beginTransaction(); 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) $bulkUpload->saveIntoDB(); 144398e4 (Jordi 2014-06-30) 9dedbd66 (Jordi 2014-06-30) $conn->commit(); 9dedbd66 (Jordi 2014-06-30)

Slide 78

Slide 78 text

a315cb4a (Jordi 2014-07-01) /* 9dedbd66 (Jordi 2014-06-30) $conn = sfContext::getInstance()->getDatabaseManager()-> 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) try { 9dedbd66 (Jordi 2014-06-30) $conn->beginTransaction(); 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) $bulkUpload->saveIntoDB(); 144398e4 (Jordi 2014-06-30) 9dedbd66 (Jordi 2014-06-30) $conn->commit(); 9dedbd66 (Jordi 2014-06-30)

Slide 79

Slide 79 text

a315cb4a (Jordi 2014-07-01) /* 9dedbd66 (Jordi 2014-06-30) $conn = sfContext::getInstance()->getDatabaseManager()-> 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) try { 9dedbd66 (Jordi 2014-06-30) $conn->beginTransaction(); 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) $bulkUpload->saveIntoDB(); 144398e4 (Jordi 2014-06-30) 9dedbd66 (Jordi 2014-06-30) $conn->commit(); 9dedbd66 (Jordi 2014-06-30)

Slide 80

Slide 80 text

a315cb4a (Jordi 2014-07-01) /* 9dedbd66 (Jordi 2014-06-30) $conn = sfContext::getInstance()->getDatabaseManager()-> 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) try { 9dedbd66 (Jordi 2014-06-30) $conn->beginTransaction(); 9dedbd66 (Jordi 2014-06-30) 144398e4 (Jordi 2014-06-30) $bulkUpload->saveIntoDB(); 144398e4 (Jordi 2014-06-30) 9dedbd66 (Jordi 2014-06-30) $conn->commit(); 9dedbd66 (Jordi 2014-06-30)

Slide 81

Slide 81 text

$ git show a315cb4a

Slide 82

Slide 82 text

commit a315cb4a741f7dec51376477c8c66419991aa22d Author: Jordi Date: Tue Jul 1 13:08:15 2014 +0100 [Inline Builder] - DB Process -- Cron added. + task/BuilderTask.class.php

Slide 83

Slide 83 text

commit a315cb4a741f7dec51376477c8c66419991aa22d Author: Jordi Date: Tue Jul 1 13:08:15 2014 +0100 [Inline Builder] - DB Process -- Cron added. + task/BuilderTask.class.php

Slide 84

Slide 84 text

BuilderTask.class.php task 0) { /** @var InlineBuilderUpload $inlineBuilder */ foreach ($inlineBuilders as $inlineBuilder) { TerminalHelper::log("Loading Bulk Upload for Inline Job " . $inli "yellow"); $bulkUpload = $inlineBuilder->loadBulkUpload(); $conn = sfContext::getInstance()->getDatabaseManager()->getDataba >getDoctrineConnection(); try { $conn->beginTransaction(); TerminalHelper::log("Saving Job to DB", "yellow"); $bulkUpload->saveIntoDB(); actions

Slide 85

Slide 85 text

actions.class.php actions it (saved) if (in_array($inlineBuilderUpload->getStatus(), array(InlineBuilderUpload::STATUS_EDITING,InlineBuilderUpload::STATUS_SAVE $inlineBuilderUpload->setStatus(InlineBuilderUpload::STATUS_IN_QUE $inlineBuilderUpload->save(); } } /* * DELETE ALL THE COMMENTED-OUT CODE! */ $this->redirect('/inlinebuilder/uploads'); } public function executeDownloadOLD(sfWebRequest $request) { // ... } public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } } task

Slide 86

Slide 86 text

actions.class.php actions it (saved) if (in_array($inlineBuilderUpload->getStatus(), array(InlineBuilderUpload::STATUS_EDITING,InlineBuilderUpload::STATUS_SAVE $inlineBuilderUpload->setStatus(InlineBuilderUpload::STATUS_IN_QUE $inlineBuilderUpload->save(); } } /* * DELETE ALL THE COMMENTED-OUT CODE! */ $this->redirect('/inlinebuilder/uploads'); } public function executeDownloadOLD(sfWebRequest $request) { // ... } public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } } task

Slide 87

Slide 87 text

actions.class.php actions it (saved) if (in_array($inlineBuilderUpload->getStatus(), array(InlineBuilderUpload::STATUS_EDITING,InlineBuilderUpload::STATUS_SAVE $inlineBuilderUpload->setStatus(InlineBuilderUpload::STATUS_IN_QUE $inlineBuilderUpload->save(); } } /* * DELETE ALL THE COMMENTED-OUT CODE! */ $this->redirect('/inlinebuilder/uploads'); } public function executeDownloadOLD(sfWebRequest $request) { // ... } public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } } task

Slide 88

Slide 88 text

$ git log -- actions.class.php

Slide 89

Slide 89 text

commit d4e72ab65850ef49fd829e367fe6970025f180bb Author: Daniel Date: Thu Oct 15 15:34:31 2015 +0300 Excel download button last page of inline builder. PR-4971 commit 937a9d05b5a9120e9345d8784814ff5dc7a1f51a Author: Radu Date: Thu Oct 15 10:14:09 2015 +0100 PR-4972 #resolve Duplicate Excel download from the upload's page

Slide 90

Slide 90 text

commit d4e72ab65850ef49fd829e367fe6970025f180bb Author: Daniel Date: Thu Oct 15 15:34:31 2015 +0300 Excel download button last page of inline builder. PR-4971 commit 937a9d05b5a9120e9345d8784814ff5dc7a1f51a Author: Radu Date: Thu Oct 15 10:14:09 2015 +0100 PR-4972 #resolve Duplicate Excel download from the upload's page

Slide 91

Slide 91 text

commit d4e72ab65850ef49fd829e367fe6970025f180bb Author: Daniel Date: Thu Oct 15 15:34:31 2015 +0300 Excel download button last page of inline builder. PR-4971 commit 937a9d05b5a9120e9345d8784814ff5dc7a1f51a Author: Radu Date: Thu Oct 15 10:14:09 2015 +0100 PR-4972 #resolve Duplicate Excel download from the upload's page

Slide 92

Slide 92 text

commit d4e72ab65850ef49fd829e367fe6970025f180bb Author: Daniel Date: Thu Oct 15 15:34:31 2015 +0300 Excel download button last page of inline builder. PR-4971 commit 937a9d05b5a9120e9345d8784814ff5dc7a1f51a Author: Radu Date: Thu Oct 15 10:14:09 2015 +0100 PR-4972 #resolve Duplicate Excel download from the upload's page

Slide 93

Slide 93 text

$ git show 937a9d05b

Slide 94

Slide 94 text

+ Excel + getUser()->hasPermission('new_excel')) {?> +

Slide 95

Slide 95 text

+ Excel + getUser()->hasPermission('new_excel')) {?> +

Slide 96

Slide 96 text

+ Excel + getUser()->hasPermission('new_excel')) {?> +

Slide 97

Slide 97 text

+ Excel + getUser()->hasPermission('new_excel')) {?> +

Slide 98

Slide 98 text

actions.class.php actions task public function executeDownloadOLD(sfWebRequest $request) { // ... } public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } }

Slide 99

Slide 99 text

actions.class.php actions public function executeDownloadOLD(sfWebRequest $request) { // ... } public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } } task

Slide 100

Slide 100 text

actions.class.php actions task public function executeDownload(sfWebRequest $request) { // ... } public function executeOldDownload(sfWebRequest $request) { // ... } }

Slide 101

Slide 101 text

actions.class.php actions task public function executeVersion2Download(sfWebRequest $request) { // ... } public function executeVersion1Download(sfWebRequest $request) { // ... } }

Slide 102

Slide 102 text

No content

Slide 103

Slide 103 text

Invest In Educating Your Developers

Slide 104

Slide 104 text

Redefine The Architecture Whenever You See A Lava Flow Forming

Slide 105

Slide 105 text

That code is a mess!

Slide 106

Slide 106 text

That code is a mess! Making a change in one place breaks something in another

Slide 107

Slide 107 text

SPAGHETTI CODE

Slide 108

Slide 108 text

Code cannot be re-used Symptoms

Slide 109

Slide 109 text

Objects are frequently named as processes Symptoms

Slide 110

Slide 110 text

Minimal relationships between objects Symptoms

Slide 111

Slide 111 text

Many object methods take no parameters, and utilise class or global variables Symptoms

Slide 112

Slide 112 text

Difficult to retain staff with OOP background Symptoms

Slide 113

Slide 113 text

Side-effects everywhere Symptoms

Slide 114

Slide 114 text

Poor application performance Symptoms

Slide 115

Slide 115 text

Programmers are new to OOP Typical Causes

Slide 116

Slide 116 text

No mentoring / inefficient code reviews Typical Causes

Slide 117

Slide 117 text

No thought of architectural design prior to implementation Typical Causes

Slide 118

Slide 118 text

Developers working in isolation Typical Causes

Slide 119

Slide 119 text

10af6d6 adding privacy policy page 93ddb94 i bcb7319 ii 8747627 Finishing up Website Usage Page 2414eb8 iwq 2a5910d ii eadc6e8 ii 0d23c1d ii e6a97b1 wq 5780487 i 64eea96 ii 6914478 qwe 1f06a5d iq 07d15b8 wr 1d6df82 echo fc014f1 Adding new background c7ed903 Here we go 9040b40 Testing 4523f29 testing 66e7e60 iok 965d026 Making modifications a5f58f8 Adding Google Manager codes 08f91d9 wq ff555c1 removing header 23d07b1 Removing content 61e480d ok trying this out

Slide 120

Slide 120 text

10af6d6 adding privacy policy page 93ddb94 i bcb7319 ii 8747627 Finishing up Website Usage Page 2414eb8 iwq 2a5910d ii eadc6e8 ii 0d23c1d ii e6a97b1 wq 5780487 i 64eea96 ii 6914478 qwe 1f06a5d iq 07d15b8 wr 1d6df82 echo fc014f1 Adding new background c7ed903 Here we go 9040b40 Testing 4523f29 testing 66e7e60 iok 965d026 Making modifications a5f58f8 Adding Google Manager codes 08f91d9 wq ff555c1 removing header 23d07b1 Removing content 61e480d ok trying this out

Slide 121

Slide 121 text

campaigns.php functions function deleteCampaignGuestsViaCampaignId($campaign_id) { $dbh = getNewPDOInstance(‘MY_DB_NAME’); $sql = 'DELETE FROM campaign_guest WHERE campaign_id = ‘ . $campaign_id; $stmt = $dbh->prepare($sql); if ($stmt->execute()) { return true; } } function deletePhotosFromCampaignFolderViaCampaignId($campaign_id) { global $root_prefix; global $rootd; $path = $root_prefix . "/uploads/campaigns/" . $campaign_id; $real_file_path = $rootd . "/uploads/campaigns/" . $campaign_id; $photos = scandir($_SERVER['DOCUMENT_ROOT'] . $path); // print_r($photos); foreach ($photos as $key => $photo) { // this is to skip . and .. in the folder. if ("." === substr($photo, 0, 1)) {

Slide 122

Slide 122 text

function deleteCampaignGuestsViaCampaignId($campaign_id) { $dbh = getNewPDOInstance(‘MY_DB_NAME’); $sql = 'DELETE FROM campaign_guest WHERE campaign_id = ‘ . $campaign_id; $stmt = $dbh->prepare($sql); if ($stmt->execute()) { return true; } } function deletePhotosFromCampaignFolderViaCampaignId($campaign_id) { global $root_prefix; global $rootd; $path = $root_prefix . "/uploads/campaigns/" . $campaign_id; $real_file_path = $rootd . "/uploads/campaigns/" . $campaign_id; $photos = scandir($_SERVER['DOCUMENT_ROOT'] . $path); // print_r($photos); foreach ($photos as $key => $photo) { // this is to skip . and .. in the folder. if ("." === substr($photo, 0, 1)) { campaigns.php functions

Slide 123

Slide 123 text

function deleteCampaignGuestsViaCampaignId($campaign_id) { $dbh = getNewPDOInstance(‘MY_DB_NAME’); $sql = 'DELETE FROM campaign_guest WHERE campaign_id = ‘ . $campaign_id; $stmt = $dbh->prepare($sql); if ($stmt->execute()) { return true; } } function deletePhotosFromCampaignFolderViaCampaignId($campaign_id) { global $root_prefix; global $rootd; $path = $root_prefix . "/uploads/campaigns/" . $campaign_id; $real_file_path = $rootd . "/uploads/campaigns/" . $campaign_id; $photos = scandir($_SERVER['DOCUMENT_ROOT'] . $path); // print_r($photos); foreach ($photos as $key => $photo) { // this is to skip . and .. in the folder. if ("." === substr($photo, 0, 1)) { campaigns.php functions

Slide 124

Slide 124 text

function createUpdateLoginClient($client){ $password_hash = password_hash($client['password'], PASSWORD_DEFAULT); $id = $client['id']; $dbh = getNewPDOInstance(‘login_clients'); if($id){ // ... } if($client['password'] && $id){ // ... } if(!$id){ $client_id = createClient($client['user_name'], $client['email'], $ // ... } if($result){ return true; } else{ return true; } } function createClient($email){ $dbh = getNewPDOInstance('MY_DB_NAME'); // ... } campaigns.php functions clients.php

Slide 125

Slide 125 text

function createUpdateLoginClient($client){ $password_hash = password_hash($client['password'], PASSWORD_DEFAULT); $id = $client['id']; $dbh = getNewPDOInstance(‘login_clients'); if($id){ // ... } if($client['password'] && $id){ // ... } if(!$id){ $client_id = createClient($client['user_name'], $client['email'], $ // ... } if($result){ return true; } else{ return true; } } function createClient($email){ $dbh = getNewPDOInstance('MY_DB_NAME'); // ... } campaigns.php functions clients.php

Slide 126

Slide 126 text

function createUpdateLoginClient($client){ $password_hash = password_hash($client['password'], PASSWORD_DEFAULT); $id = $client['id']; $dbh = getNewPDOInstance(‘login_clients'); if($id){ // ... } if($client['password'] && $id){ // ... } if(!$id){ $client_id = createClient($client['user_name'], $client['email'], $ // ... } if($result){ return true; } else{ return true; } } function createClient($email){ $dbh = getNewPDOInstance('MY_DB_NAME'); // ... } campaigns.php functions clients.php

Slide 127

Slide 127 text

function createUpdateLoginClient($client){ $password_hash = password_hash($client['password'], PASSWORD_DEFAULT); $id = $client['id']; $dbh = getNewPDOInstance(‘login_clients'); if($id){ // ... } if($client['password'] && $id){ // ... } if(!$id){ $client_id = createClient($client['user_name'], $client['email'], $ // ... } if($result){ return true; } else{ return true; } } function createClient($email){ $dbh = getNewPDOInstance('MY_DB_NAME'); // ... } campaigns.php functions clients.php

Slide 128

Slide 128 text

index.php functions

Slide 129

Slide 129 text

index.php functions if($folder){ $path = $rootd.'lib/get/'.$folder.'/'.$get.'.php'; }else{ $path = $rootd.'lib/get/'.$get.'.php'; } if (!file_exists($path)) exit(header("location: /404")); include $path; } elseif (isset($page)) { $path = $rootd.'lib/pages_data/'.$page.'.php'; if (!file_exists($path)){ error_log('lol ' . $path); exit(header("location: /404")); } include $rootd.'lib/init/html.php'; include $path; } elseif (isset($_SESSION['my_id'])) { $path = $rootd.'lib/pages_data/home.php'; if (!file_exists($path)) exit(header("location: /404")); include $rootd.'lib/init/html.php'; include $path; } else { $path = $rootd.'lib/pages_data/sign-up.php'; if (!file_exists($path)) exit(header("location: /404")); include $rootd.'lib/init/html.php'; include $path; }

Slide 130

Slide 130 text

index.php functions if($folder){ $path = $rootd.'lib/get/'.$folder.'/'.$get.'.php'; }else{ $path = $rootd.'lib/get/'.$get.'.php'; } if (!file_exists($path)) exit(header("location: /404")); include $path; } elseif (isset($page)) { $path = $rootd.'lib/pages_data/'.$page.'.php'; if (!file_exists($path)){ error_log('lol ' . $path); exit(header("location: /404")); } include $rootd.'lib/init/html.php'; include $path; } elseif (isset($_SESSION['my_id'])) { $path = $rootd.'lib/pages_data/home.php'; if (!file_exists($path)) exit(header("location: /404")); include $rootd.'lib/init/html.php'; include $path; } else { $path = $rootd.'lib/pages_data/sign-up.php'; if (!file_exists($path)) exit(header("location: /404")); include $rootd.'lib/init/html.php'; include $path; }

Slide 131

Slide 131 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 132

Slide 132 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 133

Slide 133 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 134

Slide 134 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 135

Slide 135 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 136

Slide 136 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 137

Slide 137 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 138

Slide 138 text

// Init MySQL logins function getNewPDOInstance($db_name) { require $_SERVER['DOCUMENT_ROOT']."/master_config.php"; $db_hostname = $lab_hostname; $db_username = $lab_username; $db_password = $lab_password; if($is_live & $db_name == “MY_DB_NAME” || $db_name == “OTHER_DB_NAME” || $db_name == “YET_ANOTHER_DB_NAME” ){ $db_hostname = $tracking_machine_hostname; $db_username = $tracking_machine_username; $db_password = $tracking_machine_password; } if($is_live & $db_name == "login"){ $db_hostname = $login_machine_hostname; $db_username = $login_machine_username; $db_password = $login_machine_password; } index.php functions database connection.php

Slide 139

Slide 139 text

$db_username = $login_machine_username; $db_password = $login_machine_password; } if($is_live & $db_name == “login_clients"){ $db_hostname = $login_clients_machine_hostname; $db_username = $login_clients_machine_username; $db_password = $login_clients_machine_password; } $dbh = null; try { $dbh = new PDO("mysql:host=$db_hostname;dbname=$db_name;charset=utf8mb4 $db_password); } catch(PDOException $e) { error_log("Error in newPDOInstance: ".$e->getMessage()); print_r($e); return false; } //print_r($e); return $dbh; } index.php functions database connection.php

Slide 140

Slide 140 text

$db_username = $login_machine_username; $db_password = $login_machine_password; } if($is_live & $db_name == “login_clients"){ $db_hostname = $login_clients_machine_hostname; $db_username = $login_clients_machine_username; $db_password = $login_clients_machine_password; } $dbh = null; try { $dbh = new PDO("mysql:host=$db_hostname;dbname=$db_name;charset=utf8mb4 $db_password); } catch(PDOException $e) { error_log("Error in newPDOInstance: ".$e->getMessage()); print_r($e); return false; } //print_r($e); return $dbh; } index.php functions database connection.php

Slide 141

Slide 141 text

$db_username = $login_machine_username; $db_password = $login_machine_password; } if($is_live & $db_name == “login_clients"){ $db_hostname = $login_clients_machine_hostname; $db_username = $login_clients_machine_username; $db_password = $login_clients_machine_password; } $dbh = null; try { $dbh = new PDO("mysql:host=$db_hostname;dbname=$db_name;charset=utf8mb4 $db_password); } catch(PDOException $e) { error_log("Error in newPDOInstance: ".$e->getMessage()); print_r($e); return false; } //print_r($e); return $dbh; } index.php functions database connection.php

Slide 142

Slide 142 text

$db_username = $login_machine_username; $db_password = $login_machine_password; } if($is_live & $db_name == “login_clients"){ $db_hostname = $login_clients_machine_hostname; $db_username = $login_clients_machine_username; $db_password = $login_clients_machine_password; } $dbh = null; try { $dbh = new PDO("mysql:host=$db_hostname;dbname=$db_name;charset=utf8mb4 $db_password); } catch(PDOException $e) { error_log("Error in newPDOInstance: ".$e->getMessage()); print_r($e); return false; } //print_r($e); return $dbh; } index.php functions database connection.php

Slide 143

Slide 143 text

index.php functions database

Slide 144

Slide 144 text

Refactor

Slide 145

Slide 145 text

Encapsulate Elements With Getters & Setters

Slide 146

Slide 146 text

index.php functions database

Slide 147

Slide 147 text

index.php functions database

Slide 148

Slide 148 text

index.php functions database class Database { private $default = null; private $lab = null; private $login_clients = null; public function getConnectionFor(string $databaseName): \PDO { if (false === array_key_exists( $databaseName, get_object_vars($this)) ) { throw new \InvalidArgumentException(‘Invalid DB requested'); } if (null === $this->{$databaseName}) { $this->{$databaseName} = $this->createConnection($databaseName); } return $this->{$databaseName}; } Config.php Database.php

Slide 149

Slide 149 text

index.php functions database class Database { private $default = null; private $lab = null; private $login_clients = null; public function getConnectionFor(string $databaseName): \PDO { if (false === array_key_exists( $databaseName, get_object_vars($this)) ) { throw new \InvalidArgumentException(‘Invalid DB requested'); } if (null === $this->{$databaseName}) { $this->{$databaseName} = $this->createConnection($databaseName); } return $this->{$databaseName}; } Config.php Database.php

Slide 150

Slide 150 text

index.php functions database class Database { private $default = null; private $lab = null; private $login_clients = null; public function getConnectionFor(string $databaseName): \PDO { if (false === array_key_exists( $databaseName, get_object_vars($this)) ) { throw new \InvalidArgumentException(‘Invalid DB requested'); } if (null === $this->{$databaseName}) { $this->{$databaseName} = $this->createConnection($databaseName); } return $this->{$databaseName}; } Config.php Database.php

Slide 151

Slide 151 text

index.php functions database class Database { private $default = null; private $lab = null; private $login_clients = null; public function getConnectionFor(string $databaseName): \PDO { if (false === array_key_exists( $databaseName, get_object_vars($this)) ) { throw new \InvalidArgumentException(‘Invalid DB requested'); } if (null === $this->{$databaseName}) { $this->{$databaseName} = $this->createConnection($databaseName); } return $this->{$databaseName}; } Config.php Database.php

Slide 152

Slide 152 text

index.php functions database private function createConnection(string $databaseName): \PDO { if ('lab' === $databaseName) { return new \PDO( Config::labDBHostname(), Config::labDBUsername(), Config::labDBPassword() ); } if ('login_clients' === $databaseName) { return new \PDO( // ... ); } } Config.php Database.php

Slide 153

Slide 153 text

index.php functions database private function createConnection(string $databaseName): \PDO { if ('lab' === $databaseName) { return new \PDO( Config::labDBHostname(), Config::labDBUsername(), Config::labDBPassword() ); } if ('login_clients' === $databaseName) { return new \PDO( // ... ); } } Config.php Database.php

Slide 154

Slide 154 text

index.php repository database Config.php functions ClientRepository.php class ClientRepository { private $db; public function __construct(Database $db) { $this->db = $db; } function createUpdateLoginClient($client) { $password_hash = password_hash($client['password'], PASSWORD_DEFAUL $id = $client['id']; $dbh = this->db->getConnectionFor('login_clients'); if ($id) { // ... } if ($client['password'] && $id) { // ... } if (!$id) { $client_id = $this->createClient($client['user_name'], $client[ // ... } }

Slide 155

Slide 155 text

Pluck Reusable Components

Slide 156

Slide 156 text

{ function createUpdateLoginClient($client) { if ($id) { // ... } if ($client['password'] && $id) { // ... } if (!$id) { $client_id = $this->createClient( $client['user_name'], $client[‘email'], $client['security_clearance'] ); // ... } } function createClient($email) { $this->db->getConnectionFor('login_clients'); // ... } } index.php repository database Config.php functions ClientRepository.php

Slide 157

Slide 157 text

{ function createUpdateLoginClient($client) { if ($id) { // ... } if ($client['password'] && $id) { // ... } if (!$id) { $client_id = $this->createClient( $client['user_name'], $client[‘email'], $client['security_clearance'] ); // ... } } function createClient($email) { $this->db->getConnectionFor('login_clients'); // ... } } index.php repository database Config.php functions ClientRepository.php

Slide 158

Slide 158 text

Even consistent bad code is better than inconsistent spaghetti Remember

Slide 159

Slide 159 text

class Client { private $id; private $email; private $password; private $securityClearance; public function __construct( int $id, string $email, string $password, string $securityClearance = 'CLIENT' ) { $this->id = $id; $this->email = $email; $this->password = $password; $this->securityClearance = $securityClearance; } public function getEmail(): string { return $this->email; } public function getPassword(): string index.php repository database Config.php functions Client.php model

Slide 160

Slide 160 text

index.php repository database Config.php functions Client.php model class Client { private $id; private $email; private $password; private $securityClearance; public function __construct( int $id, string $email, string $password, string $securityClearance = 'CLIENT' ) { $this->id = $id; $this->email = $email; $this->password = $password; $this->securityClearance = $securityClearance; } public function getEmail(): string { return $this->email; } public function getPassword(): string

Slide 161

Slide 161 text

index.php repository database Config.php functions Client.php model class Client { private $id; private $email; private $password; private $securityClearance; public function __construct( int $id, string $email, string $password, string $securityClearance = 'CLIENT' ) { $this->id = $id; $this->email = $email; $this->password = $password; $this->securityClearance = $securityClearance; } public function getEmail(): string { return $this->email; } public function getPassword(): string

Slide 162

Slide 162 text

index.php repository database Config.php functions Client.php model class Client { private $id; private $email; private $password; private $securityClearance; public function __construct( int $id, string $email, string $password, string $securityClearance = 'CLIENT' ) { $this->id = $id; $this->email = $email; $this->password = $password; $this->securityClearance = $securityClearance; } public function getEmail(): string { return $this->email; } public function getPassword(): string

Slide 163

Slide 163 text

{ function createUpdateLoginClient(Client $client) { $password_hash = password_hash($client->getPassword(), PASSWORD_DEF $this->db->getConnectionFor('login_clients'); if ($client->getId()) { // ... } if ($client->getPassword() && $client->getId()) { // ... } if (!$client->getId()) { $client_id = $this->createClient($client); // ... } } function createClient(Client $client) { $this->db->getConnectionFor('login_clients'); // ... } } index.php repository database Config.php functions ClientRepository.php model

Slide 164

Slide 164 text

{ function createUpdateLoginClient(Client $client) { $password_hash = password_hash($client->getPassword(), PASSWORD_DEF $this->db->getConnectionFor('login_clients'); if ($client->getId()) { // ... } if ($client->getPassword() && $client->getId()) { // ... } if (!$client->getId()) { $client_id = $this->createClient($client); // ... } } function createClient(Client $client) { $this->db->getConnectionFor('login_clients'); // ... } } index.php repository database Config.php functions ClientRepository.php model

Slide 165

Slide 165 text

Identify & Eleminate Lava Flows As You Go

Slide 166

Slide 166 text

No content

Slide 167

Slide 167 text

Take Time To Analyse The Domain Before Starting Work

Slide 168

Slide 168 text

Develop A Model Before Implementation, Rather Than Concurrently

Slide 169

Slide 169 text

Don’t touch that class

Slide 170

Slide 170 text

It’s the heart of the application Don’t touch that class

Slide 171

Slide 171 text

THE BLOB

Slide 172

Slide 172 text

Single class with large number of properties & methods (>60 is typical) Symptoms

Slide 173

Slide 173 text

Single class encapsulates a disparate collection of properties & methods Symptoms

Slide 174

Slide 174 text

Class is too inefficient or excessively complex for reuse and testing, or uses excessive resources even for simple operations Symptoms

Slide 175

Slide 175 text

Functionality is added piecemeal without ever refactoring components out Typical Causes

Slide 176

Slide 176 text

Lack of object oriented architecture Typical Causes

Slide 177

Slide 177 text

Lack of any architecture Typical Causes

Slide 178

Slide 178 text

Lack of architectural enforcement Typical Causes

Slide 179

Slide 179 text

setSuperCampaign($super); * $factory->setReportType('persona'); * $factory->addSortColumn($col, $order); * * foreach($factory->getTmpTablesSql() AS $tmp) { * $stmt = $conn->prepare($tmp); * $stmt->execute($pdo_tmp_params); * } * * $stmt = $conn->prepare($factory->getSql()); * $stmt->execute($pdo_params); * $array = $stmt->fetchAll(); * * note: $pdo_params is an array of parameters such as 'clientId', reporting ReportFactory.php

Slide 180

Slide 180 text

setSuperCampaign($super); * $factory->setReportType('persona'); * $factory->addSortColumn($col, $order); * * foreach($factory->getTmpTablesSql() AS $tmp) { * $stmt = $conn->prepare($tmp); * $stmt->execute($pdo_tmp_params); * } * * $stmt = $conn->prepare($factory->getSql()); * $stmt->execute($pdo_params); * $array = $stmt->fetchAll(); * * note: $pdo_params is an array of parameters such as 'clientId', reporting ReportFactory.php

Slide 181

Slide 181 text

setSuperCampaign($super); * $factory->setReportType('persona'); * $factory->addSortColumn($col, $order); * * foreach($factory->getTmpTablesSql() AS $tmp) { * $stmt = $conn->prepare($tmp); * $stmt->execute($pdo_tmp_params); * } * * $stmt = $conn->prepare($factory->getSql()); * $stmt->execute($pdo_params); * $array = $stmt->fetchAll(); * * note: $pdo_params is an array of parameters such as 'clientId', reporting ReportFactory.php

Slide 182

Slide 182 text

CampaignID DateTime Event 1234 2019-01-01 13:37:00 click 1234 2019-01-01 12:34:56 view 9876 2019-01-02 18:00:01 view 1234 2019-01-02 23:33:32 click … … … … … … … … …

Slide 183

Slide 183 text

CampaignID DateTime Event 1234 2019-01-01 13:37:00 click 1234 2019-01-01 12:34:56 view 9876 2019-01-02 18:00:01 view 1234 2019-01-02 23:33:32 click … … … … … … … … … CampaignID Date Clicks Likes Follows … 1234 2019-01-01 39480120 1398473 32424 … 3482 2019-01-01 23482134 235019934 94512 … 9876 2019-01-02 9324243 123489132 405202 … 9231 2019-01-02 1249823 509847 3248 …

Slide 184

Slide 184 text

CampaignID DateTime Event 1234 2019-01-01 13:37:00 click 1234 2019-01-01 12:34:56 view 9876 2019-01-02 18:00:01 view 1234 2019-01-02 23:33:32 click … … … … … … … … … CampaignID Date Event Total 1234 2019-01-01 click 1398473 1234 2019-01-01 view 235019934 9876 2019-01-02 view 123489132 1234 2019-01-02 click 509847 CampaignID Date Event Total 1234 2019-01-01 click 1398473 1234 2019-01-01 view 235019934 9876 2019-01-02 view 123489132 1234 2019-01-02 click 509847 CampaignID Date Event Total 1234 2019-01-01 click 1398473 1234 2019-01-01 view 235019934 9876 2019-01-02 view 123489132 1234 2019-01-02 click 509847 CampaignID Date Clicks Likes Follows … 1234 2019-01-01 39480120 1398473 32424 … 3482 2019-01-01 23482134 235019934 94512 … 9876 2019-01-02 9324243 123489132 405202 … 9231 2019-01-02 1249823 509847 3248 …

Slide 185

Slide 185 text

Identify Relationships According To Contracts

Slide 186

Slide 186 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {} function addSortColumn($col, $order) {} function setReportType($report_type) {} function addTmpOr($status) {} function setWhere($where) {} function addWhere($where) {} function getWhere() {} function setColumns($col) {} function addColumn($col) {} function setParameters($params) {} function addParam($param) {}

Slide 187

Slide 187 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {} function addSortColumn($col, $order) {} function setReportType($report_type) {} function addTmpOr($status) {} function setWhere($where) {} function addWhere($where) {} function getWhere() {} function setColumns($col) {} function addColumn($col) {} function setParameters($params) {} function addParam($param) {}

Slide 188

Slide 188 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {} function addSortColumn($col, $order) {} function setReportType($report_type) {} function addTmpOr($status) {} function setWhere($where) {} function addWhere($where) {} function getWhere() {} function setColumns($col) {} function addColumn($col) {} function setParameters($params) {} function addParam($param) {}

Slide 189

Slide 189 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {} function addSortColumn($col, $order) {} function setReportType($report_type) {} function addTmpOr($status) {} function setWhere($where) {} function addWhere($where) {} function getWhere() {} function setColumns($col) {} function addColumn($col) {} function setParameters($params) {} function addParam($param) {}

Slide 190

Slide 190 text

Refactor

Slide 191

Slide 191 text

Identify Natural Homes For The Contracts

Slide 192

Slide 192 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {} function addSortColumn($col, $order) {} function setReportType($report_type) {} function addTmpOr($status) {} function setWhere($where) {} function addWhere($where) {} function getWhere() {} function setColumns($col) {} function addColumn($col) {} function setParameters($params) {} function addParam($param) {}

Slide 193

Slide 193 text

Slide 194

Slide 194 text

queryBuilder = new QueryBuilder(); } public function getQueryBuilder() { return $this->queryBuilder; } public function getSql() { return $sql; } // ... } reporting ReportFactory.php Tmp

Slide 195

Slide 195 text

function someAction() { $factory = new ReportFactory(); // ... $factory->addWhere('status = '. $st); $factory->addTmpOr('(c.status <= '.$st.')'); $factory->addWhere('(time_start <= "' . date("Y-m-d H:i:s", time()) . '" OR time_start is null)’ ); } reporting Controller.php Tmp

Slide 196

Slide 196 text

function someAction() { $factory = new ReportFactory(); $builder = $factory->getQueryBuilder(); // ... $builder->addWhere('status = '. $st); $builder->addTmpOr('(c.status <= '.$st.')'); $builder->addWhere('(time_start <= "' . date("Y-m-d H:i:s", time()) . '" OR time_start is null)’ ); } reporting Controller.php Tmp

Slide 197

Slide 197 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {} function addSortColumn($col, $order) {} function setReportType($report_type) {} function addTmpOr($status) {} function setWhere($where) {} function addWhere($where) {} function getWhere() {} function setColumns($col) {} function addColumn($col) {} function setParameters($params) {} function addParam($param) {}

Slide 198

Slide 198 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {}

Slide 199

Slide 199 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {}

Slide 200

Slide 200 text

Slide 201

Slide 201 text

createCampaignGroupsTable(); $this->createCampaignGroupsStatsTable(); $this->createClicksMetricsTable(); break; case 'views': // ... break; } } } reporting TmpTable.php Tmp QueryBuilder.php

Slide 202

Slide 202 text

function _createTmpTables() {} function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {} function _createSuperCampaignsTable() {} function _createCampaignGroupsTable() {} function _createSuperCampaignsStatusesTable() {} function _createCampaignGroupsStatusesTable() {} function _createSuperCampaignsStatsTable() {} function _createCampaignGroupsStatsTable() {} function _createCampaignsStatsTable() {}

Slide 203

Slide 203 text

function _getScheduledReportsSelectSql() {} function _getCampaignSelectSql() {} function _getPersonaSelectSql() {} function _getImageSelectSql() {} function _getTitleSelectSql() {} function _getOverviewSelectSql() {} function _getExcelDownloadSelectSql() {} function _getCampaignsOverviewSelectSql() {}

Slide 204

Slide 204 text

tmpTable = $tmpTable; } public function getScheduledReportsSelectSql() {} public function getCampaignSelectSql() {} public function getPersonaSelectSql() {} public function getImageSelectSql() {} public function getTitleSelectSql() {} public function getOverviewSelectSql() {} // ... } reporting MetricsQuery.php Tmp QueryBuilder.php TmpTable.php

Slide 205

Slide 205 text

Remove Couplings

Slide 206

Slide 206 text

Controller

Slide 207

Slide 207 text

ReportFactory getQueryBuilder getSql … Controller

Slide 208

Slide 208 text

ReportFactory getQueryBuilder getSql … Controller

Slide 209

Slide 209 text

ReportFactory getQueryBuilder getSql … Controller QueryBuilder addColumn addSortColumn addWhere …

Slide 210

Slide 210 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … Controller QueryBuilder addColumn addSortColumn addWhere …

Slide 211

Slide 211 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere …

Slide 212

Slide 212 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere …

Slide 213

Slide 213 text

connection = $connection; } abstract public function setQueryConditions(); public function getReport() { $this->setQueryConditions(); $sql = $this->getSql(); $query = $this->pdo->prepare($sql); return $query->execute(); } } reporting MetricsQuery.php Tmp QueryBuilder.php TmpTable.php MetricsReport.php

Slide 214

Slide 214 text

connection = $connection; } abstract public function setQueryConditions(); public function getReport() { $this->setQueryConditions(); $sql = $this->getSql(); $query = $this->pdo->prepare($sql); return $query->execute(); } } reporting MetricsQuery.php Tmp QueryBuilder.php TmpTable.php MetricsReport.php

Slide 215

Slide 215 text

connection = $connection; } abstract public function setQueryConditions(); public function getReport() { $this->setQueryConditions(); $sql = $this->getSql(); $query = $this->pdo->prepare($sql); return $query->execute(); } } reporting MetricsQuery.php Tmp QueryBuilder.php TmpTable.php MetricsReport.php

Slide 216

Slide 216 text

function someAction() { $factory = new ReportFactory(); // ... $factory->addWhere('status = '. $st); $factory->addTmpOr('(c.status <= '.$st.')'); $factory->addWhere('(time_start <= "' . date("Y-m-d H:i:s", time()) . '" OR time_start is null)’ ); } reporting Controller.php Tmp

Slide 217

Slide 217 text

function someAction() { $report = new SomeMetricsReport(); return $report->getResult(); } reporting Controller.php Tmp

Slide 218

Slide 218 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere …

Slide 219

Slide 219 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere … MetricsReport ReportFactory setQueryConditions

Slide 220

Slide 220 text

connection = $connection; } abstract public function setQueryConditions(); public function getReport() { $this->setQueryConditions(); $sql = $this->getSql(); $query = $this->pdo->prepare($sql); return $query->execute(); } } reporting MetricsQuery.php Tmp QueryBuilder.php TmpTable.php MetricsReport.php

Slide 221

Slide 221 text

connection = $connection; } abstract public function getQuery(); public function getReport() { $sql = $this->getQuery(); $query = $this->pdo->prepare($sql); return $query->execute(); } } reporting MetricsQuery.php Tmp QueryBuilder.php TmpTable.php MetricsReport.php

Slide 222

Slide 222 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere … getQuery MetricsReport ReportFactory

Slide 223

Slide 223 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere … getQuery MetricsReport ReportFactory

Slide 224

Slide 224 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql ReportFactory getQueryBuilder getSql … … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere … MetricsReport getQuery

Slide 225

Slide 225 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere … MetricsReport getQuery

Slide 226

Slide 226 text

No content

Slide 227

Slide 227 text

Slide 228

Slide 228 text

sql = $sql; } public function getSql(): string { return $this->sql->getSql() . ' CREATE TABLE ... ;'; } } reporting TmpTableSql.php Query Tmp SomeTmpTable.php

Slide 229

Slide 229 text

The solution doesn’t have to be perfect… Remember

Slide 230

Slide 230 text

… Just less shit than it was before Remember

Slide 231

Slide 231 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql … TmpTable createTmpTables createCampaignsTable createStatsTable … Controller QueryBuilder addColumn addSortColumn addWhere … MetricsReport getQuery

Slide 232

Slide 232 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql … << interface >> TmpTable getSql Controller QueryBuilder addColumn addSortColumn addWhere … MetricsReport getQuery

Slide 233

Slide 233 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql … << interface >> TmpTable getSql Controller MetricsReport getQuery

Slide 234

Slide 234 text

connection = $connection; } abstract protected function setupTables(): TmpTable; abstract protected function getSql(): string; public function getReport() { $tmpTables = $this->connection->prepare($this->setupTables()); $tmpTables->execute(); $query = $this->connection->prepare($this->getSql()); $query->execute(); return $query->fetchAll(); } reporting TmpTableSql.php Query Tmp MetricsReport.php SomeTmpTable.php

Slide 235

Slide 235 text

connection = $connection; } abstract protected function setupTables(): TmpTable; abstract protected function getSql(): string; public function getReport() { $tmpTables = $this->connection->prepare($this->setupTables()); $tmpTables->execute(); $query = $this->connection->prepare($this->getSql()); $query->execute(); return $query->fetchAll(); } reporting TmpTableSql.php Query Tmp MetricsReport.php SomeTmpTable.php

Slide 236

Slide 236 text

connection = $connection; } abstract protected function setupTables(): TmpTable; abstract protected function getSql(): string; public function getReport() { $tmpTables = $this->connection->prepare($this->setupTables()); $tmpTables->execute(); $query = $this->connection->prepare($this->getSql()); $query->execute(); return $query->fetchAll(); } reporting TmpTableSql.php Query Tmp MetricsReport.php SomeTmpTable.php

Slide 237

Slide 237 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql … << interface >> TmpTable getSql Controller MetricsReport setupTables getSql getReport

Slide 238

Slide 238 text

MetricsQuery getCampaignSelectSql getImageSelectSql getTitleSelectSql … << interface >> TmpTable getSql Controller MetricsReport setupTables getSql getReport

Slide 239

Slide 239 text

<< interface >> TmpTable getSql Controller MetricsReport setupTables getSql getReport

Slide 240

Slide 240 text

reporting TmpTableSql.php Query Tmp MetricsReport.php SomeTmpTable.php

Slide 241

Slide 241 text

TmpTableSql.php Query MetricsReport.php SomeTmpTable.php

Slide 242

Slide 242 text

No content

Slide 243

Slide 243 text

Code Review

Slide 244

Slide 244 text

The project is almost finished, we’ve been a great team

Slide 245

Slide 245 text

The project is almost finished, we’ve been a great team Everybody got on so well & worked so hard together

Slide 246

Slide 246 text

The project is almost finished, we’ve been a great team Everybody got on so well & worked so hard together It’s going to be a disaster

Slide 247

Slide 247 text

FEAR OF SUCCESS

Slide 248

Slide 248 text

People involved in the project begin to obsess over things which could go wrong Symptoms

Slide 249

Slide 249 text

Insecurities about professional competence begin to surface Symptoms

Slide 250

Slide 250 text

Conflicts between team members arise just before project completion Symptoms

Slide 251

Slide 251 text

Irrational decisions are made & inappropriate resolutions to problems are taken Symptoms

Slide 252

Slide 252 text

Negative publicity on the project is generated by people involved with the work Symptoms

Slide 253

Slide 253 text

People have ‘termination issues’ Typical Causes

Slide 254

Slide 254 text

Team members build relationships which they subconsciously don’t want to break up at the end of the project Typical Causes

Slide 255

Slide 255 text

People have assumed roles / status within the team which they don’t want to lose when the project is completed Typical Causes

Slide 256

Slide 256 text

People do crazy things Typical Causes

Slide 257

Slide 257 text

Declare Success

Slide 258

Slide 258 text

Celebrate Achievements & Praise Team Members

Slide 259

Slide 259 text

We spent 6 months waiting for requirements from management

Slide 260

Slide 260 text

We spent 6 months waiting for requirements from management But they’ll cancel the project unless it’s completed soon

Slide 261

Slide 261 text

We spent 6 months waiting for requirements from management They’ve given us 4 weeks But they’ll cancel the project unless it’s completed soon

Slide 262

Slide 262 text

FIRE DRILL

Slide 263

Slide 263 text

Management prevents development teams from making progress by telling them to wait Symptoms

Slide 264

Slide 264 text

Management prevents development teams from making progress by giving unclear or conflicting requirements Symptoms

Slide 265

Slide 265 text

After a few months management calls an emergency launch meeting, setting unrealistic deadlines & threatening to cancel the project Symptoms

Slide 266

Slide 266 text

Managers have too many projects competing for their attention at any given time Typical Causes

Slide 267

Slide 267 text

Shelter

Slide 268

Slide 268 text

Iterative Progress

Slide 269

Slide 269 text

Internal Progress

Slide 270

Slide 270 text

External Progress

Slide 271

Slide 271 text

Notes

Slide 272

Slide 272 text

Pick The Low Hanging Fruit

Slide 273

Slide 273 text

Learning & Experience

Slide 274

Slide 274 text

Training

Slide 275

Slide 275 text

Leadership

Slide 276

Slide 276 text

Teamwork

Slide 277

Slide 277 text

No content

Slide 278

Slide 278 text

No content

Slide 279

Slide 279 text

WE ARE HIRING!