项目需求

角色: 学校、学员、课程、讲师、管理员
要求:
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.修改密码

三层架构设计

实现思路

  • 项目采用三层架构设计,基于面向对象封装角色数据和功能。面向过程和面向对象搭配使用

    SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
  • 程序开始,用户选择角色,进入不同的视图层,展示每个角色的功能,供用户选择。

  • 进入具体角色视图后,调用功能,对接逻辑接口层获取数据并展示给用户视图层。

  • 逻辑接口层需要调用数据处理层的类,获取类实例化对象,进而实现数据的增删改查。

选课系统-面向对象-三层架构 Python 第1张

# 用户视图层
- 提供用户数据交互和展示的功能
# 逻辑接口层
- 提供核心逻辑判断,处理用户的请求,调用数据处理层获取数据并将结果返回给用户视图层
# 数据处理层
- 提供数据支撑,使用面向对象的数据管理,将数据和部分功能封装在类中,将对象保存在数据库

程序结构

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)。

代码量统计见下图

选课系统-面向对象-三层架构 Python 第2张

运行环境

- 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多级判断。

  • 面向过程和面向对象搭配使用。

  • 三层架构,明确每层职责,分别使用面向对象和面向过程编码。

  • 尽可能封装成工具:函数或者类

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄