選課系統-面向對象-三層架構
項目需求
角色: 學校、學員、課程、講師、管理員
要求:
1. 創建北京、上海 2 所學校 ---> 管理員創建學校
2. 創建linux , python , go 3個課程 , linux\py 在北京開, go 在上海開
3. 課程包含,周期,價格,通過學校創建課程
4. 創建講師
5. 創建學員時,選擇學校,關聯班級
5. 創建講師
6. 提供兩個角色接口
6.1 學員視圖, 直接登錄,選擇課程(等同於選擇班級)
6.2 講師視圖, 講師可管理自己的課程, 上課時選擇班級,
查看班級學員列表 , 修改所管理的學員的成績
6.3 管理視圖,創建講師, 創建班級,創建課程等
7. 上面的操作產生的數據都通過pickle序列化保存到文件里
- pickle 可以幫我們保存對象
需求分析
角色設計:管理員、學校、老師、學生、課程等
需求分析 (課程與班級合為一體)
- 管理員視圖
- 1.註冊
- 2.登錄
- 3.創建學校
- 4.創建課程(先選擇學校)
- 5.創建講師(默認設置初始密碼)
- 6.創建學生(先選擇學校,默認設置初始密碼)
- 7.修改密碼
- 8.重置老師、學生密碼
- 學員視圖
- 1.登錄功能
- 2.選擇課程
- 3.已選課程查看
- 5.查看分數
- 6.修改密碼
- 講師視圖
- 1.登錄
- 2.查看課程
- 3.選擇課程
- 4.我的學生(按課程分類查看)
- 5.修改學生分數(找到課程再找學生)
- 6.修改密碼
三層架構設計
實現思路:
-
項目採用三層架構設計,基於面向對象封裝角色數據和功能。面向過程和面向對象搭配使用。
-
程序開始,用戶選擇角色,進入不同的視圖層,展示每個角色的功能,供用戶選擇。
-
進入具體角色視圖後,調用功能,對接邏輯接口層獲取數據並展示給用戶視圖層。
-
邏輯接口層需要調用數據處理層的類,獲取類實例化對象,進而實現數據的增刪改查。
# 用戶視圖層
- 提供用戶數據交互和展示的功能
# 邏輯接口層
- 提供核心邏輯判斷,處理用戶的請求,調用數據處理層獲取數據並將結果返回給用戶視圖層
# 數據處理層
- 提供數據支撐,使用面向對象的數據管理,將數據和部分功能封裝在類中,將對象保存在數據庫
程序結構:
CSS/ # Course Selection System
|-- conf
| |-- setting.py # 項目配置文件
|-- core
| |-- admin.py # 管理員視圖層函數
| |-- current_user.py # 記錄當前登錄用戶信息
| |-- teacher.py # 老師視圖層函數
| |-- student.py # 學生視圖層函數
| |-- css.py # 主程序(做視圖分發)
|-- db
|-- |-- models.py # 存放類
| |-- db_handle.py # 數據查詢和保存函數
| |-- Admin # 管理員用戶對象文件夾
| |-- Course # 課程對象文件夾
| |-- School # 學校對象文件夾
| |-- Student # 學生對象文件夾
| |-- Teacher # 老師對象文件夾
|-- interface # 邏輯接口
| |-- admin_interface.py # 管理員邏輯接口
| |-- common_interface.py # 公共功能邏輯接口
| |-- student_interface.py # 學生功能邏輯接口
| |-- teacher_interface.py # 老師功能邏輯接口
|-- lib
| |-- tools.py # 公用函數:加密|登錄裝飾器權限校驗等
|-- readme.md
|-- run.py # 項目啟動文件
版本:
版本1:採用上述的邏輯架構,視圖層采層面向過程的方式,即函數組織。
版本2:用戶視圖層採用面向對象的封裝加反射,實現用戶功能函數的自動添加(但個人感覺不如面向過程的簡潔清晰)。
項目源碼
項目源碼在github個人倉庫,感興趣的園友可以參考,歡迎交流分享。點擊一下連接到倉庫地址
下面默認總結版本1的要點,總結版本2的要點時會明顯指出(即用類封裝視圖層的兩個關鍵點:裝飾器,Mixins)。
運行環境
- windows10, 64位
- python3.8
- pycharm2019.3
角色類的設計
import sys
from conf import settings
from db import db_handle
class FileMixin:
@classmethod
def get_obj(cls, name):
return db_handle.get_obj(cls, name)
def save_obj(self):
db_handle.save_obj(self)
class Human:
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
self.__pwd = settings.INIT_PWD
self.role = self.__class__.__name__
@property
def pwd(self):
return self.__pwd
@pwd.setter
def pwd(self, new_pwd):
self.__pwd = new_pwd
class Admin(FileMixin, Human):
def __init__(self, name, age, sex):
super().__init__(name, age, sex)
self.save_obj()
@staticmethod
def create_school(school_name, school_addr):
School(school_name, school_addr)
@staticmethod
def create_course(school_name, course_name, course_period, course_price):
Course(course_name, course_period, course_price, school_name)
@staticmethod
def create_teacher(teacher_name, teacher_age, teacher_sex, teacher_level):
Teacher(teacher_name, teacher_age, teacher_sex, teacher_level)
@staticmethod
def create_student(stu_name, stu_age, stu_sex, school_name, homeland):
Student(stu_name, stu_age, stu_sex, school_name, homeland)
@staticmethod
def reset_user_pwd(name, role):
obj = getattr(sys.modules[__name__], role).get_obj(name)
obj.pwd = settings.INIT_PWD
obj.save_obj()
class School(FileMixin):
def __init__(self, name, addr):
self.name = name
self.addr = addr
self.course_list = []
self.save_obj()
def relate_course(self, course_name):
self.course_list.append(course_name)
self.save_obj()
class Course(FileMixin):
def __init__(self, name, period, price, school_name):
self.name = name
self.period = period
self.price = price
self.school = school_name
self.teacher = None
self.student_list = []
self.save_obj()
def relate_teacher(self, teacher_name):
self.teacher = teacher_name
self.save_obj()
def relate_student(self, stu_name):
self.student_list.append(stu_name)
self.save_obj()
class Teacher(FileMixin, Human):
def __init__(self, name, age, sex, level):
super().__init__(name, age, sex)
self.level = level
self.course_list = []
self.save_obj()
def select_course(self, course_name):
self.course_list.append(course_name)
self.save_obj()
course_obj = Course.get_obj(course_name)
course_obj.relate_teacher(self.name)
def check_my_courses(self):
return self.course_list
@staticmethod
def check_my_student(course_name):
course_obj = Course.get_obj(course_name)
return course_obj.student_list
@staticmethod
def set_score(stu_name, course_name, score):
stu_obj = Student.get_obj(stu_name)
stu_obj.score_dict[course_name] = int(score)
stu_obj.save_obj()
class Student(FileMixin, Human):
def __init__(self, name, age, sex, school_name, homeland):
super().__init__(name, age, sex)
self.school = school_name
self.homeland = homeland
self.course_list = []
self.score_dict = {}
self.save_obj()
def select_course(self, course_name):
self.course_list.append(course_name)
self.score_dict[course_name] = None
self.save_obj()
course_obj = Course.get_obj(course_name)
course_obj.relate_student(self.name)
def check_my_course(self):
return self.course_list
def check_my_score(self):
return self.score_dict
-
從管理員、學生、老師角色中抽象出
Human
類,有用戶基本數據屬性和密碼相關的公共屬性 -
為了角色數據的讀取和保存,定義了一個接口類
FileMixin
,用於對象數據的讀取和保存。 -
FileMixin
中設置一個綁定類的方法,這樣每個繼承FileMixin
的類都可以通過對象名判斷這個對象的存在與否。 -
注意,多繼承時遵循
Mixins
規範。 -
對象初始化後立即保存數據,每個功能操作後,也跟一個
save_obj
方法,這樣類的使用者就很方便。 -
在用戶類中設置角色的方法屬性,這樣直接在邏輯接口層中在獲取對象後,直接調用對象的方法即可。這樣做是為了保證面向對象的完整性,每個對象都對應其現實意義。
登錄功能分析
-
每個角色都有登錄需求,因此這裡打算做一個公用的登錄邏輯接口層。
-
不過因為數據存放格式的限制,這裡妥協一下。每個登錄視圖層還是直接調用各自的登錄邏輯接口,然後從各自的邏輯接口層中調用公用邏輯接口層的核心登錄邏輯判斷。
-
這裡在角色的登錄接口中做一個中轉的目的是為了給登錄用戶設置一個登錄角色;
-
並且這個角色的字符串名字和類的名字保持一致,為了方便在公共登錄接口中使用反射判斷。
admin_interface.py
def login_interface(name, pwd):
"""
登錄接口
:param name:
:param pwd: 密碼,密文
:return:
"""
from interface import common_interface
role = 'Admin'
flag, msg = common_interface.common_login_interface(name, pwd, role)
return flag, msg
common_interface.py
def common_login_interface(name, pwd, role):
"""
登錄接口
:param name:
:param pwd: 密碼,密文
:param role: 角色,如,Admin|Teacher|Student
:return:
"""
if hasattr(models, role):
obj = getattr(models, role).get_obj(name)
if not obj:
return False, f'用戶名[{name}]不存在'
if pwd != obj.pwd:
return False, '用戶名或密碼錯誤'
return True, '登錄成功'
else:
return False, '您沒有權限登錄'
時刻想着封裝
這個項目按照三層架構的模式,只要實現了一個角色,其他角色的功能在編寫的時候,會存在大量重複的代碼。
所以,儘可能地提取公共的邏輯接口和工具函數,減輕程序組織結構臃腫,提高代碼復用率。
場景一:視圖層中,功能函數的展示和選擇
這個場景主要用在視圖分發和視圖內用戶功能函數的選擇。
如果視圖層採用面向對象的方式,封裝成一個視圖類,使用裝飾器和反射就可以避免功能字典的使用。
lib/tools.py
def menu_display(menu_dict):
"""
展示功能字典,然用戶選擇使用
:param menu_dict:
:return:
"""
while 1:
for k, v in menu_dict.items():
print(f'({k}) {v[0]}', end='\t')
func_choice = input('\n請輸入選擇的功能編號(Q退出):').strip().lower()
if func_choice == 'q':
break
if func_choice not in menu_dict:
continue
func = menu_dict.get(func_choice)[1]
func()
場景二:展示數據並返回用戶選擇的數據
這個場景是用戶在選擇一個需求時,先將選項展示給用戶看,供用戶輸入選擇編號。
這個過程就涉及到用戶的退出選擇和輸入編號的合法性驗證。返回用戶的選擇結果或者錯誤信息提示。
前提:調用該函數之前判斷info_list
為空的情況;在該函數內也可以判斷,不同這樣的話就降低了其通用程度。
lib/tools.py
def select_item(info_list):
"""
枚舉展示數據列表,並支持用戶數據編號返回編號對應的數據,支持編號合法校驗
:param info_list:
:return:
"""
while 1:
for index, school in enumerate(info_list, 1):
print(index, school)
choice = input('請輸入選擇的編號(Q退出):').strip().lower()
if choice == 'q':
return False, '返回'
if not choice.isdigit() or int(choice) not in range(1, len(info_list) + 1):
print('您輸入的編號不存在')
continue
else:
return True, info_list[int(choice) - 1]
這樣的需求或者說場景還有很多,不做列舉。
數據存放格式
將一個類實例化對象按照類型保存在不同的文件夾中,文件夾名與類名相同,文件名為對象的name屬性的名字。
這樣做的好處是方便對象數據的讀取和保存,並且對象間沒有使用組合的方式,避免數據的重複保存。
但是這樣做的缺點很明顯:每個類下面的對象不能重名。這個問題需要重新組織數據管理方式,讓其更實際化。
視圖層封裝成視圖類
之所以想要將視圖層封裝成視圖類,主要是為了簡化代碼和避免手動編寫用戶的功能函數字典。
採用視圖類之後,可以將功能函數做成視圖類的對象的綁定方法,採用反射,可以自動獲取並調用。
但這裡需要做一個處理:用戶選擇角色後,如何獲取並顯示這個角色的功能函數函數列表?
這裡需要在視圖類裏面做一個顯示功能的方法start
,這個方法要在用戶選擇先顯示所有的功能,
在此之前,還需要一個收集角色功能的方法auto_get_func_menu
,這個函數必須在對象使用時就立即工作,
最後,還要配合一個裝飾器my_func
,讓收集函數知道搜集那些功能,保存下來func_list
,讓顯示函數獲取。
上述這個過程涉及的方法是每個視圖類都要有的,因此抽象出來一個基礎視圖類BaseViewer
。
最後,視圖類需要用到一些公用工具(lib/tool.py),將它封裝成一個ToolsMixin
類,視圖類繼承之,方便傳參。
關鍵點:
- 使用有參裝飾器,自動獲取角色功能方法並保存,給顯示方法獲取功能,顯示之供用戶選擇並調用。
- 因此並沒有使用反射(本來喜愛那個是用反射的,可惜沒用上)。
- 裝飾器這裏面有兩個,一個是登錄驗證的,一個是自動獲取角色功能的。
- 這兩個裝飾器都使用定義成靜態方法,方便繼承的子類調用;但總覺得很不舒服。
core/baseview.py
from functools import wraps
class BaseViewer:
name = None
role = None
func_list = [] # 存放角色功能方法
def __init__(self):
self.auto_get_func_menu() # 初始化就啟動,搜集角色功能方法
def auto_get_func_menu(self):
"""
自動調用功能函數觸發裝飾器的執行,將功能函數添加到類屬性 func_list中
:return:
"""
not_this = ['auto_get_func_menu', 'my_func', 'start']
all_funcs = {k: v for k, v in self.__class__.__dict__.items()
if callable(v) and not k.startswith('__') and k not in not_this}
for func in all_funcs.values():
func()
def start(self):
"""
開始函數,功能菜單顯示,供管理員選擇
:return:
"""
while 1:
for index, func_name in enumerate(self.func_list, 1):
print('\t\t\t\t\t\t', index, func_name[0], sep='\t')
choice = input('>>>(Q退出):').strip().lower()
if choice == 'q':
self.func_list.clear()
break
if not choice.isdigit() or int(choice) not in range(1, len(self.func_list) +1):
print('編號不存在, 請重新輸入')
continue
func = self.func_list[int(choice) - 1][1]
func(self)
@staticmethod
def my_func(desc):
"""
裝飾器,實現功能函數自動添加到類的func_list中
:return:
"""
def wrapper(func):
@wraps(func)
def inner(*args, **kwargs):
BaseViewer.func_list.append((desc, func))
return inner
return wrapper
@staticmethod
def auth(role):
"""
裝飾器,登錄校驗
:return:
"""
def wrapper(func):
@wraps(func)
def inner(*args, **kwargs):
if BaseViewer.name and BaseViewer.role == role:
res = func(*args, **kwargs)
return res
else:
print('您未登錄或沒有該功能的使用權限')
return inner
return wrapper
def login(self, role_interface):
while 1:
print('登錄頁面'.center(50, '-'))
name = input('請輸入用戶名(Q退出):').strip().lower()
if name == 'q':
break
pwd = input('請輸入密碼:').strip()
if self.is_none(name, pwd):
print('用戶名或密碼不能為空')
continue
flag, msg = role_interface.login_interface(name, self.hash_md5(pwd))
print(msg)
if flag:
BaseViewer.name = name
break
學生視圖類:core/student.py
from core.baseview import BaseViewer as Base
from lib.tools import ToolsMixin
from interface import student_interface, common_interface
class StudentViewer(ToolsMixin, Base):
@Base.my_func('登錄')
def login(self):
Base.role = 'Student'
super().login(student_interface)
@Base.my_func('選擇課程')
@Base.auth('Student')
def select_course(self):
while 1:
school_name = student_interface.get_my_school_interface(self.name)
flag, course_list = common_interface.get_course_list_from_school(school_name)
if not flag:
print(course_list)
break
print('待選課程列表'.center(30, '-'))
flag2, course_name = self.select_item(course_list)
if not flag2:
break
flag3, msg = student_interface.select_course_interface(course_name, self.name)
print(msg)
@Base.my_func('我的課程')
@Base.auth('Student')
def check_my_course(self):
flag, course_list = student_interface.check_my_course_interface(self.name)
if not flag:
print(course_list)
return
print('我的課程:'.center(30, '-'))
for index, course_name in enumerate(course_list, 1):
print(index, course_name)
@Base.my_func('我的分數')
@Base.auth('Student')
def check_my_score(self):
flag, score_dict = student_interface.check_score_interface(self.name)
if not flag:
print(score_dict)
else:
print('課程分數列表')
for index, course_name in enumerate(score_dict, 1):
score = score_dict[course_name]
print(index, course_name, score)
@Base.my_func('修改密碼')
@Base.auth('Student')
def edit_my_pwd(self):
self.edit_pwd(common_interface.edit_pwd_interface)
總結
-
一定要先分析需求,再構思設計,最後開始編碼。
-
角色設計時,需要考慮角色之間的關係,抽象繼承,多繼承遵循
Mixins
規範。 -
使用property,遵循鴨子類型,方便接口設計。
-
基於反射可以做很多動態判斷,避免使用
if-elif-else
多級判斷。 -
面向過程和面向對象搭配使用。
-
三層架構,明確每層職責,分別使用面向對象和面向過程編碼。
-
儘可能封裝成工具:函數或者類