Heim >php教程 >PHP开发 >Setzen Sie rein funktionale Programmierung mit Bedacht ein

Setzen Sie rein funktionale Programmierung mit Bedacht ein

高洛峰
高洛峰Original
2016-11-22 12:25:551226Durchsuche

Funktionale Programmierung kann die Komplexität des Programms reduzieren: Eine Funktion sieht aus wie eine mathematische Formel. Das Erlernen der funktionalen Programmierung kann Ihnen helfen, einfacheren Code mit weniger Fehlern zu schreiben.

Reine Funktion

Eine reine Funktion kann als eine Funktion verstanden werden, die die gleiche Eingabe hat und die gleiche Ausgabe haben muss, ohne beobachtbare Nebenwirkungen

//pure
function add(a + b) {
  return a + b;
}

Die Oben ist eine reine Funktion, die nicht von anderen Variablen als der Funktion abhängt oder deren Status ändert und immer die gleiche Ausgabe für die gleiche Eingabe zurückgeben kann.

//impure
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum; // 如果minimum改变,函数结果也会改变
}

Diese Funktion ist keine reine Funktion, da sie auf einem externen veränderlichen Zustand basiert

Wenn wir die Variable innerhalb der Funktion verschieben, wird sie zu einer reinen Funktion, also stellen wir sicher, dass Die Funktion kann das Alter jedes Mal korrekt vergleichen.

var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

Eine reine Funktion hat keine Nebenwirkungen. Einige Dinge, die Sie beachten müssen, sind, dass sie nicht auf den Systemstatus außerhalb der Funktion zugreift.

Änderungen werden übergeben in als Parameter Objekt

HTTP-Anfrage initiieren

Benutzereingaben beibehalten

DOM abfragen

Kontrollierte Mutation

Sie müssen einiges beachten Mutator-Methoden mutieren Arrays und Objekte. Sie müssen beispielsweise den Unterschied zwischen Splice und Slice kennen.

Wenn wir die Verwendung von Mutatormethoden für die an die Funktion übergebenen Objekte vermeiden, werden unsere Programme leichter zu verstehen und wir können vernünftigerweise davon ausgehen, dass unsere Funktionen nichts außerhalb der Funktion ändern.
//impure, splice 改变了原数组
var firstThree = function(arr) {
  return arr.splice(0,3);
}

//pure, slice 返回了一个新数组
var firstThree = function(arr) {
  return arr.slice(0,3);
}

Vorteile reiner Funktionen
let items = ['a', 'b', 'c'];
let newItems = pure(items);
//对于纯函数items始终应该是['a', 'b', 'c']

Im Vergleich zu unreinen Funktionen haben reine Funktionen die folgenden Vorteile:

Sie sind einfacher zu testen, da ihre einzige Verantwortung auf der Berechnungsausgabe basiert bei Eingabe

Ergebnisse können zwischengespeichert werden, da die gleiche Eingabe immer die gleiche Ausgabe erhält

Selbstdokumentierend, da Funktionsabhängigkeiten klar sind

Einfacher aufzurufen, da Sie nicht Wir müssen uns keine Gedanken über die Nebenwirkungen der Funktion machen

Da die Ergebnisse reiner Funktionen zwischengespeichert werden können, können wir sie uns merken, sodass komplexe und teure Vorgänge beim Aufruf nur einmal ausgeführt werden müssen. Beispielsweise kann das Zwischenspeichern der Ergebnisse eines großen Abfrageindex die Programmleistung erheblich verbessern.

Unvernünftige Programmierung reiner Funktionen

Die Verwendung reiner Funktionen kann die Komplexität des Programms erheblich reduzieren. Wenn wir jedoch zu viele abstrakte Konzepte der funktionalen Programmierung verwenden, wird auch unsere funktionale Programmierung sehr schwer zu verstehen sein.

Nehmen Sie sich eine Minute, um den obigen Code zu verstehen.
import _ from 'ramda';
import $ from 'jquery';

var Impure = {
  getJSON: _.curry(function(callback, url) {
    $.getJSON(url, callback);
  }),

  setHtml: _.curry(function(sel, html) {
    $(sel).html(html);
  })
};

var img = function (url) {
  return $(&#39;<img />&#39;, { src: url });
};

var url = function (t) {
  return &#39;http://api.flickr.com/services/feeds/photos_public.gne?tags=&#39; +
    t + &#39;&format=json&jsoncallback=?&#39;;
};

var mediaUrl = _.compose(_.prop(&#39;m&#39;), _.prop(&#39;media&#39;));
var mediaToImg = _.compose(img, mediaUrl);
var images = _.compose(_.map(mediaToImg), _.prop(&#39;items&#39;));
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
app("cats");

Solange Sie nicht mit diesen Konzepten der funktionalen Programmierung (Currying, Komposition und Requisiten) vertraut sind, ist es schwierig, den obigen Code zu verstehen. Im Vergleich zum rein funktionalen Ansatz ist der folgende Code einfacher zu verstehen und zu ändern, er beschreibt das Programm klarer und erfordert weniger Code.

Der Parameter der App-Funktion ist eine Tag-Zeichenfolge

JSON-Daten von Flickr abrufen

URLs aus den zurückgegebenen Daten extrahieren

Erstellen Sie a1f02c36ba31691bcfe87b2722de723b Knotenarray

fügen Sie sie in das Dokument ein

oder Sie können Fetch und Promise für bessere asynchrone Vorgänge verwenden.
var app = (tags) => {
  let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`;
  $.getJSON(url, (data) => {
    let urls = data.items.map((item) => item.media.m)
    let images = urls.map(url) => $(&#39;<img />&#39;, {src:url}) );
    
    $(document.body).html(images);
  })
}
app("cats");

Ajax-Anfragen und DOM-Operationen sind nicht rein, aber wir können die verbleibenden Operationen in reine Funktionen umwandeln und die zurückgegebenen JSON-Daten in ein Array von Bildknoten konvertieren.
let flickr = (tags)=> {
  let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`
  
  return fetch(url)
    .then((resp)=> resp.json())
    .then((data)=> {
      let urls = data.items.map((item)=> item.media.m )
      let images = urls.map((url)=> $(&#39;<img />&#39;, { src: url }) )

      return images
  })
}
flickr("cats").then((images)=> {
  $(document.body).html(images)
})

Unsere Funktion macht zwei Dinge:
let responseToImages = (resp) => {
  let urls = resp.items.map((item) => item.media.m)
  let images = urls.map((url) => $(&#39;<img />&#39;, {src:url}))
  
  return images
}

Konvertiert die zurückgegebenen Daten in URLs

Konvertiert URLs in Bildknoten

Funktion Die traditionelle Methode besteht darin, Trennen Sie die beiden oben genannten Aufgaben und übergeben Sie dann mit Compose das Ergebnis einer Funktion als Parameter an einen anderen Parameter.

compose gibt eine Kombination von Funktionen zurück. Jede Funktion verwendet das Ergebnis der letzteren Funktion als eigenen Eingabeparameter.
let urls = (data) => {
  return data.items.map((item) => item.media.m)
}
let images = (urls) => {
  return urls.map((url) => $(&#39;<img />&#39;, {src: url}))
}
let responseToImages = _.compose(images, urls)

Was Compose hier tut, besteht darin, die Ergebnisse von URLs einschließlich der zu übergeben Die Bilderfunktion

gibt uns die Möglichkeit, sie in Zukunft wiederzuverwenden, indem wir den Code in reine Funktionen umwandeln, wodurch sie einfacher zu testen und selbstdokumentierbar sind. Das Schlimme ist, dass die übermäßige Verwendung dieser funktionalen Abstraktionen (wie im ersten Beispiel) die Dinge komplizierter macht, was nicht das ist, was wir wollen. Die wichtigste Frage, die Sie sich bei der Umgestaltung von Code stellen sollten, ist:
let responseToImages = (data) => {
  return images(urls(data))
}

Wird der Code dadurch leichter lesbar und verständlicher?

基本功能函数

我并不是要诋毁函数式编程。每个程序员都应该齐心协力去学习基础函数,这些函数让你在编程过程中使用一些抽象出的一般模式,写出更加简洁明了的代码,或者像Marijn Haverbeke说的

一个程序员能够用常规的基础函数武装自己,更重要的是知道如何使用它们,要比那些苦思冥想的人高效的多。--Eloquent JavaScript, Marijn Haverbeke

这里列出了一些JavaScript开发者应该掌握的基础函数
Arrays
-forEach
-map
-filter
-reduce

Functions
-debounce
-compose
-partial
-curry

Less is More

让我们来通过实践看一下函数式编程能如何改善下面的代码

let items = [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;];
let upperCaseItems = () => {
  let arr = [];
  for (let i=0, ii= items.length; i<ii; i++) {
    let item = items[i];
    arr.push(item.toUpperCase());
  }
  items = arr;
}

共享状态来简化函数

这看起来很明显且微不足道,但是我还是让函数访问和修改了外部的状态,这让函数难以测试且容易出错。

//pure
let upperCaseItems = (items) => {
  let arr = [];
  for (let i =0, ii= items.length; i< ii; i++) {
    let item = items[i];
    arr.push(item.toUpperCase());
  }
  return arr;
}

使用更加可读的语言抽象forEach来迭代

let upperCaseItems = (items) => {
  let arr = [];
  items.forEach((item) => {
    arr.push(item.toUpperCase());
  })
  return arr;
}

使用map进一步简化代码

let upperCaseItems = (items) => {
  return items.map((item) => item.toUpperCase())
}

进一步简化代码

let upperCase = (item) => item.toUpperCase()
let upperCaseItems = (item) => items.map(upperCase)

删除代码直到它不能工作

我们不需要为这种简单的任务编写函数,语言本身就提供了足够的抽象来完成功能

let items = [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]
let upperCaseItems = item.map((item) => item.toUpperCase())

测试

纯函数的一个关键优点是易于测试,所以在这一节我会为我们之前的Flicker模块编写测试。

我们会使用Mocha来运行测试,使用Babel来编译ES6代码。

mkdir test-harness
cd test-harness
npm init -y
npm install mocha babel-register babel-preset-es2015 --save-dev
echo &#39;{ "presets": ["es2015"] }&#39; > .babelrc
mkdir test
touch test/example.js

Mocha提供了一些好用的函数如describe和it来拆分测试和钩子(例如before和after这种用来组装和拆分任务的钩子)。assert是用来进行相等测试的断言库,assert和assert.deepEqual是很有用且值得注意的函数。

让我们来编写第一个测试test/example.js

import assert from &#39;assert&#39;;

describe(&#39;Math&#39;, () => {
  describe(&#39;.floor&#39;, () => {
    it(&#39;rounds down to the nearest whole number&#39;, () => {
      let value = Math.floor(4.24)
      assert(value === 4)
    })
  })
})

打开package.json文件,将"test"脚本修改如下

mocha --compilers js:babel-register --recursive

然后你就可以在命令行运行npm test

Math
  .floor
    ✓ rounds down to the nearest whole number
1 passing (32ms)

Note:如果你想让mocha监视改变,并且自动运行测试,可以在上述命令后面加上-w选项。

mocha --compilers js:babel-register --recursive -w

测试我们的Flicker模块

我们的模块文件是lib/flickr.js

import $ from &#39;jquery&#39;;
import { compose } from &#39;underscore&#39;;

let urls = (data) => {
  return data.items.map((item) => item.media.m)
}

let images = (urls) => {
  return urls.map((url) => $(&#39;<img />&#39;, {src: url})[0] )
}

let responseToImages = compose(images, urls)

let flickr = (tags) => {
  let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`
  
  return fetch(url)
    .then((response) => reponse.json())
    .then(responseToImages)
}

export default {
  _responseToImages: responseToImages,
  flickr: flickr
}

我们的模块暴露了2个方法:一个公有flickr和一个私有函数_responseToImages,这样就可以独立的测试他们。

我们使用了一组依赖:jquery,underscore和polyfill函数fetch和Promise。为了测试他们,我们使用jsdom来模拟DOM对象window和document,使用sinon包来测试fetch api。

npm install jquery underscore whatwg-fetch es6-promise jsdom sinon --save-dev
touch test/_setup.js

打开test/_setup.js,使用全局对象来配置jsdom

global.document = require(&#39;jsdom&#39;).jsdom(&#39;<html></html>&#39;);
global.window = document.defaultView;
global.$ = require(&#39;jquery&#39;)(window);
global.fetch = require(&#39;whatwg-fetch&#39;).fetch;

我们的测试代码在test/flickr.js,我们将为函数的输出设置断言。我们"stub"或者覆盖全局的fetch方法,来阻断和模拟HTTP请求,这样我们就可以在不直接访问Flickr api的情况下运行我们的测试。

import assert from &#39;assert&#39;;
import Flickr from &#39;../lib/flickr&#39;;
import sinon from &#39;sinon&#39;;
import { Promise } from &#39;es6-promise&#39;;
import { Response } from &#39;whatwg-fetch&#39;;

let sampleResponse = {
  items: [{
    media: { m: &#39;lolcat.jpg&#39; }
  }, {
    media: {m: &#39;dancing_pug.gif&#39;}
  }]
}

//实际项目中我们会将这个test helper移到一个模块里
let jsonResponse = (obj) => {
  let json = JSON.stringify(obj);
  var response = new Response(json, {
    status: 200,
    headers: {&#39;Content-type&#39;: &#39;application/json&#39;}
  });
  return Promise.resolve(response);
}


describe(&#39;Flickr&#39;, () => {
  describe(&#39;._responseToImages&#39;, () => {
    it("maps response JSON to a NodeList of <img>", () => {
      let images = Flickr._responseToImages(sampleResponse);
      
      assert(images.length === 2);
      assert(images[0].nodeName === &#39;IMG&#39;);
      assert(images[0].src === &#39;lolcat.jpg&#39;);
    })
  })
  
  describe(&#39;.flickr&#39;, () => {
    //截断fetch 请求,返回一个Promise对象
    before(() => {
      sinon.stub(global, &#39;fetch&#39;, (url) => {
        return jsonResponse(sampleResponse)
      })
    })
    
    after(() => {
      global.fetch.restore();
    })
    
    it("returns a Promise that resolve with a NodeList of <img>", (done) => {
      Flickr.flickr(&#39;cats&#39;).then((images) => {
        assert(images.length === 2);
        assert(images[1].nodeName === &#39;IMG&#39;);
        assert(images[1].src === &#39;dancing_pug.gif&#39;);
        done();
      })
    })
  })  
  
})

运行npm test,会得到如下结果:

Math
  .floor
    ✓ rounds down to the nearest whole number

Flickr
  ._responseToImages
    ✓ maps response JSON to a NodeList of <img>
  .flickr
    ✓ returns a Promise that resolves with a NodeList of <img>

3 passing (67ms)

到这里,我们已经成功的测试了我们的模块以及组成它的函数,学习到了纯函数以及如何使用函数组合。我们知道了纯函数与不纯函数的区别,知道纯函数更可读,由小函数组成,更容易测试。相比于不太合理的纯函数式编程,我们的代码更加可读、理解和修改,这也是我们重构代码的目的。

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn