選課系統-面向物件-三層架構
阿新 • • 發佈:2020-04-25
## 專案需求
~~~python
角色: 學校、學員、課程、講師、管理員
要求:
1. 建立北京、上海 2 所學校 ---> 管理員建立學校
2. 建立linux , python , go 3個課程 , linux\py 在北京開, go 在上海開
3. 課程包含,週期,價格,通過學校建立課程
4. 建立講師
5. 建立學員時,選擇學校,關聯班級
5. 建立講師
6. 提供兩個角色介面
6.1 學員檢視, 直接登入,選擇課程(等同於選擇班級)
6.2 講師檢視, 講師可管理自己的課程, 上課時選擇班級,
檢視班級學員列表 , 修改所管理的學員的成績
6.3 管理檢視,建立講師, 建立班級,建立課程等
7. 上面的操作產生的資料都通過pickle序列化儲存到檔案裡
- pickle 可以幫我們儲存物件
~~~
## 需求分析
~~~python
角色設計:管理員、學校、老師、學生、課程等
需求分析 (課程與班級合為一體)
- 管理員檢視
- 1.註冊
- 2.登入
- 3.建立學校
- 4.建立課程(先選擇學校)
- 5.建立講師(預設設定初始密碼)
- 6.建立學生(先選擇學校,預設設定初始密碼)
- 7.修改密碼
- 8.重置老師、學生密碼
- 學員檢視
- 1.登入功能
- 2.選擇課程
- 3.已選課程檢視
- 5.檢視分數
- 6.修改密碼
- 講師檢視
- 1.登入
- 2.檢視課程
- 3.選擇課程
- 4.我的學生(按課程分類檢視)
- 5.修改學生分數(找到課程再找學生)
- 6.修改密碼
~~~
## 三層架構設計
**實現思路**:
- **專案採用三層架構設計,基於面向物件封裝角色資料和功能。面向過程和麵向物件搭配使用**。
- 程式開始,使用者選擇角色,進入不同的檢視層,展示每個角色的功能,供使用者選擇。
- 進入具體角色檢視後,呼叫功能,對接邏輯介面層獲取資料並展示給使用者檢視層。
- 邏輯介面層需要呼叫資料處理層的類,獲取類例項化物件,進而實現資料的增刪改查。
![](https://img2020.cnblogs.com/blog/1950650/202004/1950650-20200425101349062-358734909.png)
~~~python
# 使用者檢視層
- 提供使用者資料互動和展示的功能
# 邏輯介面層
- 提供核心邏輯判斷,處理使用者的請求,呼叫資料處理層獲取資料並將結果返回給使用者檢視層
# 資料處理層
- 提供資料支撐,使用面向物件的資料管理,將資料和部分功能封裝在類中,將物件儲存在資料庫
~~~
**程式結構**:
~~~python
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個人倉庫,感興趣的園友可以參考,歡迎交流分享。**[點選一下連線到倉庫地址](https://github.com/the3times/CSS)**
下面預設總結版本1的要點,總結版本2的要點時會明顯指出(即用類封裝檢視層的兩個關鍵點:裝飾器,Mixins)。
## 執行環境
~~~python
- windows10, 64位
- python3.8
- pycharm2019.3
~~~
## 角色類的設計
~~~python
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
~~~python
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
~~~python
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
~~~python
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
~~~python
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
~~~python
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`多級判斷。
- 面向過程和麵向物件搭配使用。
- 三層架構,明確每層職責,分別使用面向物件和麵向過程編碼。
- 儘可能封裝成工具:函式