選課系統-面向對象-三層架構

項目需求

角色: 學校、學員、課程、講師、管理員
要求:
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

  • 項目採用三層架構設計,基於面向對象封裝角色數據和功能。面向過程和麪向對象搭配使用python

  • 程序開始,用戶選擇角色,進入不一樣的視圖層,展現每一個角色的功能,供用戶選擇。linux

  • 進入具體角色視圖後,調用功能,對接邏輯接口層獲取數據並展現給用戶視圖層。git

  • 邏輯接口層須要調用數據處理層的類,獲取類實例化對象,進而實現數據的增刪改查。github

# 用戶視圖層
- 提供用戶數據交互和展現的功能
# 邏輯接口層
- 提供核心邏輯判斷,處理用戶的請求,調用數據處理層獲取數據並將結果返回給用戶視圖層
# 數據處理層
- 提供數據支撐,使用面向對象的數據管理,將數據和部分功能封裝在類中,將對象保存在數據庫

程序結構數據庫

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				# 項目啓動文件

版本windows

版本1:採用上述的邏輯架構,視圖層採層面向過程的方式,即函數組織。數組

版本2:用戶視圖層採用面向對象的封裝加反射,實現用戶功能函數的自動添加(但我的感受不如面向過程的簡潔清晰)。架構

項目源碼

項目源碼在github我的倉庫,感興趣的園友能夠參考,歡迎交流分享。點擊一下鏈接到倉庫地址app

下面默認總結版本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多級判斷。

  • 面向過程和麪向對象搭配使用。

  • 三層架構,明確每層職責,分別使用面向對象和麪向過程編碼。

  • 儘量封裝成工具:函數或者類

相關文章
相關標籤/搜索