Min-Maxing Software Costs - Laracon EU 2015

13,029 views

Published on

Software development is riddled with explicit and implicit costs. Every decision you make has a cost attached to it. When you're writing code, you're making an investment, the size of which will for a long time define the costs of your future growth. In this talk you will learn how to see, understand and game some of these forces in your favour.

Published in: Technology

Min-Maxing Software Costs - Laracon EU 2015

  1. 1. Min-Maxing Software Costs
  2. 2. @everzet
  3. 3. @inviqa
  4. 4. What is this talk about?
  5. 5. Harm that "Laravel Facades" inflict on not-suspecting developers.
  6. 6. Bad idea that is Active Record and Eloquent.
  7. 7. Other framework's superiority over Laravel.
  8. 8. Killing kittens.
  9. 9. And other subjective and nonconstructive crap like that ...
  10. 10. ... is not in this talk.
  11. 11. Actually in this talk 1. Introducing & making sense of development costs 2. Highlighting the context of tools & practices we use 3. Years of observation & experience, not data collection and analysis
  12. 12. Context, the talk
  13. 13. Software Costs
  14. 14. Software Costs
  15. 15. Software Costs 1. Time to write & test code 2. Time to change code & tests 3. Time to refactor code & tests
  16. 16. Software Costs 1. Time to write & test code - Cost of Introduction 2. Time to change code & tests - Cost of Change 3. Time to refactor code & tests - Cost of Ownership
  17. 17. Cost of Introduction
  18. 18. Cost of Introduction Time it takes to introduce new, naturally independent application logic.
  19. 19. Attributes — Has direct correlation to business value — Has direct correlation to LOC — Relatively easy to optimise by generalisation
  20. 20. Dynamics — Visible from the outset — Loses relevancy over the project lifetime — Stable across projects
  21. 21. Dynamics — Visible from the outset — Loses relevancy over the project lifetime — Stable across projects
  22. 22. Cost of Introduction is relatively easy to optimise.
  23. 23. Optimising for CoI: Convenience Layer
  24. 24. Service Locator // --- Explicit Dependency public function __construct(Cache $cache) { $this->cache = $cache; } public function index() { $photos = $this->cache->get('photos'); // ... } // --- "Laravel Facade" public function index() { $photos = Cache::get('photos'); // ... }
  25. 25. Base Class // --- Base Controller class MyController extends Controller { public function indexAction() { $homepageUrl = $this->generateUrl('homepage'); // ... } }
  26. 26. Optimising for CoI: Generalisation
  27. 27. Active Record // --- Custom Mapping class DbalCustomerRepository implements CustomerRepository { public function findCustomerWithName($name) { // ... } } // --- Eloquent use IlluminateDatabaseEloquentModel; class Customer extends Model { // ... }
  28. 28. Event Dispatcher // --- Event Subscriber interface MyListener { public function emailWasSent($email, $text); } // ... public function sendEmail() { // ... $this->myListenerInstance->emailWasSent($email, $text); } // --- Event Dispatcher $eventDispatcher->dispatch('email.sent', new Event($email, $text));
  29. 29. Dependency Injection Container // --- Dependency Inversion Principle $contoller = new MyController( new Router(), new Cache(new Configuration()) ); // --- Dependency Injection Container $controller = $container->get('controller.my');
  30. 30. No matter what you think, optimising for CoI (Cost of Introduction) is not inherently a bad thing.
  31. 31. A Cut-Off of the product
  32. 32. If the product life is short enough to not encounter loss of CoI relevancy, then the CoI is the only cost worth optimising for.
  33. 33. Convenience based projects either die a hero or live long enough to see themselves become the villain.
  34. 34. Cost of Change
  35. 35. Cost of Change Time it takes to adapt the existing application logic to new business realities.
  36. 36. Attributes — Has direct correlation to business value — Has no direct correlation to LOC — Affected by generalisation
  37. 37. Dynamics — Invisible from the outset — Gains relevancy during the project lifetime — Exponentially increases over time
  38. 38. Cost of Change increases exponentially over time.
  39. 39. public function searchAction(Request $req) { $form = $this->createForm(new SearchQueryType, new SearchQuery); $normalizedOrderBys = $this->getNormalizedOrderBys($filteredOrderBys); $this->computeSearchQuery($req, $filteredOrderBys); if ($req->query->has('search_query')) { /** @var $solarium Solarium_Client */ $solarium = $this->get('solarium.client'); $select = $solarium->createSelect(); // configure dismax $dismax = $select->getDisMax(); $dismax->setQueryFields(array('name^4', 'description', 'tags', 'text', 'text_ngram', 'name_split^2')); if ($req->query->has('search_query')) { $form->bind($req); if ($form->isValid()) { $escapedQuery = $select->getHelper()->escapeTerm($form->getData()->getQuery()); $escapedQuery = preg_replace('/(^| )-(S)/', '$1-$2', $escapedQuery); $escapedQuery = preg_replace('/(^| )+(S)/', '$1+$2', $escapedQuery); if ((substr_count($escapedQuery, '"') % 2) == 0) { $escapedQuery = str_replace('"', '"', $escapedQuery); } $select->setQuery($escapedQuery); } } } elseif ($req->getRequestFormat() === 'json') { return JsonResponse::create(array( 'error' => 'Missing search query, example: ?q=example' ), 400)->setCallback($req->query->get('callback')); } return $this->render('SomeAppWebBundle:Web:search.html.twig'); }
  40. 40. public function searchAction(Request $req) { $form = $this->createForm(new SearchQueryType, new SearchQuery); $this->computeSearchQuery($req, $filteredOrderBys); $typeFilter = $req->query->get('type'); if ($req->query->has('search_query') || $typeFilter) { /** @var $solarium Solarium_Client */ $solarium = $this->get('solarium.client'); $select = $solarium->createSelect(); // configure dismax $dismax = $select->getDisMax(); $dismax->setQueryFields(array('name^4', 'description', 'tags', 'text', 'text_ngram', 'name_split^2')); $dismax->setPhraseFields(array('description')); $dismax->setBoostFunctions(array('log(trendiness)^10')); $dismax->setMinimumMatch(1); $dismax->setQueryParser('edismax'); // filter by type if ($typeFilter) { $filterQueryTerm = sprintf('type:"%s"', $select->getHelper()->escapeTerm($typeFilter)); $filterQuery = $select->createFilterQuery('type')->setQuery($filterQueryTerm); $select->addFilterQuery($filterQuery); } if ($req->query->has('search_query')) { $form->bind($req); if ($form->isValid()) { $escapedQuery = $select->getHelper()->escapeTerm($form->getData()->getQuery()); $escapedQuery = preg_replace('/(^| )-(S)/', '$1-$2', $escapedQuery); $escapedQuery = preg_replace('/(^| )+(S)/', '$1+$2', $escapedQuery); if ((substr_count($escapedQuery, '"') % 2) == 0) { $escapedQuery = str_replace('"', '"', $escapedQuery); } $select->setQuery($escapedQuery); } } $paginator = new Pagerfanta(new SolariumAdapter($solarium, $select)); $perPage = $req->query->getInt('per_page', 15); if ($perPage <= 0 || $perPage > 100) { if ($req->getRequestFormat() === 'json') { return JsonResponse::create(array( 'status' => 'error', 'message' => 'The optional packages per_page parameter must be an integer between 1 and 100 (default: 15)', ), 400)->setCallback($req->query->get('callback')); } $perPage = max(0, min(100, $perPage)); } } elseif ($req->getRequestFormat() === 'json') { return JsonResponse::create(array( 'error' => 'Missing search query, example: ?q=example' ), 400)->setCallback($req->query->get('callback')); } return $this->render('SomeAppWebBundle:Web:search.html.twig'); }
  41. 41. public function searchAction(Request $req) { $form = $this->createForm(new SearchQueryType, new SearchQuery); $filteredOrderBys = $this->getFilteredOrderedBys($req); $normalizedOrderBys = $this->getNormalizedOrderBys($filteredOrderBys); $this->computeSearchQuery($req, $filteredOrderBys); $typeFilter = $req->query->get('type'); $tagsFilter = $req->query->get('tags'); if ($req->query->has('search_query') || $typeFilter || $tagsFilter) { /** @var $solarium Solarium_Client */ $solarium = $this->get('solarium.client'); $select = $solarium->createSelect(); // configure dismax $dismax = $select->getDisMax(); $dismax->setQueryFields(array('name^4', 'description', 'tags', 'text', 'text_ngram', 'name_split^2')); $dismax->setPhraseFields(array('description')); $dismax->setBoostFunctions(array('log(trendiness)^10')); $dismax->setMinimumMatch(1); $dismax->setQueryParser('edismax'); // filter by type if ($typeFilter) { $filterQueryTerm = sprintf('type:"%s"', $select->getHelper()->escapeTerm($typeFilter)); $filterQuery = $select->createFilterQuery('type')->setQuery($filterQueryTerm); $select->addFilterQuery($filterQuery); } // filter by tags if ($tagsFilter) { $tags = array(); foreach ((array) $tagsFilter as $tag) { $tags[] = $select->getHelper()->escapeTerm($tag); } $filterQueryTerm = sprintf('tags:("%s")', implode('" AND "', $tags)); $filterQuery = $select->createFilterQuery('tags')->setQuery($filterQueryTerm); $select->addFilterQuery($filterQuery); } if (!empty($filteredOrderBys)) { $select->addSorts($normalizedOrderBys); } if ($req->query->has('search_query')) { $form->bind($req); if ($form->isValid()) { $escapedQuery = $select->getHelper()->escapeTerm($form->getData()->getQuery()); $escapedQuery = preg_replace('/(^| )-(S)/', '$1-$2', $escapedQuery); $escapedQuery = preg_replace('/(^| )+(S)/', '$1+$2', $escapedQuery); if ((substr_count($escapedQuery, '"') % 2) == 0) { $escapedQuery = str_replace('"', '"', $escapedQuery); } $select->setQuery($escapedQuery); } } $paginator = new Pagerfanta(new SolariumAdapter($solarium, $select)); $perPage = $req->query->getInt('per_page', 15); if ($perPage <= 0 || $perPage > 100) { if ($req->getRequestFormat() === 'json') { return JsonResponse::create(array( 'status' => 'error', 'message' => 'The optional packages per_page parameter must be an integer between 1 and 100 (default: 15)', ), 400)->setCallback($req->query->get('callback')); } $perPage = max(0, min(100, $perPage)); } $paginator->setMaxPerPage($perPage); $paginator->setCurrentPage($req->query->get('page', 1), false, true); $metadata = array(); foreach ($paginator as $package) { if (is_numeric($package->id)) { $metadata['downloads'][$package->id] = $package->downloads; $metadata['favers'][$package->id] = $package->favers; } } if ($req->getRequestFormat() === 'json') { try { $result = array( 'results' => array(), 'total' => $paginator->getNbResults(), ); } catch (Solarium_Client_HttpException $e) { return JsonResponse::create(array( 'status' => 'error', 'message' => 'Could not connect to the search server', ), 500)->setCallback($req->query->get('callback')); } return JsonResponse::create($result)->setCallback($req->query->get('callback')); } if ($req->isXmlHttpRequest()) { try { return $this->render('PackagistWebBundle:Web:search.html.twig', array( 'packages' => $paginator, 'meta' => $metadata, 'noLayout' => true, )); } catch (Twig_Error_Runtime $e) { if (!$e->getPrevious() instanceof Solarium_Client_HttpException) { throw $e; } return JsonResponse::create(array( 'status' => 'error', 'message' => 'Could not connect to the search server', ), 500)->setCallback($req->query->get('callback')); } } return $this->render('PackagistWebBundle:Web:search.html.twig', array( 'packages' => $paginator, 'meta' => $metadata, )); } elseif ($req->getRequestFormat() === 'json') { return JsonResponse::create(array( 'error' => 'Missing search query, example: ?q=example' ), 400)->setCallback($req->query->get('callback')); } return $this->render('PackagistWebBundle:Web:search.html.twig'); }
  42. 42. Exponential increase of Cost of Change is not inherently a problem of every product.
  43. 43. A Cut-Off of the product
  44. 44. If the product life is long enough to encounter exponential growth of CoC, then the CoC is the cost worth optimising for.
  45. 45. If you want to change the world with your product, then the change is the primary thing your product must prepare for.
  46. 46. Optimising for Cost of Introduction in most cases has a negative effect on the Cost of Change curve.
  47. 47. That's why some engineers try to increase the Cost of Introduction in attempt to affect the Cost of Change curve.
  48. 48. Cost of Introduction and Change
  49. 49. Increasing Cost of Introduction
  50. 50. Cost of Change with increased Cost of Introduction
  51. 51. Upfront Design (aka Waterfall) Illusion that one can control cost of change by applying enough analysis upfront.
  52. 52. Upfront Design fails to achieve long-lasting effect because both rate and nature of change for arbitrary domain is unpredictable.
  53. 53. Cost of Ownership
  54. 54. Cost of Ownership Time it takes to maintain the owned application logic to support its ongoing change.
  55. 55. Attributes — Intermediate between Cost of Introduction & Cost of Change — Has no direct correlation to business value — Has direct correlation to LOC
  56. 56. Dynamics — Always invisible — Always relevant — Stable over time, but adds up
  57. 57. Cost of Ownership is the cost you pay for the right to change a particular part (module, class, method) of application continuosly and sustainably.
  58. 58. Testing
  59. 59. Unit testing
  60. 60. Refactoring
  61. 61. Introducing Cost of Ownership allows you to balance two other costs.
  62. 62. Cost of Introduction and Change
  63. 63. Cost of Introduction and Ownership
  64. 64. Cost of Ownership effect on Cost of Change curve
  65. 65. Emergent Design Usual result of ongoing ownership.
  66. 66. Cost of Ownership of everything
  67. 67. Cost of Ownership of everything
  68. 68. Owning everything fails to achieve ever-increasing benefits, because you rarely need to change the entire system.
  69. 69. End-to-end testing is owning everything
  70. 70. Cost of Ownership of the wrong thing
  71. 71. Ownership wouldn't help if you're owning the wrong thing.
  72. 72. Exclusive end-to-end testing is owning the wrong thing
  73. 73. You do want to own everything worth owning.
  74. 74. But you don't know what's worth owning at the beginning of the project.
  75. 75. Software Costs recap 1. Cost of Introduction - Linear. Relevant at the beginning. Very easy to optimise for. 2. Cost of Change - Exponential. Relevant everywhere except the beginning. Hard to optimise for. 3. Cost of Ownership - Linear. Relevant throughout. Owning the wrong thing is bad.
  76. 76. Gaming Software Costs
  77. 77. Own only the logic you need to change.
  78. 78. Write only the logic you need to own.
  79. 79. Own everything you write.
  80. 80. Own everything you write. Try to not write anything.
  81. 81. Own everything you write. Try to not write anything. Reuse everything else.
  82. 82. 1. Document the need
  83. 83. 2. Spike - Experiment with tools available
  84. 84. 3. Document changes & constraints
  85. 85. 4. Stabilise - Claim ownership when the thing grows outside of tool boundaries
  86. 86. 5. Isolate Religiously
  87. 87. Steps 1. Document the need 2. Spike 3. Document changes & constraints 4. Stabilise 5. Isolate Religiously
  88. 88. Unit testing is owning
  89. 89. Refactoring is owning
  90. 90. Test Driven Development is an ownership technique
  91. 91. Gaming Software Costs 1. Document 2. Spike & Stabilise 3. Use TDD for stabilisation
  92. 92. CC credits - money.jpg - https://flic.kr/p/s6895e - time.jpg - https://flic.kr/p/4tNrxq - cheating.jpg - https://flic.kr/p/7FCr59 - developing.jpg - https://flic.kr/p/bHLu96 - change.jpg - https://flic.kr/p/6PtfXL - ownership.jpg - https://flic.kr/p/bwJSRV - pair_programming.jpg - https://flic.kr/p/QNdeB - unit_tests.jpg - https://flic.kr/p/7KEnN7 - testing.jpg - https://flic.kr/p/tpCxq - test_driven.jpg - https://flic.kr/p/7Lx9Kk - refactoring.jpg - https://flic.kr/p/dUmmRN - context.jpg - https://flic.kr/p/93iAmM
  93. 93. Thank you!
  94. 94. Questions?Please, leave feedback: https://joind.in/15022

×