Coding for Scale and Sanity

Preview:

DESCRIPTION

Presentation for Drupaldelphia 2014. Given by Jim Keller of EasternStandard (easternstandard.com). Description: No developer in history had enough time and enough up-front information to make perfectly scalable architecture decisions, get everything right the first time, and craft all of their code exquisitely right out of the gate. Coding is an organic process, and often one that's driven by changing requirements, dreadful deadlines, and unreliable third parties. It's a fact of our lives: you will inevitably end up writing code you're not proud of because you needed to get something done in a pinch. That said, the tradeoff between speed, flexibility, and quality doesn't have to be as drastic as you might think. In this session, I will share a few methodologies and tricks for writing quick, flexible code that doesn't lock you into technical debt and doesn't require you to sacrifice your dignity as a software developer. Also included are some general tips and techniques for writing scalable code that will help future-you not hate current-you for some of the decisions you've been making.

Citation preview

Coding for Scale and SanityWriting code you won’t regret later

Jim KellerPartner, Technology Director

JimKellerES

jimk@easternstandard.com

http://easternstandard.com

Lamplighter

2001-2009

The natural state of codeis that it is constantly in flux

Code is split into core and crust

A misconception?

Code that was written

well

Code that was written

quickly

A note aboutPreferences

( or: please don’tyell at me onTwitter )

Objects. Use them, even within procedural code.

function _some_module_function() { try { require_once( dirname(__FILE__) . DIRECTORY_SEPARATOR . 'my_class.php' ); $obj = new MyClass(); $obj->some_function(); } catch( Exception $e ) { watchdog( __FUNCTION__, $e->getMessage(), array(), WATCHDOG_ALERT ); return false; } }

beware of function definitions like:check_access( $role_id )get_token()

Not only are they (probably) too ambiguous, but they’re titled backwards.

Nomenclature

access_check( $role_id )token_get() or token_generate()

<?php

class MyAuthenticationClass { public function access_check_by_role_id( $role_id ); public function access_check_by_uid( $uid );

public function token_generate(); public function token_get_existing(); public function token_save();

}

?>

<?php

class MyAuthenticationClass {

public $access_is_admin = false; public $access_force_override = false;

public $token_serialize = false; public $token_check_cookie = true;

public function access_check_by_role_id( $role_id ); public function access_check_by_uid( $uid );

public function token_generate(); public function token_get_existing(); public function token_save(); }

?>

Regarding Exceptions…

Use exceptions for error handling. Lots of them.

Throw ‘em, catch ‘em, handle them.

Don’t return “false” or “0” on error.

public function count_jawns() {

$count = some_other_func(); // zero might be a valid count... // so how will I know if this function // failed if some_other_function() returned false?

return $count; }

Check your function & method arguments

Check your arguments to make sure you got what you think you’ve got.

If you’re expecting a uid, check to make sure it looks like a uid.

This can help against security compromises (e.g. SQL injection), but more likely it’s just going to help you troubleshoot faster.

Check your arguments before you try to use them.

public function access_check_by_uid( $uid, $args = array() ) { if ( !is_numeric($uid) ) { throw new InvalidArgumentException( 'non-numeric UID found in ' . __METHOD__ ); }

// stuff

}

Don’t be afraid of utility functions

public function access_check_by_uid( $uid, $args = array() ) {

if ( !self::Uid_is_sane($uid) ) { throw new InvalidArgumentException( 'non-numeric UID found in ' . __METHOD__ ); }

// stuff }

public static function Uid_is_sane( $uid ) {

return is_numeric($uid);

}

Your methods/functions should take as few arguments as possible.

If you need many arguments, you either need to split into multiple functions, or pass an array of optional args.

Don’t do this:

Arguing about arguments

public function access_check ( $uid = null, $entity_id, $user_obj = null, role_id =0, $force_override = false, $check_cookie = true );

All hope is lost when you see a method called like this:

Arguing about arguments

$this->access_check ( null, $id, $user, null, true, false );

Arguing about arguments

public function access_check_by_uid( $uid, $entity_id, $args = array() ) {

if ( !empty($args['force_override']) ) { //do something here } }

public function access_check_by_role_id( $role_id, $entity_id, $args = array() ){

if ( !empty($args['force_override']) ) { //do something here } }

Arguing about arguments

public function access_check_by_uid( $uid, $args = array() ) {

$this->_Access_check( $uid, $args, self::ACCESS_PARAM_UID );

}

public function access_check_by_role_id( $role_id, $args = array() ){

$this->_Access_check( $role_id, $args, self::ACCESS_PARAM_ROLE_ID );}

protected final function _Access_check( $id, $args, $type ) {

if ( $type == self::ACCESS_PARAM_ROLE ) { // do some role stuff here }

if ( $type == self::ACCESS_PARAM_UID ) { // do some uid-only stuff here }

// shared stuff here}

I even have strong feelings about if statements

if ( $app_type == 'xbox' ) { $image_width = 100; $image_height = 120; } else if ( $app_type == 'android' ) { $image_width = 60; $image_height = 80; }

If you find yourself writing long chains of if statements, considering writing a class factory instead.

A more scalable approach

interface ExternalAppOptionsManager() { public $image_height; public $image_width; }

class OptionsManager_android implements ExternalAppOptionsManager(){ public $image_height = 80; public $image_width = 60; }

class OptionsManager_xbox implements ExternalAppOptionsManager() { public $image_height = 120; public $image_width = 100; }

A quick & dirty class factory

$options_class_name = 'OptionsManager_' . $app_type;

if ( !require_once($options_class_name . '.php') || !class_exists($options_class_name) ) { throw new Exception('Invalid class: ' . $options_class_name); }

$options_manager = new $options_class_name; $image_height = $options_manager->image_height; $image_width = $options_manager->image_width;

Conditional objects

if ( $item_type == 't_shirt' ) { if ( $_SESSION['shipping_provider'] == 'USPS' ) { $base_price = 3.00; if ( $_SESSION['shipping_method'] == 'first_class' ) { include( 'usps_funcs.php' ); $real_rate = usps_get_rate( $_SESSION['shipping_method'], $item_weight ); $final_rate = $base_price + $real_rate; } } else if ( $_SESSION['shipping_provider'] == 'UPS' ) { $base_price = 3.00; if ( $_SESSION['shipping_method'] = '2_day' ) { include( 'ups_funcs.php' ); $real_rate = ups_get_rate( $_SESSION['shipping_method'], $item_weight ); } }

// and so forth....

Conditional objects

class ShippingConditional_usps extends ShippingConditional use USPS_API_Class;

protected $_Shipping_method; protected $_Item;

public function pricing_calculate() { $base_price = $this->base_price_by_shipping_method ( $this->_Shipping_method );

$usps = new USPS_Api_Class; $usps->method_set( $this->shipping_method ); $real_price = $usps->rate_calculate( $item->weight );

return $real_price + $base_price; } }

Conditional objects

if ( $item_type == 't_shirt' ) { if ( $_SESSION['shipping_provider'] == 'USPS' ) { $base_price = 3.00; if ( $_SESSION['shipping_method'] == 'first_class' ) { include( 'usps_funcs.php' ); $real_rate = usps_get_rate( $_SESSION['shipping_method'], $item_weight ); $final_rate = $base_price + $real_rate; } } else if ( $_SESSION['shipping_provider'] == 'UPS' ) { $base_price = 3.00; if ( $_SESSION['shipping_method'] = '2_day' ) { include( 'ups_funcs.php' ); $real_rate = ups_get_rate( $_SESSION['shipping_method'], $item_weight ); } }

// and so forth....

A rewrite

$shipping_class_name = "ShippingConditional_{$item_type}";

if ( !class_exists($shipping_class_name) ) { throw new InvalidArgumentException( "Invalid shipping class name: {$shipping_class_name}" ); }

$shipping_obj = new $shipping_class_name;

$shipping_obj->shipping_method_set($_SESSION['chosen_shipping_method']); $shipping_obj->item_set ( $cart_items[$j] ); $shipping_price = $shipping_obj->pricing_calculate();

In Closing

- Assume that your code will have to change. Plan accordingly.

- Learn to identify areas of code that are likely to get messy or change suddenly. Isolate these components in a way that it’s easy to work on them without having to refactor in many places.

- For complex logic, don’t just write the logic in your code like a long narrative story. Break it out into classes and methods that process individual bits of the overall process.

Happy Coding.

@JimKellerES

jimk@easternstandard.com

http://easternstandard.com