Method chaining

2017-01-05 22:20:16

De flesta utvecklare som har använt sig av något framework eller library, har högst antagligen nån gång tillämpat method-chaining utan att kanske ens ha noterat nåt speciellt med detta.
 method chaining
Method-chaining är kort sagt möjligheten att kombinera flera metoder efter varandra utan att behöva mellanlagra returvärdet eller att behöva referera till ett objekt vid varje metodanrop. Detta gör koden lättläst och enligt många också "snygg" kod.

Exempel på method chaining:

jQuery (JS):

$("body").find("div.row").find("span").css({color: "red"});

Javascript har även nativa metoder som tillämpar chaining:

var date = "2016-12-28";
var formattedDate = date.split("-").reverse().join(".");

Godtycklig ORM i PHP

$query = Model_Article::query()->where('category_id', 1)->order_by('date', 'desc');

Detta inlägg tänkte jag ägna åt att titta närmare på hur man strukturerar sina kod-klasser för att kunna tillämpa method chaining. Notera att dessa principer inte är något som man har nytta av i alla hänseenden, utan lämpar sig för specifika uppgifter såsom stränghantering, XML och HTML-DOM manipulering m.m. 

Exempel

Här kommer ett praktiskt exempel på hur man får till stånd method-chaining i en PHP-klass, och slutligen hur man tillämpar denna klass.

Jag har valt att använda method-chaining för att hantera en XML-fil, som jag filtrerar, tranformerar till en vektor, formaterar och slutligen JSON-encode:ar, som givetvis returneras med ändmålsenlig header-information.

Det centrala att ta fasta på här är att man mellanlagrar resultatet från metoderna i en klass-variabel och returnerar $this det vill säga en referens till objekt instansen.

XML-DATA

Här har vi en restaurang meny i XML-format: 


<?xml version="1.0" encoding="UTF-8"?>
<food_menu>
<food>
    <name>Spenatsoppa</name>
    <price>4.90</price>
    <description>Spenatsoppa med ägg</description>
    <calories>100</calories>
</food>
<food>
    <name>Vegetarisk korv</name>
    <price>6.50</price>
    <description>Grillad vege korv</description>
    <calories>20</calories>
</food>
<food>
    <name>Pepperoni pizza</name>
    <price>8.90</price>
    <description>Bästa pizzan i stan</description>
    <calories>400</calories>
</food>
<food>
    <name>Ärtsoppa</name>
    <price>3.50</price>
    <description>Ärtsoppa, varje torsdag!</description>
    <calories>100</calories>
</food>
<food>
    <name>Matti's special</name>
    <price>10.80</price>
    <description>900g Friterad bacon med äggröra och stekt potatis</description>
    <calories>2000</calories>
</food>
</food_menu>

PHP Kod


<?php

/**
* XML class is a wrapper class for loading XML data and parsing. Just example
* code for part of blog post at http://mrenlund.com/blog/method-chaining/
* 
* @author Mathias Renlund <renlund86@gmail.com>
*/
class XML
{
    public $instance;
    
    public function __construct($filename)
    {
        $file = file_get_contents($filename);
        $this->instance = new SimpleXMLElement($file);
    }
}

/**
* SuperLib class is just example code to showing how method chaining works and what
* it could be used for. Part of blog post at http://mrenlund.com/blog/method-chaining/
* 
* @author Mathias Renlund <renlund86@gmail.com>
*/
class SuperLib
{

    private $item;
    private $xml;
    
    public function __construct(SimpleXMLElement $inputXML){
        $this->xml = $inputXML;
    }

    /**
    * Method for finding node with xpath 
    *
    * @param string $find regex search string
    * @return obj reference
    */
    
    public function find($find)
    {
        $result     = $this->xml->xpath("//".$find);
        $this->item = $result;
        return $this;
    }
    
    /**
    * Method for sorting multidimensional array 
    *
    * @param Array $options sorting options
    * @return obj reference
    */
    
    public function sort($options) 
    {
        
        $field  = $options['field'];
        $type   = $options['type'];
        
        switch($type) {
            case "float":
                usort($this->item, function($a, $b) use($field) {
                    return (float)$a[$field] - (float)$b[$field];
                });
            break;
            case "int":
                usort($this->item, function($a, $b) use($field) {
                    return (int)$a[$field] - (int)$b[$field];
                });
            break;
        }
        
        return $this;
    }
    
    /**
    * Method for encoding target item to JSON-format 
    *
    * @return obj reference
    */
    
    public function asJSON() 
    {
        $this->item = json_encode($this->item);
        return $this;
    }
    
    /**
    * Method for translating target item to an array 
    *
    * @return obj reference
    */
    
    public function asArray() 
    {
        $arr = array();
        
            if(count($this->item) > 0) {
                foreach($this->item as $row) {
                    $arr[] = (array)$row;
                }
            }
            
        $this->item = $arr;
        return $this;
    }
    
    /**
    * Method for echoing out result along with appropriate HTTP-header
    *
    * @param string $headerType HTTP-header definition
    * @return null
    */
    
    public function output($headerType) 
    {
        header($headerType);
        echo $this->item;
    }
    
    /**
    * Method for formatting given field to currency with optional currency symbol
    *
    * @param Array $options formatting option parameters
    * @return obj reference
    */
    
    public function formatCurrency($options) 
    {
        
        $symbol = $options['symbol'];
        $field  = $options['field'];
        
            foreach($this->item as &$row) {
                $row[$field] = number_format((float)$row[$field],2,",","").$symbol;
            }
            
        return $this;
    }

}

Exempel tillämpning:


$xml = new XML("produkter.xml");
$lib = new SuperLib($xml->instance);

$lib->find("food[price>5.00]")
    ->asArray()
    ->sort(["field" => "price", "type" => "float"])
    ->formatCurrency(["field" => "price", "symbol" => "€"])
    ->asJSON()
    ->output('Content-Type: application/json');

Snyggt och lättläst eller hur?

Resultat i webbläsaren:

json

 

Denna syntax-stil är inget som är specifikt förbehållet webbutveckling utan kan användas i princip i vilket språk som helst. Denna wikipedia sida har samlat flera exempel: https://en.wikipedia.org/wiki/Method_chaining