Домой Edit me on GitHub

2020-12-05

Каналы передачи данных | Сетевое программирование | Базы данных | Основы Веб-программирования

Разделение кода

В программном обеспечении принято разделять: программную логику, код, который относится к данным, настройки и шаблоны. Таким образом, код становится более структурированным, и его легче развивать, а также сопровождать в дальнейшем.

MVC

MVC (Model-View-Controller: модель-вид-контроллер) — шаблон архитектуры ПО, который подразумевает разделение программы на 3 слабосвязанных компонента, каждый из которых отвечает за свою сферу деятельности.

Бешеная популярность данной структуры в Веб-приложениях сложилась благодаря её включению в две среды разработки, которые стали очень востребованными: Struts и Ruby on Rails. Эти среды разработки наметили пути развития для сотен рабочих сред, созданных позже.

../../_images/mvc.svg

Паттерн MVC (Model-View-Controller)

  • Model - модель, предоставляющая доступ к данным. Позволяет извлекать данные и менять их состояние;
  • View - представление, отображающее данные клиенту. В веб-программировании существует в виде конечных данных (HTML, JSON, …), которые получает клиент. Может формироваться при помощи генераторов по заданному шаблону, например Jinja2, Mako; или систем для построения интерфейсов по разметке, таких, как Windows Presentation Foundation (WPF), либо Qt Widgets; или описываться декларативно, как это делается в QML и ReactJs.
  • Controller - контроллер, отслеживающий различные события (действия пользователя) и по заданной логике оповещающий модель о необходимости изменить состояние системы.

Классические MVC фреймворки:

MTV

Фреймворк Django ввел новую терминологию MTV.

В Django функции, отвечающие за обработку логики, соответствуют части Controller из MVC, но называются View, а отображение соответствует части View из MVC, но называется Template. Получилось, что:

  • M -> M Модели остались неизменными
  • V -> T Представление назвали Templates
  • C -> V Контроллеры назвали Views

Так появилась аббревиатура MTV.

../../_images/mtv_logo.jpg

TADA!! Django invented MTV

Вся логика при таком подходе вынесена во View, а то, как будут отображаться данные в Template. Из-за ограничений HTTP протокола, View в Django описывает, какие данные будут представленны по запросу на определенный URL. View, как и протокол HTTP, не хранит состояний и по факту является обычной функцией обратного вызова, которая запускается вновь при каждом запросе по URL. Шаблоны (Templates), в свою очередь, описывают, как данные представить пользователю.

../../_images/mtv.svg

Паттерн MTV (Model-Template-View)

MTV фреймворки:

RV

В защиту своего дизайна авторы Pyramid написали довольно большой документ, который призван развеять мифы о фреймворке. Например, на критику модели MVC в Pyramid следует подробное объяснение, что MVC «притянут за уши» к веб-приложениям. Следующая цитата хорошо характеризует подход к терминологии в Pyramid:

«Мы считаем, что есть только две вещи: ресурсы (Resource) и виды (View). Дерево ресурсов представляет структуру сайта, а вид представляет ресурс.

«Шаблоны» (Template) в реальности лишь деталь реализации некоторого вида: строго говоря, они не обязательны, и вид может вернуть ответ (Response) и без них.

Нет никакого «контроллера» (Controller): его просто не существует.

«Модель» (Model) же либо представлена деревом ресурсов, либо «доменной моделью» (domain model) (например, моделью SQLAlchemy), которая вообще не является частью каркаса.

Нам кажется, что наша терминология более разумна при существующих ограничениях веб-технологий.»

../../_images/Pyramid_rv.svg

Паттерн RV (Resources-View)

Веб ограничен URL, который и представляет из себя дерево ресурсов или структуру сайта.

Также протокол HTTP не позволяет хранить состояние и отправлять/принимать оповещения клиенту от сервера, что ограничивает возможность отслеживания действий клиента для последующего уведомления модели на изменение состояния.

Поэтому данные часто используются на «frontend»-е (например в связке React/Redux), а на стороне сервера формируются только один раз во время ответа, либо загружаются отдельным запросом при помощи AJAX, или даже с помощью других протоколов, например WebSocket.

RV фреймворки:

Пример MVC блога

Структура файлов

Приведем структуру нашего блога к следующему виду:

.
├── __init__.py
├── models.py
└── views.py

0 directories, 3 files

Примечание

Исходный код доступен по адресу:

Где:

  • __init__.py - входная точка программы, которая содержит основные настройки и запуск Веб-сервера
  • models.py - код, который представляет данные, обычно называется модели
  • views.py - логика программы (в нашем случае WSGI-приложения)

Предупреждение

Примеры работают только в Python3

Данные

models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ARTICLES = [
    {
        'id': 1,
        'title': 'Lorem ipsum dolor sid amet!',
        'content': '''
     Lorem ipsum dolor sit amet, consectetur adipiscing elit.
     Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
     Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
     Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
     mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
     blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
     pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
     ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
     volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
     Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
     id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
     faucibus. Nulla bibendum suscipit convallis.
        '''
    },
    {
        'id': 2,
        'title': 'Hello',
        'content': 'Test2'
    },
    {
        'id': 3,
        'title': 'World',
        'content': 'Test2'
    },
]

Авторизация

Мы использовали самописные WSGI-middleware, которые решают стандартные задачи. Заменим их на уже существующие:

  • selector - URL-диспетчеризация
  • wsgi-basic-auth - авторизация по методу Basic Auth

Настройки авторизации __init__.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth

# third-party
import selector


def make_wsgi_app():
    passwd = {
        'admin': '123'
    }
    # BasicAuth applications
    create = BasicAuth(BlogCreate, 'www', passwd)
    update = BasicAuth(BlogUpdate, 'www', passwd)
    delete = BasicAuth(BlogDelete, 'www', passwd)

    # URL dispatching middleware
    dispatch = selector.Selector()
    dispatch.add('/', GET=BlogIndex)
    dispatch.prefix = '/article'
    dispatch.add('/add', GET=create, POST=create)
    dispatch.add('/{id:digits}', GET=BlogRead)
    dispatch.add('/{id:digits}/edit', GET=update, POST=update)
    dispatch.add('/{id:digits}/delete', GET=delete)
    return dispatch

if __name__ == '__main__':
    from paste.httpserver import serve
    app = make_wsgi_app()
    serve(app, host='0.0.0.0', port=8000)

URL-диспетчеризация

Настройки URL-диспетчеризации __init__.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth

# third-party
import selector


def make_wsgi_app():
    passwd = {
        'admin': '123'
    }
    # BasicAuth applications
    create = BasicAuth(BlogCreate, 'www', passwd)
    update = BasicAuth(BlogUpdate, 'www', passwd)
    delete = BasicAuth(BlogDelete, 'www', passwd)

    # URL dispatching middleware
    dispatch = selector.Selector()
    dispatch.add('/', GET=BlogIndex)
    dispatch.prefix = '/article'
    dispatch.add('/add', GET=create, POST=create)
    dispatch.add('/{id:digits}', GET=BlogRead)
    dispatch.add('/{id:digits}/edit', GET=update, POST=update)
    dispatch.add('/{id:digits}/delete', GET=delete)
    return dispatch

if __name__ == '__main__':
    from paste.httpserver import serve
    app = make_wsgi_app()
    serve(app, host='0.0.0.0', port=8000)

WSGI-приложение можно указывать как объект (BlogRead) или как строку импорта ("views.BlogIndex").

views.py:

1
2
3
4
5
6
7
8
9
class BaseArticle(BaseBlog):

    def __init__(self, *args):
        super(BaseArticle, self).__init__(*args)
        article_id = self.environ['wsgiorg.routing_args'][1]['id']
        (self.index,
         self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
                               if art['id'] == int(article_id)),
                              (None, None))

urlrelay добавляет результат поиска в переменную с названием wsgiorg.routing_args.

WSGI-приложения

Практически не изменились.

views.py:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
from models import ARTICLES


class BaseBlog(object):

    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response


class BaseArticle(BaseBlog):

    def __init__(self, *args):
        super(BaseArticle, self).__init__(*args)
        article_id = self.environ['wsgiorg.routing_args'][1]['id']
        (self.index,
         self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
                               if art['id'] == int(article_id)),
                              (None, None))


class BlogIndex(BaseBlog):

    def __iter__(self):
        self.start('200 OK', [('Content-Type', 'text/html')])
        yield b'<h1>Simple Blog</h1>'
        yield b'<a href="/article/add">Add article</a>'
        yield b'<br />'
        yield b'<br />'
        for article in ARTICLES:
            yield str.encode(
                '''
                {0} - (<a href="/article/{0}/delete">delete</a> |
                <a href="/article/{0}/edit">edit</a>)
                <a href="/article/{0}">{1}</a><br />
                '''.format(
                    article['id'],
                    article['title']
                )
            )


class BlogCreate(BaseBlog):

    def __iter__(self):
        if self.environ['REQUEST_METHOD'].upper() == 'POST':
            from urllib.parse import parse_qs
            values = parse_qs(self.environ['wsgi.input'].read())
            max_id = max([art['id'] for art in ARTICLES])
            ARTICLES.append(
                {'id': max_id+1,
                 'title': values[b'title'].pop().decode(),
                 'content': values[b'content'].pop().decode()
                 }
            )
            self.start('302 Found',
                       [('Content-Type', 'text/html'),
                        ('Location', '/')])
            return

        self.start('200 OK', [('Content-Type', 'text/html')])
        yield b'<h1><a href="/">Simple Blog</a> -> CREATE</h1>'
        yield b'''
            <form action="" method="POST">
                Title:<br>
                <input type="text" name="title"><br>
                Content:<br>
                <textarea name="content"></textarea><br><br>
                <input type="submit" value="Submit">
            </form>
            '''


class BlogRead(BaseArticle):

    def __iter__(self):
        if not self.article:
            self.start('404 Not Found', [('content-type', 'text/plain')])
            yield b'not found'
            return

        self.start('200 OK', [('Content-Type', 'text/html')])
        yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
        yield str.encode(
            '<h2>{}</h2>'.format(self.article['title'])
        )
        yield str.encode(self.article['content'])


class BlogUpdate(BaseArticle):

    def __iter__(self):
        if self.environ['REQUEST_METHOD'].upper() == 'POST':
            from urllib.parse import parse_qs
            values = parse_qs(self.environ['wsgi.input'].read())
            self.article['title'] = values[b'title'].pop().decode()
            self.article['content'] = values[b'content'].pop().decode()
            self.start('302 Found',
                       [('Content-Type', 'text/html'),
                        ('Location', '/')])
            return
        self.start('200 OK', [('Content-Type', 'text/html')])
        yield b'<h1><a href="/">Simple Blog</a> -> UPDATE</h1>'
        yield str.encode(
            '''
            <form action="" method="POST">
                Title:<br>
                <input type="text" name="title" value="{0}"><br>
                Content:<br>
                <textarea name="content">{1}</textarea><br><br>
                <input type="submit" value="Submit">
            </form>
            '''.format(
                self.article['title'],
                self.article['content']
            )
        )


class BlogDelete(BaseArticle):

    def __iter__(self):
        self.start('302 Found',  # '301 Moved Permanently',
                   [('Content-Type', 'text/html'),
                    ('Location', '/')])
        ARTICLES.pop(self.index)
        yield b''
Previous: Paste Next: Маршруты