上篇文章談到了項目開發關於如何進行功能劃分,如何設計表結構,如何設計API的話題(傳送門:Python + Django 開發實戰(中) )。接下來談談日誌記錄,單元測試和程序調優和重構等內容。
這一篇要談的內容,也是開發中不可忽視的環節。 開發中日誌記錄能幫我們記錄信息定位問題;單元測試幫助我們在迭代開發過程及時發現問題,減少bug的引入; 而程序調優與重構,是一個永恆的話題。
日誌的重要性想必不用多說。 在我看來,日誌的作用主要有兩點:
運營數據支撐。 比如頁面訪問情況,接口調用情況等等,方便運營人員後續的統計分析。
錯誤回溯定位。 捕捉異常,並記錄錯誤信息,方便在系統出現問題時快速進行定位。
<關於日誌的種類>
日誌的種類很多,比如系統日誌, nginx日誌, 網絡日誌, 還有業務日誌等等。
這裡主要討論的是業務日誌,即我們在開發過程中為記錄錯誤信息和業務信息的日誌。
<關於異常捕捉>
異常捕捉是必要的,但是這裡面有兩個小建議:
1. 異常捕捉儘量不影響代碼的可讀性
2. 異常捕捉不要太籠統,儘量分得細緻一點
舉個例子,我比較喜歡下面的書寫方式:
try: <業務邏輯> ...except Table1.DoesNotExist: <錯誤處理>except KeyError: <錯誤處理>except FooException: <錯誤處理>except BarException: <錯誤處理>except Exception: <錯誤處理><關於錯誤碼定義>
一個系統越複雜,越容易出現問題。錯誤碼的用途在於協助定位和修復問題。
最常見的錯誤碼是http狀態碼,比如500代碼軟體內部錯誤,404代碼找不到頁面等
另外各大開發平臺,對應的接口都會有自己的錯誤碼,比如淘寶開放平臺,新浪開放平臺,
微信開放平臺等,也都有自己一套錯誤碼的設計規則。 錯誤碼的設計規則遵循「足夠短」,
「字面容易望文生義」, 「儘量遵循已經達成共識」等。舉個慄子(僅供參考):
CODE_OK= 0 # 成功CODE_ERROR_AUTH_FAIL=40100 # 權限錯誤CODE_ERROR_DB_NOTEXIST=50100 # 資料庫錯誤CODE_ERROR_ACTIVITY_NOTSUPPORT=50200 # 活動業務邏輯錯誤CODE_ERROR_USER_NOTFOUND=50300 # 用戶信息錯誤擴展閱讀:
錯誤碼設計以及 Django 的異常統一處理 https://www.chenshaowen.com/blog/error-code-design-and-unified-processing-in-django.html
<關於日誌配置>
Django使用python自帶的logging 作為日誌列印工具。簡單介紹下logging。
logging 是線程安全的,其主要由4部分組成:
Logger 用戶使用的直接接口,將日誌傳遞給Handler
Handler 控制日誌輸出到哪裡,console,file… 一個logger可以有多個Handler
Filter 控制哪些日誌可以從logger流向Handler
Formatter 控制日誌的格式
在django settings配置文件中,可以進行logging的配置。 一個典型的logging配置示例:
BASE_LOG_DIR = os.path.join(BASE_DIR, "log")LOGGING = { 'version': 1, # 保留字 'disable_existing_loggers': False, # 禁用已經存在的logger實例 # 日誌文件的格式 'formatters': { # 詳細的日誌格式 'standard': { 'format': '[%(asctime)s][%(threadName)s:%(thread)d][task_id:%(name)s][%(filename)s:%(lineno)d]' '[%(levelname)s][%(message)s]' }, # 簡單的日誌格式 'simple': { 'format': '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s' }, # 定義一個特殊的日誌格式 'collect': { 'format': '%(message)s' } }, # 過濾器 'filters': { 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, }, # 處理器 'handlers': { # 在終端列印 'console': { 'level': 'DEBUG', 'filters': ['require_debug_true'], # 只有在Django debug為True時才在屏幕列印日誌 'class': 'logging.StreamHandler', # 'formatter': 'simple' }, # 默認的 'default': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 保存到文件,自動切 'filename': os.path.join(BASE_LOG_DIR, "xxx_info.log"), # 日誌文件 'maxBytes': 1024 * 1024 * 50, # 日誌大小 50M 'backupCount': 3, # 最多備份幾個 'formatter': 'standard', 'encoding': 'utf-8', }, # 專門用來記錯誤日誌 'error': { 'level': 'ERROR', 'class': 'logging.handlers.RotatingFileHandler', # 保存到文件,自動切 'filename': os.path.join(BASE_LOG_DIR, "xxx_err.log"), # 日誌文件 'maxBytes': 1024 * 1024 * 50, # 日誌大小 50M 'backupCount': 5, 'formatter': 'standard', 'encoding': 'utf-8', }, # 專門定義一個收集特定信息的日誌 'collect': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 保存到文件,自動切 'filename': os.path.join(BASE_LOG_DIR, "xxx_collect.log"), 'maxBytes': 1024 * 1024 * 50, # 日誌大小 50M 'backupCount': 5, 'formatter': 'collect', 'encoding': "utf-8" } }, 'loggers': { # 默認的logger應用如下配置 '': { 'handlers': ['default', 'console', 'error'], # 上線之後可以把'console'移除 'level': 'DEBUG', 'propagate': True, # 向不向更高級別的logger傳遞 }, # 名為 'collect'的logger還單獨處理 'collect': { 'handlers': ['console', 'collect'], 'level': 'INFO', } },}在文件中使用logging也很簡單
import logginglogger = logging.getLogger(__name__)logger.debug("這是一個debug級別的日誌。。。。")logger.info("這是一個info級別的日誌。。。。")擴展閱讀:
Django之logging日誌 https://cloud.tencent.com/developer/article/1093273
當我們在談及單元測試時,大家可能會說這個單元測試很好,好在哪裡不知道,為什麼要搞單元測試也不清楚。很多時候我們寧願寫花時間寫業務代碼,手動測試,也不願意寫單元測試。
那單元測試的價值在哪裡呢, 總結以下五點:
確保了代碼在一定設定條件下的正確性,幫助我們很容易的檢查出基本的語法錯誤和一般邏輯錯誤
確保了代碼的改動不會影響現有的功能,這一點在代碼重構的時候特別有用,如果沒有單元測試,改動了一個地方,往往不知道會對原有代碼產生什麼影響
使開發人員更好的理解代碼邏輯,良好的測試建立在對代碼邏輯的理解上
良好的測試要求模塊化, 解耦代碼,這是良好設計的標誌,換句話說測試使你的系統設計更好,如果你的系統不容易測試,那麼就要思考一下系統的設計是否有問題
大大減少花在調試上的時間
前人總結的單元測試的一些最佳實踐:
同樣的輸入要有同樣的輸出
原子性: 要麼成功,要麼失敗,不能部分通過
單一職責: 一個單元測試只測試一個行為
單元測試之間無互相調用
隔離外部調用,不依賴資料庫,網絡,外部文件,本地系統時間,環境變量,如果需要可以進行mock
不要在業務代碼裡插入測試邏輯
django單元測試一般寫在tests.py文件裡, 一個典型的單元測試用例
from django.test import TestCasefrom myapp.models import Animal class AnimalTestCase(TestCase): def setUp(self): Animal.objects.create(name="lion", sound="roar") Animal.objects.create(name="cat", sound="meow") def test_animals_can_speak(self): """Animals that can speak are correctly identified""" lion = Animal.objects.get(name="lion") cat = Animal.objects.get(name="cat") self.assertEqual(lion.speak(), 'The lion says "roar"') self.assertEqual(cat.speak(), 'The cat says "meow"')寫完單元測試了,如何執行呢?可以執行下面的命令
python manage.py test python manage.py test packageA.tests python manage.py test packageA.tests.AnimalTestCase python manage.py test packageA.tests.AnimalTestCase.test_animals_can_speak擴展閱讀:
單元測試及最佳實踐 https://www.jianshu.com/p/3b6daabeb91e
項目開發完不代表一勞永逸, 因為一直會有新的需求和新的情況出現。 所以調優和代碼重構一直在路上。
<settings區分不同環境>
正常創建完django 項目, settings只有一個settings.py。 但是我們有幾個部署環境, 一個本地開發環境, 一個測試環境, 一個生產環境。
單純一個settings不能滿足需求,於是把settings按照不同的環境進行拆分。 拆分後建立一個settings文件夾,把配置文件放在settings下,目錄結構如:
settings - default.py # 共用的變量放這裡 - local.py # 放差異化的變量, 第一行是from .default import *導入共用變量 - test.py # 同上 - prod.py # 同上進行拆分之後,要使用不同的環境,就需要設置環境變量來指定要使用的settings配置
export DJANGO_SETTINGS_MODULE=oakvip.settings.prod<引入設計模式>
以前的開發是線性的, 一直堆測試邏輯,寫一堆if--else, 代碼一多,就容易出現各種問題,比如說可讀性差,代碼耦合多靈活性差(增加需求,往往要寫很多的額外代碼,還很容易出錯),重複代碼多難維護等等。 所以後面的代碼重構引入了設計模式來改善這類問題。
舉一個例子, 運營經常會有各種各樣的活動需求,需要增加活動模塊。 以前的做法是很多活動的代碼都混在一塊,每次要加新的活動模塊的時候,總是很容易影響之前的活動模塊,導致其它活動執行代碼時出錯。 後面我把活動的業務邏輯抽象出來,寫了一個基本類, 其它的活動都繼承這個類,邏輯不一樣的地方就重寫方法即可。 然後放一個統一的入口,根據活動的名稱路由到不同的活動代碼(這裡比較抽象,可以參考工廠模式, 模板模式,想了解更多設計模式相關的知識,請移步擴展閱讀)。 這樣做的好處是以後新增一個活動,只要新增一個繼承活動基類的子類就可以了。 其它代碼都不需要動。
擴展閱讀:
Python與設計模式系列連載 https://yq.aliyun.com/articles/70448
<其它優化>
還有其它優化,或者說必要的操作, 比如說將日誌進行集中採集發送到ES並設置日誌監控, 將一些異步操作和定時任務用Celery框架管理起來,增加Redis緩存提升接口性能等等。限於篇幅,這裡就不展開了。後面用單獨的文章來介紹。
到此,我們講了開發過程中的9個方面的內容,分別是: 版本選擇,目錄結構設計, 編程規範,模塊劃分, 表結構設計, 接口設計, 日誌記錄, 單元測試,以及代碼的重構調優。
篇幅有限,不可能涵蓋開發所有方面的內容, 只能蜻蜓點水, 講得不夠深入。只希望借這三篇文章初步幫各位梳理開發的脈絡,對各位能夠有一點點啟發。
開發過程中其實也會遇到各種各樣的問題,比如說並發問題,編碼問題,還有一些奇奇怪怪的報錯,都是在不斷的探索。 也許我會在後面的文章單獨我這些經驗分享出來。歡迎大家關注本系列的後續更新,也歡迎和我一起交流探討。
年初立了一個FLAG,接下來我準備在公眾號裡寫一個專欄,站在初創企業的視角,來寫技術選型,項目開發的細節,項目管理的經驗等等,把遇到的問題踩到的坑統統記錄下來,都是些實戰的內容。
內容包括但不限於以下這些內容
如果你覺得文章有用,就順手點個」好看「吧。寫作不易,分享和轉發是最大的支持。