Flaskで作ったアプリのコードレビューをしてもらった話
はじめに
初めてのZennへの投稿になります!
以前、ハッカソンで開発したアプリのコードレビューをしていただいたので、そのときに指摘していただいたことのまとめです。
以下のコードを修正していくという流れで進めていこうと思います!
サインアップ機能のためのコード
from backend import db
from backend.common.models.user import User
import datetime
from flask import Blueprint
from flask import jsonify
from flask import request
signup_bp = Blueprint("signup", __name__, url_prefix="/signup")
@signup_bp.route("/", methods=["POST"])
def signup():
email = request.json["email"]
password = request.json["password"]
user = User.query.filter(User.email==email).first()
if user is not None:
return jsonify({
"status": 409,
"message": "This email address is already in use."
}), 409
else:
new_user = User(name="匿名ユーザ", email=email, password=password)
dt = datetime.date.today()
string_date = dt.strftime("%Y.%m.%d")
current_date = string_date[2:]
new_user.create_at = current_date
db.session.add(new_user)
db.session.commit()
return {
"status": 200,
"data": {
"id": new_user.id,
"name": new_user.name,
}
}, 200
Pythonのimport文のコードスタイル
PEP 8では以下のようにimport文を記述するべきだそうです。
Imports should be grouped in the following order:
- Standard library imports.
- Related third party imports.
- Local application/library specific imports.
You should put a blank line between each group of imports.
つまり、ライブラリをimportする際は
- 標準ライブラリ
- サードパーティライブラリ
- ローカルライブラリ
の順でimport文を記述し、1・2・3の間には空白行を記述するべきだと書かれています。
❌ コードレビュー前のimport文
from backend import db
from backend.common.models.user import User
import datetime
from flask import Blueprint
from flask import jsonify
from flask import request
⭕️ コードレビュー後のimport文
import datetime
from flask import Blueprint
from flask import jsonify
from flask import request
from backend import db
from backend.common.models.user import User
手動でもできますが、isortやblackといったフォーマッターを使うのも良いでしょう。
リクエストがJSON形式で送られない場合を考慮
いきなりサインアップの処理を書き始めています。このとき、リクエストがJSON形式であることを期待しています。
しかし、もし、リクエストがJSON形式でなかければ、エラーが起きてしまいます。
そのための、エラーハンドリングをしておくと良いでしょう。
❌ コードレビュー前のコード
def signup():
email = request.json["email"]
# 略
⭕️ コードレビュー後のコード
def signup():
if not request.is_json():
return jsonify({
"status": 404,
"message": "Serve internal error."
}), 404
email = request.json["email"]
メールアドレスとパスワードのバリデーション
バリデーションにはFlask-WTFなどのサードパーティライブラリを使います。
もし、バリデーションを定義しないとメールアドレスやパスワードを空で保存できたり、メールアドレスの形式でないのに(例えば, メールアドレスがexample@gmail@gmail^com)保存できてしまいます。
したがって、バリデーションを行いましょう!
❌ コードレビュー前のコード
def signup():
email = request.json["email"]
password = request.json["password"]
user = User.query.filter(User.email==email).first()
⭕️ コードレビュー後のコード
# 略
from backend.common.models.user import User
from backend.common.validater.loginForm import LoginForm
# 略
def signup():
email = request.json["email"]
password = request.json["password"]
form = LoginForm()
if not form.validate_on_submit():
return jsonify({
"status": 400,
"message": "Email address is invalid or Password is invalid."
}), 400
ビジネスロジックの記述場所
開発したアプリでは、MVCアーキテクチャを採用していました。
ここで、モデルとコントローラの役割と責任について改めて確認しましょう。
モデルの役割と責任
- データの取得・更新・保存
- データの整合性の維持
- データの更新の通知
コントローラの役割と責任
- リクエストの処理
- モデルとビューの橋渡し
(ビューに関しては省略します)
このようなモデルとコントローラの役割を曖昧にして開発していました💦
ビジネスロジックはモデルに記述すべきです。
❌ コードレビュー前のコード
def signup():
email = request.json["email"]
password = request.json["password"]
# ビジネスロジックがコントローラに記述してある
user = User.query.filter(User.email==email).first()
コントローラにビジネスロジックが記述されています。これはFat Controllerにつながるため、よくないコードとされています。
ビジネスロジックはモデルに記述しましょう!!
⭕️ コードレビュー後のコード
def signup():
email = request.json["email"]
password = request.json["password"]
# ビジネスロジックが記述されていないため、処理がわかりやすい!!
user = User.get_user_by_email(email)
class User(db.Model):
# 略
@staticmethod
def get_user_by_email(email: str) -> User | None:
return User.query.filter(User.email==email).first()
ユーザを新規に作成する処理に関しても同様のことが言えます。
❌ コードレビュー前のコード
else:
new_user = User(name="匿名ユーザ", email=email, password=password)
dt = datetime.date.today()
string_date = dt.strftime("%Y.%m.%d")
current_date = string_date[2:]
new_user.create_at = current_date
db.session.add(new_user)
db.session.commit()
⭕️ コードレビュー後のコード
else:
new_user = User.create(email=email, password=password)
class User(db.Model):
# 略
@staticmethod
def create(email: str, password: str, name="匿名ユーザ") -> User:
user = User(email=email, password=password, name=name)
db.session.add(user)
db.session.commit()
return user
また、このようにすることでdatetime
やdb
をimportする必要はなくなりました。
コントローラにビジネスロジックを書くのはやめましょう!
モデルに記述してください!!
early return
signup
は関数なので、この中で記述しているif文に関してはearly returnをすると良いでしょう。
❌ コードレビュー前のコード
if user is not None:
return jsonify({
"status": 409,
"message": "This email address is already in use."
}), 409
else:
new_user = User(name="匿名ユーザ", email=email, password=password)
# 略
return {
"status": 200,
"data": {
"id": new_user.id,
"name": new_user.name,
}
}, 200
⭕️ コードレビュー後のコード
if user is not None:
return jsonify({
"status": 409,
"message": "This email address is already in use."
}), 409
new_user = User.create(email=email, password=password)
return {
"status": 200,
"data": {
"id": new_user.id,
"name": new_user.name,
}
}, 200
(ここに関しては少し考えればわかったはずですが、徹夜で開発したせいもあり、気付きませんでした💦)
まとめ
今回はYouTubeで活動されている方に依頼をして、コードレビューしていただきました。
改めて、指摘されたことをまとめると基本的なことが多いように感じます。いかに基本をおろそかにして開発していたかがわかります。
何をするにも基本が大事ですね!
以下はコードレビュー前のコードとコードレビュー後のコードです。
コードレビュー前のコード
from backend import db
from backend.common.models.user import User
import datetime
from flask import Blueprint
from flask import jsonify
from flask import request
signup_bp = Blueprint("signup", __name__, url_prefix="/signup")
@signup_bp.route("/", methods=["POST"])
def signup():
email = request.json["email"]
password = request.json["password"]
user = User.query.filter(User.email==email).first()
if user is not None:
return jsonify({
"status": 409,
"message": "This email address is already in use."
}), 409
else:
new_user = User(name="匿名ユーザ", email=email, password=password)
dt = datetime.date.today()
string_date = dt.strftime("%Y.%m.%d")
current_date = string_date[2:]
new_user.create_at = current_date
db.session.add(new_user)
db.session.commit()
return {
"status": 200,
"data": {
"id": new_user.id,
"name": new_user.name,
}
}, 200
コードレビュー後のコード
from flask import Blueprint
from flask import jsonify
from flask import request
from backend.common.models.user import User
from backend.common.validater.loginForm import LoginForm
signup_bp = Blueprint("signup", __name__, url_prefix="/signup")
@signup_bp.route("/", methods=["POST"])
def signup():
if not request.is_json():
return jsonify({
"status": 404,
"message": "Serve internal error."
}), 404
email = request.json["email"]
password = request.json["password"]
form = LoginForm()
if not form.validate_on_submit():
return jsonify({
"status": 400,
"message": "Email address is invalid or Password is invalid."
}), 400
user = User.get_user_by_email(email)
if user is not None:
return jsonify({
"status": 409,
"message": "This email address is already in use."
}), 409
new_user = User.create(email=email, password=password)
return {
"status": 200,
"data": {
"id": new_user.id,
"name": new_user.name,
}
}, 200
参考文献
Discussion