MVC 架构解析 - 模型(Model)篇

Posted 东方睡衣

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MVC 架构解析 - 模型(Model)篇相关的知识,希望对你有一定的参考价值。

概念

首先我们要了解的是,我们总在谈“模型”,那到底什么是模型?

简单说来,模型就是当我们使用软件去解决真实世界中各种实际问题的时候,对那些我们关心的实际事物的抽象和简化。比如我们在软件系统中设计“人”这个事物类型的时候,通常只会考虑姓名、性别和年龄等一些系统用得着的必要属性,而不会把性格、血型和生辰八字等我们不关心的东西放进去。

更进一步讲,我们会谈领域模型(Domain Model)。“领域”两个字显然给出了抽象和简化的范围,不同的软件系统所属的领域是不同的,比如金融软件、医疗软件和社交软件等等。如今领域模型的概念包含了比其原本范围定义以外更多的内容,我们会更关注这个领域范围内各个模型实体之间的关系

MVC 中的“模型”,说的是“模型层”,它正是由上述的领域模型来实现的,可是当我们讲这一层的时候,它包含了模型上承载的实实在在的业务数据,还有不同数据间的关联关系。因此,我们在谈模型层的时候,有时候会更关心领域模型这一抽象概念本身,有时候则会更关心数据本身

贫血模型和充血模型

第一次听到“贫血模型”“充血模型”这两个词的时候,可能你就会问了,什么玩意儿?领域模型还有贫血和充血之说?其实,这两兄弟是 Martin Fowler 造出来的概念。要了解它们,得先知道这里讲的“血”是什么。

这里的“血”,就是逻辑。它既包括我们最关心的业务逻辑,也包含非业务逻辑 。因此,贫血模型意味着模型实体在设计和实现上,不包含或包含很少的逻辑。通常这种情况下,逻辑是被挪了出去,由其它单独的一层代码(比如这层代码是“Service”)来完成。

严格说起来,贫血模型不是面向对象的,因为对象需要数据和逻辑的结合,这也是贫血模型反对者的重要观点之一。如果主要逻辑在 Service 里面,这一层对外暴露的接口也在 Service 上,那么事实上它就变成了面向“服务”的了,而模型实体,实际只扮演了 Service API 交互入参出参的角色,或者从本质上说,它只是遵循了一定封装规则的容器而已。

这时的模型实体,不包含逻辑,但包含状态,而逻辑被解耦到了无状态 Service 中。既然没有了状态,Service 中的方法,就成为过程式代码的了。请注意,不完全面向对象并不代表它一定“不好”,事实上,在互联网应用设计中,贫血模型和充血模式都有很多成功的使用案例,且非常常见。

比如这样的一个名为 Book 的类:

public class Book private int id;private boolean onLoan;public int getId() return this.id;public void setId(int id) this.id = id;public boolean isOnLoan() return this.loan;public void setOnLoan(boolean onLoan) this.onLoan = onLoan;

 

你可以看到,它并没有任何实质上的逻辑在里面,方法也只有简单的 getters 和 setters 等属性获取和设置方法,它扮演的角色基本只是一个用作封装的容器。

那么真正的逻辑,特别是业务逻辑在哪里呢?有这样一个 Service:

public class BookService  public Book lendOut(int bookId, int userId, Date date)  ... 

 

这个 lendOut 方法表示将书从图书馆借出,因此它需要接收图书 id 和 用户 id。在实现中,可能需要校验参数,可能需要查询数据库,可能需要将从数据源获得的原始数据装配到返回对象中,可能需要应用过滤条件,这里的内容,就是逻辑。

现在,我们再来了解一下充血模型(Rich Domain Model)。在充血模型的设计中,领域模型实体就是有血有肉的了,既包含数据,也包含逻辑,具备了更高程度的完备性和自恰性,并且,充血模型的设计才是真正面向对象的。在这种设计下,我们看不到 XXXService 这样的类了,而是通过操纵有状态的模型实体类,就可以达到数据变更的目的。

public class Book private int id;private boolean onLoan;public void lendOut(User user, Date date)  ... ... // 省略属性的获取和设置方法

 

在这种方式下,lendOut 方法不再需要传入 bookId,因为它就在 Book 对象里面存着呢;也不再需要传出 Book 对象作为返回值,因为状态的改变直接反映在 Book 对象内部了,即 onLoan 会变成 true。

也就是说,Book 的行为和数据完完全全被封装的方法控制起来了,中间不会存在不应该出现的不一致状态,因为任何改变状态的行为只能通过 Book 的特定方法来进行,而它是可以被设计者严格把控的。

而在贫血模型中就做不到这一点,一是因为数据和行为分散在两处,二是为了在 Service 中能组装模型,模型实体中本不该对用户开放的接口会被迫暴露出来,于是整个过程中就会存在状态不一致的可能。

但是请注意,无论是充血模型还是贫血模型,它和 Model 层做到何种程度的解耦往往没有太大关系。比如说这个 lendOut 方法,在某些设计中,它可以拆分出去。对于贫血模型来说,它并非完全属于 BookService,可以拿到新建立的“借书关系”的服务中去,比如:

public class LoanService public Loan add(int bookId, int userId, Date date)  ... 

 

这样一来,借书的关系就可以单独维护了,借书行为发生的时候,Book 和 User 两个实体对应的数据都不需要发生变化,只需要改变这个借书关系的数据就可以了。对于充血模型来说,一样可以做类似拆分。


内部层次划分

软件的耦合和复杂性问题往往都可以通过分层解决,模型层内部也一样,但是我们需要把握其中的度。层次划分过多、过细,并不利于开发人员严格遵从和保持层次的清晰,也容易导致产生过多的无用样板代码,从而降低开发效率。下面是一种比较常见的 Model 层,它是基于贫血模型的分层方式。

在这种划分方式下,每一层都可以调用自身所属层上的其它类,也可以调用自己下方一层的类,但是不允许往上调用,即依赖关系总是“靠上面的层”依赖着“靠下面的层”。最上面三层是和业务模型实体相关的,而最下面一层是基础设施服务,和业务无关。从上到下,我把各层依次简单介绍一下:

1.第一层 Facade,提供粗粒度的接口,逻辑上是对 Service 功能的组合。有时候由于事务需要跨多个领域模型的实体控制,那就适合放在这里。举例来说,创建用户的时候,我们同时免费赠送一本电子书给用户,我们既要调用 UserService 去创建用户对象,也要调用 SubscriptionService 去添加一条订购(赠送)记录,而这两个属于不同 Service 的行为需要放到一处 Facade 类里面做统一事务控制。在某些较小系统的设计里面,Service 和 Facade 这两层是糅合在一起的。2.第二层 Service,前面已经介绍了,通常会存放仅属于单个领域模型实体的操作。3.第三层数据访问层,在某些类型的数据访问中需要,比如关系型数据库,这里存放数据库字段和模型对象之间的对象关系映射关系。4.第四层基础设施层,这一层的通用性最好,必须和业务无关* * *

CQRS 模式

你也许听说过数据库的读写分离,其实,在模型的设计中,也有类似读写分离的机制,其中最常见的一种就叫做 CQRS(Command Query Responsibility Segregation,命令查询职责分离)。

一般我们设计的业务模型,会同时被用作读(查询模式)和写(命令模式),但是,实际上这两者是有明显区别的,在一些业务场景中,我们希望这两者被分别对待处理,那么这种情况下,CQRS 就是一个值得考虑的选项。

为什么要把命令和查询分离?我举个例子来说明吧,比如这样的贫血模型:

class Book private long id;private String name;private Date publicationDate;private Date creationDate;... // 省略其它属性和 getter/setter 方法

 

那么,相应地,就有这样一个 BookService:

class BookService public Book add(Book book);public Pagination<Book> query(Book book);

 

这个接口提供了两个方法:

一个叫做 add 方法,接收一个 book 对象,这个对象的 name 和 publicationDate 属性会被当做实际值写入数据库,但是 id 会被忽略,因为 id 是数据库自动生成的,具备唯一性,creationDate 也会被忽略,因为它也是由数据库自动生成的,表示数据条目的创建时间。写入数据库完成后,返回一个能够反映实际写入库中数据的 Book 对象,它的 id 和creationDate 都填上了数据库生成的实际值。

你看,这个方法,实际做了两件事,一件是插入数据,即写操作;另一件是返回数据库写入的实际对象,即读操作。

另一个方法是 query 方法,用于查询,这个 Book 入参,被用来承载查询参数了 。比方说,如果它的 author 值为“Jim”,表示查询作者名字为“Jim”的图书,返回一个分页对象,内含分页后的结果列表。

但这个方法,其实有着明显的问题。这个问题就是,查询条件的表达,并不能用简单的业务模型很好地表达。换言之,这个模型 Book,能用来表示写入,却不适合用来表示查询。为什么呢?

比方说,你要查询出版日期从 2018 年到 2019 年之间的图书,你该怎么构造这个 Book 对象?很难办对吧,因为 Book 只能包含一个 publicationDate 参数,这种“难办”的本质原因,是模型的不匹配,即这个 Book 对象根本就不适合用来做查询调用的模型。

在清楚了问题以后,解决方法 CQRS 就自然而然产生了。简单来说,CQRS 模式下,模型层的接口分为且只分为两种:

1.命令(Command),它不返回任何结果,但会改变数据的状态
2.查询(Query),它返回结果,但是不会改变数据的状态

也就是说,它把命令和查询的模型彻底分开了。上面的例子 ,使用 CQRS 的方式来改写一下,会变成这样:

class BookService public void add(Book book);public Pagination<Book> query(Query bookQuery);

 

你看,在 add 操作的时候,不再有返回值;而在 query 操作的时候,入参变成了一个 Query 对象,这是一个专门的“查询对象”,查询对象里面可以放置多个查询条件,比如:

Query bookQuery = new Query(Book.class);
query.addCriteria(Criteria.greaterThan("publicationDate", date_2018));
query.addCriteria(Criteria.lessThan("publicationDate", date_2019)); 

最后

最近还整理一份javascript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

Django框架-基础篇

Django框架简介

一、软件架构模式MVC和MTV

1、MVC模型

MVC(model view controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(model)、视图(view)和控制器(controller),具有耦合性低,重用性高,生命周期成本低等优点。

技术分享图片

 

2、MTV模型

Django框架的设计模式借鉴了MVC框架的思想,也是分别三部分,来降低各个部分之间的耦合性。

Django的MTV分别代表:

  Model(模型):和数据库相关的,负责业务对象与数据库的对象(ORM)

  Template(模板):放所有的html文件,负责如何把页面展示给用户

           模板语法:目的是将自变量(数据库的内容)如何巧妙的嵌入到html页面中

  View(视图):负责业务逻辑,并在适当的时候调用Model和Template

  此外,Django还有一个URL分发器。它的作用是将一个个URL的页面请求分别发给不同的Views处理,Views再调用相应的Model和Template。

技术分享图片

3、MVC和MTCV的比较

技术分享图片

二、Django 下载安装

查看django版本:

cmd——>python -m django --version

pycharm——> import django ; print(django.version)

   命令行下载安装:

dos窗口中:
pip  install  django== 1.11.15 # 等号后为版本号,不加的话默认安装最新版本
pip  install  -i  https://pypi.doubanio.com/simple/  django==1.11.15(或者-i 指定源)

  pycharm的方式安装

打开pycharm -->file -->settings --> project interpreter --> 点击右侧绿色加号 -->在弹出对话框中输入django -->选中specify version,指定下载版本,默认最高版  -->最后点击左下角 install package

注:默认从https://pypi.python.org/simple下载模块,可更改国内的豆瓣源,点击install package 右侧的Manage Repositories 在里面添加其它的源,如https://pypi.doubanio.com/simple/

详细见下图:

技术分享图片

三、创建Django项目对象

  命令行创建:

切换目录到预存放项目文件的文件目录

django-admin  startproject   项目名称

eg:  django-admin  startproject    myblog

生成目录结构如下:

技术分享图片

  • manage.py------启动文件 (Django项目里面的工具,通过它可以调用Django shell的数目和数据库等)
  • settings.py------包含了项目的一些设置,包括数据库信息、调试标志以及其他一些工作的变量。
  • urls.py-----------路径与视图函数的映射关系
  • wsgi.py ...........帮我们实现socket,响应http请求

  注:命令行创建,不会创建templates模板【用于仅存放html文件,css,js等静态文件放到新建的static目录中】,

需要自己在项目根目录即与manage.py同级目录下创建templates文件夹,且要到settings.py中修改templates的DIRS中加入os.path.join(BASE_DIR,‘templates‘)

技术分享图片

  pycharm方式创建

 打开pycharm--->file ---->new project ---> 选中django 

 技术分享图片

 创建完成后项目目录结构如下:

技术分享图片

注:上面app01为创建的一个应用,Mysite为创建django目录时,自动在项目名下建立一个同名的目录。

四、启动Django项目

  1、命令行方式

切换到django项目根目录下(含有manage.py 的文件)
# 默认为本地,端口为8000
python manage.py runserver    # 127.0.0.1:8000  
# 可修改端口
python manage.py runserver 80   # 127.0.0.1:80
# 可修改ip和端口,4个零表示所有人都可以访问
python manage.py runserver 0.0.0.0:80   # 0.0.0.0:80


注:当项目上线后,需将ip和端口改为实际的

  2、pycharm方式

一个项目就是一个包,不要单独执行某个py文件,而是运行整个项目

技术分享图片

五、创建应用

注:因为一个项目要实现多个功能,因此一个Django项目可以分为很多个APP,用来隔离不同功能模块的代码

如果不建立app也可以执行项目,即视图函数直接写在urls.py中,在settings.py中配置相关的模板,数据库信息,静态文件等信息。

  命令行创建app

python manage.py  startapp  应用名

eg: python manage.py startapp  app01

执行命令后,项目目录下多出一个app01的文件夹,目录结构如下:

app01/
    migrations         记录修改models即orm操作数据库的更改信息,每次更改都会在改目录下新建一个文件
        __init__.py     
    __init__.py        
    admin.py           
    apps.py            记录app信息,当在settings.py中注册app时是从此处找
    models.py          记录创建类【即相当如向数据库中创建表】在此处创建
    tests.py           
    views.py           本app自己的业务逻辑函数即视图函数写在这里

  使用PyCharm创建

可以使用PyCharm的manage.py工具来执行命名。在主菜单栏中选择Tools,在下拉菜单中选择Run manage.py task,会出现如下图所示的工具对话框:


技术分享图片

技术分享图片

在弹出的命令窗口中直接输入下面的命令便可创建app:

startapp app01

使用PyCharm的manage.py工具执行命令时,只用输入命令及参数即可,不再输入python manage.py了。

  注册app应用

创建完app后,想要使用,需先注册才能使用,在django根目录下的settings.py文件中的INSTALLED_APPS模块中注册app信息

格式如下:

应用名.apps.应用名Config
示例:【本质是导入应用目录下apps中为app建立的类】
INSTALLED_APPS = [ ‘django.contrib.admin‘, ‘django.contrib.auth‘, ‘django.contrib.contenttypes‘, ‘django.contrib.sessions‘, ‘django.contrib.messages‘, ‘django.contrib.staticfiles‘, ‘classModel.apps.ClassmodelConfig‘ ]

apps.py文件内容如下图:

技术分享图片

六、配置django项目与数据库mysql的连接

1、安装mysql数据库软件

2、安装 模块pymysql【python3.6后连接mysql只能用pymysql】

cmd--> pip3  install pymysql

3、手动连接数据库,创建一个库及创建一个操作此数据库的用户,赋予权限【django的ORM只能创建表,不能创建库】

# 创建库
create database db_name charset utf8;
# 创建用户及赋予权限
grant all privileges on db_name.* to ‘userName‘@‘ip‘ identified by ‘pwd‘;

4、在Django项目总配置文件setting.py同级目录下[不是app下的__init__]的__init__.py文件中配置项目使用pymysql来操作数据库

import pymysql

pymysql.install_as_MySQLdb()       # 告知django使用pymysql来连接书库

5、在Django项目总配置文件setting.py中的DATABASES模块中配置连接数据库相关信息,格式如下

DATABASES = {
                    ‘default‘: {
                            ‘ENGINE‘: ‘django.db.backends.mysql‘,  # 修改数据库引擎,默认为sqlite3文本数据库
                            ‘NAME‘: ‘db_name‘,
                            ‘USER‘: ‘**‘,
                            ‘PASSWORD‘:‘***‘,
                            ‘HOST‘:‘ip‘,
                            ‘PORT‘: 3306,     # 注意是数字,不是字符串
                        }
                    }

6、建表   在应用app的目录下的models.py中写类【必须继承models.Model类

from django.db import models   # 需先导入models模块

class Class(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return self.name

# 在orm中
类      对应数据库中的       表
对象   对应数据库中的       记录(数据行)
属性   对应数据库中的       字段

7、建好类即表后,执行数据库迁移命令【让创建的表在数据库中生成】

# 记录models.py中类是不是有变化 将变化的内容记录下来 
python manage.py  makemigrations

# 将model的变更同步到数据库中
python  manage.py migrate


注:执行第一条命令后models.py中的变化就会在到migrations目录中创建一个新的文件并将变化写入其中
如果在原有表张增加字段的话,会提示要设置默认值,因为新的那列数据不能为空

七、项目启动后相关配置

  1、更改setting.py 

1.1、检查项目BASE_DIR是否如下:

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

1.2、配置ALLOWED_HOSTS可以运行本项目的ip,‘*‘ 代表所有

ALLOWED_HOSTS = [‘*‘,‘127.0.0.1’,‘192.168.19.66’]
注:如想别人访问你的网站,需在启动项目时将ip配置为0.0.0.0,如何将运行项目的ip写入上面列表,用户就可以通过你的主机ip访问你的网站了

1.3、配置INSTALLED_APPS,为创建的应用app进行注册,不然操作数据库时报错

格式:‘app_name.Apps.AppName.Config‘ 或 ‘app_name’

INSTALLED_APPS = [
    ‘classModel.apps.ClassmodelConfig‘
]    

1.4、配置中间件MIDDLEWARE,注释csrf,让后台能够接收所有的post请求,不关闭django将对不合法的post拦截403

将 ‘django.middleware.csrf.CsrfViewMiddleware‘注释掉,否则会出现下面的情况。

技术分享图片

 

 技术分享图片

注:上面的方案,不建议使用,不安全,可以在html文件中要发送post请求的from表单里加入{% csrf_token %}就不用注释上面的csrf的中间件了

如下图:

技术分享图片

 

前端实际效果为:

 

技术分享图片

 

 

1.5、配置模块templates 中的DIRS需要检查下是否为空,如果为空,添加上os.path.join(BASE_DIR,‘templates‘)

命令创建的项目未创建模板templates目录,需自己创建。

技术分享图片

这是模板中没有设置,设置模板在python安装目录中找到如下图的路径,打开settings.py-tpl,相同位置添加上上os.path.join(BASE_DIR,‘templates‘)。下次创建项目就可不再手动添加。

技术分享图片

1.6、settings.py中增加静态文件目录STATICFILES_DIRS【(js,css,image)存放目录】,这是放在templates中html文件能够使用js,css资源的关键

在项目根目录即与manage.py文件同级下创建静态文件夹static,如果有多个就创建多个,但网页中不论是放在那个文件中,开始都以/static/开头

技术分享图片

1.7、templates文件夹下创建/或存放HTML文件,将html的js,css,imgs等文件放入images

技术分享图片

  2、编辑urls.py文件,增加映射关系。

 业务函数的实现,业务函数与url的映射可都在此文件中书写,以后规范化,可将业务函数移入到应用的views.py中,此文件仅编辑业务函数与url的映射关系,再导入views.py中所有的业务函数

from app名 import views

技术分享图片

下图为:直接将视图函数写在全局urls.py文件中

技术分享图片

八、访问web

  1、局域网中的浏览器输入HTTP://IP:PORT/login 就可访问验证。

九、如何完全的删除一个应用app? 

1.删除models.py 

  无论是删除一个单独的model还是删除整个App,都需要首先删除models.py文件中的模型。

确认没有其他文件引用models.py中的类。

迁移或者删除你的数据库,Django提供了简便的方法方便用户删除某App下的所有数据(Django 1.7)。

python  manage.py migrate your_app_name zero   

  删除models.py中的数据模型。

2.删除App

  再次确认其他App中没有引用此App的文件

  删除整个App文件夹

  在settings.py的Installed Apps中移除该app

  在urls.py中移除该App相关内容。 

 十、Django快速开发配置小结:

在app的models.py创建类(即数据库中的表)
在views.py下创建业务视图函数[导入orm模块]
  from django.shortcuts import render
  from django.shortcuts import HttpResponse
  from django.shortcuts import redirect
  from classModel import models

在django的templates目录下创建前端html文件
在在django的static目录下存放js,css.images 文件
在urls.py下建立映射关系
  from classModel import views

以上是关于MVC 架构解析 - 模型(Model)篇的主要内容,如果未能解决你的问题,请参考以下文章

转MVC架构

开发架构模式

ThinkPHP 3.2.3架构之多层MVC

mvc是啥,有啥用

SpringMVC学习篇

ASP.NET MVC 的三层架构 + EF数据模型