Après une récente comparaison de Python, Ruby et Golang pour une application en ligne de commande, j'ai décidé d'utiliser le même modèle pour comparer la création d'un service Web simple. J'ai sélectionné Flask (Python), Sinatra (Ruby) et Martini (Golang) pour cette comparaison. Oui, il existe de nombreuses autres options pour les bibliothèques d'applications Web dans chaque langue, mais j'ai trouvé que ces trois options se prêtaient bien à la comparaison.
Aperçus de la bibliothèque
Voici une comparaison de haut niveau des bibliothèques par Stackshare.
Flacon (Python)
Flask est un micro-framework pour Python basé sur Werkzeug, Jinja2 et les bonnes intentions.
Pour des applications très simples, telles que celle présentée dans cette démo, Flask est un excellent choix. L'application Flask de base ne contient que 7 lignes de code (LOC) dans un seul fichier source Python. L'attrait de Flask par rapport aux autres bibliothèques Web Python (telles que Django ou Pyramid) est que vous pouvez commencer petit et développer une application plus complexe selon vos besoins.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Sinatra (Rubis)
Sinatra est un DSL permettant de créer rapidement des applications Web en Ruby avec un minimum d'effort.
Tout comme Flask, Sinatra est idéal pour les applications simples. L'application Sinatra de base ne contient que 4 LOC dans un seul fichier source Ruby. Sinatra est utilisé à la place de bibliothèques telles que Ruby on Rails pour la même raison que Flask - vous pouvez commencer petit et étendre l'application selon vos besoins.
require 'sinatra'
get '/hi' do
"Hello World!"
end
Martini (Golang)
Martini est un package puissant permettant d'écrire rapidement des applications/services Web modulaires dans Golang.
Martini est livré avec quelques piles de plus que Sinatra et Flask, mais reste très léger pour commencer - seulement 9 LOC pour l'application de base. Martini a fait l'objet de critiques de la part de la communauté Golang, mais possède toujours l'un des projets Github les mieux notés de tous les frameworks Web Golang. L'auteur de Martini a répondu directement à la critique ici. Certains autres frameworks incluent Revel, Gin et même la bibliothèque intégrée net/http.
package main
import "github.com/go-martini/martini"
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}
Une fois les bases éliminées, créons une application !
Description des services
Le service créé fournit une application de blog très basique. Les routes suivantes sont construites :
GET /
:renvoie le blog (en utilisant un modèle pour le rendu).GET /json
:renvoie le contenu du blog au format JSON.POST /new
:Ajouter un nouvel article (titre, résumé, contenu) au blog.
L'interface externe du service de blog est exactement la même pour chaque langue. Pour plus de simplicité, MongoDB sera utilisé comme magasin de données pour cet exemple car il est le plus simple à configurer et nous n'avons pas du tout à nous soucier des schémas. Dans une application normale de type "blog", une base de données relationnelle serait probablement nécessaire.
Ajouter une publication
POST /new
$ curl --form title='Test Post 1' \
--form summary='The First Test Post' \
--form content='Lorem ipsum dolor sit amet, consectetur ...' \
http://[IP]:[PORT]/new
Afficher le HTML
GET /
Afficher le JSON
GET /json
[
{
content:"Lorem ipsum dolor sit amet, consectetur ...",
title:"Test Post 1",
_id:{
$oid:"558329927315660001550970"
},
summary:"The First Test Post"
}
]
Structure de candidature
Chaque application peut être décomposée en composants suivants :
Configuration de l'application
- Initialiser une application
- Exécuter l'application
Demande
- Définir les itinéraires sur lesquels un utilisateur peut demander des données (GET)
- Définir les itinéraires sur lesquels un utilisateur peut soumettre des données (POST)
Réponse
- Rendre JSON (
GET /json
) - Afficher un modèle (
GET /
)
Base de données
- Initialiser une connexion
- Insérer des données
- Récupérer des données
Déploiement d'applications
- Docker !
Le reste de cet article comparera chacun de ces composants pour chaque bibliothèque. Le but n'est pas de suggérer que l'une de ces bibliothèques est meilleure que l'autre, mais de fournir une comparaison spécifique entre les trois outils :
- Flacon (Python)
- Sinatra (rubis)
- Martini (Golang)
Configuration du projet
Tous les projets sont amorcés à l'aide de docker et de docker-compose. Avant de plonger dans la façon dont chaque application est amorcée sous le capot, nous pouvons simplement utiliser docker pour que chacune soit opérationnelle exactement de la même manière - docker-compose up
Sérieusement, c'est ça ! Maintenant pour chaque application il y a un Dockerfile
et un docker-compose.yml
fichier qui spécifie ce qui se passe lorsque vous exécutez la commande ci-dessus.
Python (flacon) - Dockerfile
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
Ce Dockerfile
dit que nous partons d'une image de base avec Python 3.4 installé, en ajoutant notre application au /app
répertoire et en utilisant pip pour installer nos exigences d'application spécifiées dans requirements.txt
.
Rubis (sinatra)
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
Ce Dockerfile
dit que nous partons d'une image de base avec Ruby 2.2 installé, en ajoutant notre application au /app
répertoire et en utilisant bundler pour installer nos exigences d'application spécifiées dans le Gemfile
.
Golang (martini)
FROM golang:1.3
ADD . /go/src/github.com/kpurdon/go-blog
WORKDIR /go/src/github.com/kpurdon/go-blog
RUN go get github.com/go-martini/martini && \
go get github.com/martini-contrib/render && \
go get gopkg.in/mgo.v2 && \
go get github.com/martini-contrib/binding
Ce Dockerfile
dit que nous partons d'une image de base avec Golang 1.3 installé, en ajoutant notre application au /go/src/github.com/kpurdon/go-blog
répertoire et obtenir toutes nos dépendances nécessaires en utilisant le go get
commande.
Initialiser/exécuter une application
Python (flacon) - app.py
# initialize application
from flask import Flask
app = Flask(__name__)
# run application
if __name__ == '__main__':
app.run(host='0.0.0.0')
$ python app.py
Rubis (Sinatra) - app.rb
# initialize application
require 'sinatra'
$ ruby app.rb
Golang (Martini) - app.go
// initialize application
package main
import "github.com/go-martini/martini"
import "github.com/martini-contrib/render"
func main() {
app := martini.Classic()
app.Use(render.Renderer())
// run application
app.Run()
}
$ go run app.go
Définir un itinéraire (GET/POST)
Python (flacon)
# get
@app.route('/') # the default is GET only
def blog():
# ...
#post
@app.route('/new', methods=['POST'])
def new():
# ...
Rubis (Sinatra)
# get
get '/' do
# ...
end
# post
post '/new' do
# ...
end
Golang (Martini)
// define data struct
type Post struct {
Title string `form:"title" json:"title"`
Summary string `form:"summary" json:"summary"`
Content string `form:"content" json:"content"`
}
// get
app.Get("/", func(r render.Render) {
// ...
}
// post
import "github.com/martini-contrib/binding"
app.Post("/new", binding.Bind(Post{}), func(r render.Render, post Post) {
// ...
}
Rendre une réponse JSON
Python (flacon)
Flask fournit une méthode jsonify() mais comme le service utilise MongoDB, l'utilitaire mongodb bson est utilisé.
from bson.json_util import dumps
return dumps(posts) # posts is a list of dicts [{}, {}]
Rubis (Sinatra)
require 'json'
content_type :json
posts.to_json # posts is an array (from mongodb)
Golang (Martini)
r.JSON(200, posts) // posts is an array of Post{} structs
Rendre une réponse HTML (modèle)
Python (flacon)
return render_template('blog.html', posts=posts)
<!doctype HTML>
<html>
<head>
<title>Python Flask Example</title>
</head>
<body>
{% for post in posts %}
<h1> {{ post.title }} </h1>
<h3> {{ post.summary }} </h3>
<p> {{ post.content }} </p>
<hr>
{% endfor %}
</body>
</html>
Rubis (Sinatra)
erb :blog
<!doctype HTML>
<html>
<head>
<title>Ruby Sinatra Example</title>
</head>
<body>
<% @posts.each do |post| %>
<h1><%= post['title'] %></h1>
<h3><%= post['summary'] %></h3>
<p><%= post['content'] %></p>
<hr>
<% end %>
</body>
</html>
Golang (Martini)
r.HTML(200, "blog", posts)
<!doctype HTML>
<html>
<head>
<title>Golang Martini Example</title>
</head>
<body>
{{range . }}
<h1>{{.Title}}</h1>
<h3>{{.Summary}}</h3>
<p>{{.Content}}</p>
<hr>
{{ end }}
</body>
</html>
Connexion à la base de données
Toutes les applications utilisent le pilote mongodb spécifique au langage. La variable d'environnement DB_PORT_27017_TCP_ADDR
est l'IP d'un conteneur Docker lié (l'IP de la base de données).
Python (flacon)
from pymongo import MongoClient
client = MongoClient(os.environ['DB_PORT_27017_TCP_ADDR'], 27017)
db = client.blog
Rubis (Sinatra)
require 'mongo'
db_ip = [ENV['DB_PORT_27017_TCP_ADDR']]
client = Mongo::Client.new(db_ip, database: 'blog')
Golang (Martini)
import "gopkg.in/mgo.v2"
session, _ := mgo.Dial(os.Getenv("DB_PORT_27017_TCP_ADDR"))
db := session.DB("blog")
defer session.Close()
Insérer des données à partir d'un POST
Python (flacon)
from flask import request
post = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
db.blog.insert_one(post)
Rubis (Sinatra)
client[:posts].insert_one(params) # params is a hash generated by sinatra
Golang (Martini)
db.C("posts").Insert(post) // post is an instance of the Post{} struct
Récupérer des données
Python (flacon)
posts = db.blog.find()
Rubis (Sinatra)
@posts = client[:posts].find.to_a
Golang (Martini)
var posts []Post
db.C("posts").Find(nil).All(&posts)
Déploiement d'applications (Docker !)
Une excellente solution pour déployer toutes ces applications consiste à utiliser docker et docker-compose.
Python (flacon)
Dockerfile
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
docker-compose.yml
web:
build: .
command: python -u app.py
ports:
- "5000:5000"
volumes:
- .:/app
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Rubis (Sinatra)
Dockerfile
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
docker-compose.yml
web:
build: .
command: bundle exec ruby app.rb
ports:
- "4567:4567"
volumes:
- .:/app
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Golang (Martini)
Dockerfile
FROM golang:1.3
ADD . /go/src/github.com/kpurdon/go-todo
WORKDIR /go/src/github.com/kpurdon/go-todo
RUN go get github.com/go-martini/martini && go get github.com/martini-contrib/render && go get gopkg.in/mgo.v2 && go get github.com/martini-contrib/binding
docker-compose.yml
web:
build: .
command: go run app.go
ports:
- "3000:3000"
volumes: # look into volumes v. "ADD"
- .:/go/src/github.com/kpurdon/go-todo
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Conclusion
Pour conclure, jetons un coup d'œil à ce que je pense être quelques catégories où les bibliothèques présentées se séparent les unes des autres.
Simplicité
Alors que Flask est très léger et se lit clairement, l'application Sinatra est la plus simple des trois à 23 LOC (contre 46 pour Flask et 42 pour Martini). Pour ces raisons, Sinatra est le gagnant dans cette catégorie. Il convient de noter cependant que la simplicité de Sinatra est due à plus de "magie" par défaut - par exemple, un travail implicite qui se produit dans les coulisses. Pour les nouveaux utilisateurs, cela peut souvent prêter à confusion.
Voici un exemple spécifique de "magie" à Sinatra :
params # the "request.form" logic in python is done "magically" behind the scenes in Sinatra.
Et le code Flask équivalent :
from flask import request
params = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
Pour les débutants en programmation, Flask et Sinatra sont certainement plus simples, mais pour un programmeur expérimenté ayant passé du temps dans d'autres langages à typage statique, Martini fournit une interface assez simpliste.
Documents
La documentation Flask était la plus simple à rechercher et la plus accessible. Bien que Sinatra et Martini soient tous deux bien documentés, la documentation elle-même n'était pas aussi accessible. Pour cette raison, Flask est le gagnant dans cette catégorie.
Communauté
Flask est le gagnant haut la main dans cette catégorie. La communauté Ruby est le plus souvent dogmatique sur le fait que Rails est le seul bon choix si vous avez besoin de quelque chose de plus qu'un service de base (même si Padrino l'offre en plus de Sinatra). La communauté Golang est encore loin d'un consensus sur un (ou même quelques) frameworks Web, ce qui est normal car le langage lui-même est si jeune. Python a cependant adopté un certain nombre d'approches de développement Web, notamment Django pour des applications Web complètes prêtes à l'emploi et Flask, Bottle, CheryPy et Tornado pour une approche de micro-framework.
Détermination finale
Notez que le but de cet article n'était pas de promouvoir un seul outil, mais plutôt de fournir une comparaison impartiale de Flask, Sinatra et Martini. Cela dit, je sélectionnerais Flask (Python) ou Sinatra (Ruby). Si vous venez d'un langage comme C ou Java, peut-être que la nature typée statiquement de Golang peut vous intéresser. Si vous êtes un débutant, Flask pourrait être le meilleur choix car il est très facile à démarrer et à utiliser et il y a très peu de "magie" par défaut. Ma recommandation est que vous soyez flexible dans vos décisions lors de la sélection d'une bibliothèque pour votre projet.
Des questions? Retour? Veuillez commenter ci-dessous. Merci !
N'hésitez pas non plus à nous faire savoir si vous aimeriez voir quelques points de repère.