Min-Maxing Software Costs - Laracon EU 2015

Post on 16-Apr-2017

13.087 views 0 download

Transcript of Min-Maxing Software Costs - Laracon EU 2015

Min-Maxing Software Costs

@everzet

@inviqa

What is this talk about?

Harm that "Laravel Facades" inflict on not-suspecting

developers.

Bad idea that is Active Record and Eloquent.

Other framework's superiority over Laravel.

Killing kittens.

And other subjective and nonconstructive crap like that ...

... is not in this talk.

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

Context, the talk

Software Costs

Software Costs

Software Costs

1. Time to write & test code

2. Time to change code & tests

3. Time to refactor code & tests

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

Cost of Introduction

Cost of IntroductionTime it takes to introduce new,

naturally independent application logic.

Attributes

— Has direct correlation to business value

— Has direct correlation to LOC

— Relatively easy to optimise by generalisation

Dynamics

— Visible from the outset

— Loses relevancy over the project lifetime

— Stable across projects

Dynamics

— Visible from the outset

— Loses relevancy over the project lifetime

— Stable across projects

Cost of Introduction is relatively easy to optimise.

Optimising for CoI: Convenience Layer

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'); // ...}

Base Class

// --- Base Controller

class MyController extends Controller{ public function indexAction() { $homepageUrl = $this->generateUrl('homepage'); // ... }}

Optimising for CoI: Generalisation

Active Record

// --- Custom Mapping

class DbalCustomerRepository implements CustomerRepository{ public function findCustomerWithName($name) { // ... }}

// --- Eloquent

use Illuminate\Database\Eloquent\Model;

class Customer extends Model{ // ...}

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));

Dependency Injection Container

// --- Dependency Inversion Principle

$contoller = new MyController( new Router(), new Cache(new Configuration()));

// --- Dependency Injection Container

$controller = $container->get('controller.my');

No matter what you think, optimising for CoI (Cost of

Introduction) is not inherently a bad thing.

A Cut-Off of the product

If the product life is short enough to not encounter loss of CoI

relevancy, then the CoI is the only cost worth optimising for.

Convenience based projects either die a hero or live long

enough to see themselves become the villain.

Cost of Change

Cost of ChangeTime it takes to adapt the

existing application logic to new business realities.

Attributes

— Has direct correlation to business value

— Has no direct correlation to LOC

— Affected by generalisation

Dynamics

— Invisible from the outset

— Gains relevancy during the project lifetime

— Exponentially increases over time

Cost of Change increases exponentially over time.

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');}

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');}

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');}

Exponential increase of Cost of Change is not inherently a problem of every product.

A Cut-Off of the product

If the product life is long enough to encounter exponential growth of CoC, then the CoC is the cost

worth optimising for.

If you want to change the world with your product, then the

change is the primary thing your product must prepare for.

Optimising for Cost of Introduction in most cases has a

negative effect on the Cost of Change curve.

That's why some engineers try to increase the Cost of Introduction

in attempt to affect the Cost of Change curve.

Cost of Introduction and Change

Increasing Cost of Introduction

Cost of Change with increased Cost of Introduction

Upfront Design (aka Waterfall) Illusion that one can control

cost of change by applying enough analysis upfront.

Upfront Design fails to achieve long-lasting effect because both

rate and nature of change for arbitrary domain is

unpredictable.

Cost of Ownership

Cost of OwnershipTime it takes to maintain the

owned application logic to support its ongoing change.

Attributes

— Intermediate between Cost of Introduction & Cost of Change

— Has no direct correlation to business value

— Has direct correlation to LOC

Dynamics

— Always invisible

— Always relevant

— Stable over time, but adds up

Cost of Ownership is the cost you pay for the right to change a

particular part (module, class, method) of application

continuosly and sustainably.

Testing

Unit testing

Refactoring

Introducing Cost of Ownership allows you to balance two other

costs.

Cost of Introduction and Change

Cost of Introduction and Ownership

Cost of Ownership effect on Cost of Change curve

Emergent DesignUsual result of ongoing

ownership.

Cost of Ownership of everything

Cost of Ownership of everything

Owning everything fails to achieve ever-increasing benefits,

because you rarely need to change the entire system.

End-to-end testing is owning everything

Cost of Ownership of the wrong thing

Ownership wouldn't help if you're owning the wrong thing.

Exclusive end-to-end testing is owning the wrong thing

You do want to own everything worth owning.

But you don't know what's worth owning at the beginning of the

project.

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.

Gaming Software Costs

Own only the logic you need to change.

Write only the logic you need to own.

Own everything you write.

Own everything you write.Try to not write anything.

Own everything you write.Try to not write anything.

Reuse everything else.

1. Document the need

2. Spike - Experiment with tools available

3. Document changes & constraints

4. Stabilise - Claim ownership when the thing grows outside of tool boundaries

5. Isolate Religiously

Steps

1. Document the need

2. Spike

3. Document changes & constraints

4. Stabilise

5. Isolate Religiously

Unit testing is owning

Refactoring is owning

Test Driven Development is an ownership technique

Gaming Software Costs

1. Document

2. Spike & Stabilise

3. Use TDD for stabilisation

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

Thank you!

Questions?Please, leave feedback: https://joind.in/15022