min-maxing software costs - laracon eu 2015
TRANSCRIPT
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