Chapter "Create Request / Response / View class to improve visibility" The has been updated.
If you want to read more, please "like" or "follow me" in Book ;-)
The following is an excerpt of the contents of the book.
There are now three endpoints that generate dynamic responses, and workerthread.py
is now close to 200 lines.
Even at this point, I'm doing a lot of different things with one file, so even 200 lines has become a messy module with very poor visibility.
What's more, as you evolve this web application, you will have more and more endpoints.
It is obvious that maintenance will not be possible if you add it to workerthread.py
each time.
It can be said that it has become necessary to improve the visibility of workerthread.py
by separating responsibilities and dividing files.
In other words, it's about time ** the season for refactoring has arrived **.
In this chapter, we will cut out "processing that dynamically generates a response body for each endpoint" to an external module.
First, let's simply cut out the HTML generation process for each endpoint into another module.
The name of the module to cut out is views
.
This is because it is a module whose responsibility is only to generate the view part (= request body), regardless of the HTTP situation such as connection or header parsing.
study/workerthread.py
https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter16/workerthread.py#L50-L59
study/views.py
https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter16/views.py
study/workerthread.py
if path == "/now":
response_body, content_type, response_line = views.now()
elif path == "/show_request":
response_body, content_type, response_line = views.show_request(
method, path, http_version, request_header, request_body
)
elif path == "/parameters":
response_body, content_type, response_line = views.parameters(method, request_body)
Until the last time, I wrote that the process of generating HTML was sticky for each path, but first I decided to cut out that part to the function of the views
module.
by this,
--workerthread.py
receives the HTTP request, parses it, gets the response contents from the function of the views
module according to the path, constructs the HTTP response, and returns it to the client.
--views.py
has a function according to each path, receives the contents of the request and returns the contents of the dynamically generated response.
The task of "dynamically generating the content of the response according to the path" was cut out in views
.
study/views.py
import textwrap
import urllib.parse
from datetime import datetime
from pprint import pformat
from typing import Tuple, Optional
def now() -> Tuple[bytes, Optional[str], str]:
"""
Generate HTML to display the current time
"""
html = f"""\
<html>
<body>
<h1>Now: {datetime.now()}</h1>
</body>
</html>
"""
response_body = textwrap.dedent(html).encode()
# Content-Specify Type
content_type = "text/html; charset=UTF-8"
#Generate response line
response_line = "HTTP/1.1 200 OK\r\n"
return response_body, content_type, response_line
def show_request(
method: str,
path: str,
http_version: str,
request_header: dict,
request_body: bytes,
) -> Tuple[bytes, Optional[str], str]:
"""
Generate HTML to display the contents of the HTTP request
"""
html = f"""\
<html>
<body>
<h1>Request Line:</h1>
<p>
{method} {path} {http_version}
</p>
<h1>Headers:</h1>
<pre>{pformat(request_header)}</pre>
<h1>Body:</h1>
<pre>{request_body.decode("utf-8", "ignore")}</pre>
</body>
</html>
"""
response_body = textwrap.dedent(html).encode()
# Content-Specify Type
content_type = "text/html; charset=UTF-8"
#Generate response line
response_line = "HTTP/1.1 200 OK\r\n"
return response_body, content_type, response_line
def parameters(
method: str,
request_body: bytes,
) -> Tuple[bytes, Optional[str], str]:
"""
Display HTML to display POST parameters
"""
#Returns 405 for GET requests
if method == "GET":
response_body = b"<html><body><h1>405 Method Not Allowed</h1></body></html>"
content_type = "text/html; charset=UTF-8"
response_line = "HTTP/1.1 405 Method Not Allowed\r\n"
elif method == "POST":
post_params = urllib.parse.parse_qs(request_body.decode())
html = f"""\
<html>
<body>
<h1>Parameters:</h1>
<pre>{pformat(post_params)}</pre>
</body>
</html>
"""
response_body = textwrap.dedent(html).encode()
# Content-Specify Type
content_type = "text/html; charset=UTF-8"
#Generate response line
response_line = "HTTP/1.1 200 OK\r\n"
return response_body, content_type, response_line
This is not particularly difficult either.
I just brought in the exact process of dynamically generating the response that was originally written in workerthread.py
.
It is good to cut out the views function, but as it is now, the number of arguments is different for each function, "The mantissa that processes this path needs the arguments of this and this, and the function that processes this path needs the arguments of that, that, and that ..." And so on, the caller must know the details of the caller.
In the world of programming, it is known that the source code becomes simple when one module is made so that the details of the other module are not known as much as possible.
Let's refactor a little more in the next STEP and realize that.
Now the WorkerThread
class needs to know the details of the views
function because it can't be called without knowing what and how many arguments it needs for each function.
An easy way to get rid of this situation is to ** "I don't know which parameter each function uses, but I'll pass it all anyway" **.
Let's actually take a look at the source code.
study/workerthread.py
https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter16-2/workerthread.py
study/views.py
https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter16-2/views.py
study/views.py
Let's start with views.py
def now(
method: str,
path: str,
http_version: str,
request_header: dict,
request_body: bytes,
) -> Tuple[bytes, Optional[str], str]:
def parameters(
method: str,
path: str,
http_version: str,
request_header: dict,
request_body: bytes,
) -> Tuple[bytes, Optional[str], str]:
Arguments are unified in all view functions so that all request information can be received. These arguments are not used in the function, but by making them available, the caller does not have to think about "what is needed and what is not needed".
study/workerthread.py
Next is the side that calls the view function.
#Correspondence between path and view functions
URL_VIEW = {
"/now": views.now,
"/show_request": views.show_request,
"/parameters": views.parameters,
}
The correspondence between the path and view functions is defined as a constant. It is a ** dictionary that has path as a key and ** view function corresponding to path as a value.
Depending on the language, you may be surprised to "set a" function "as a dictionary (or associative array) value" or "assign a" function "to a variable" as described above.
But in python this is a legitimate treatment.
Objects that can be handled as values, such as assigning to variables and passing to operations and functions (as arguments and return values), are called ** first-class citizens **. In python ** all objects are first-class citizens **, and functions are no exception.
Therefore, it is also possible to assign a function to a variable or create a function that receives a function and returns a function.
The latter is known as "metaprogramming" and anyone interested should check it out.
#If there is a view function corresponding to path, get the function and call it to generate a response
if path in self.URL_VIEW:
view = self.URL_VIEW[path]
response_body, content_type, response_line = view(
method, path, http_version, request_header, request_body
)
path in self.URL_VIEW
checks to see if the dictionary key self.URL_VIEW
contains path
.
In other words, we are checking if the view function corresponding to path is registered.
If it has been registered, the value of the dictionary corresponding to that key is acquired and assigned to the variable view
.
That is, the variable view
is assigned the ** view function ** (rather than the return value of calling the view function).
In the last line, view (~~)
is used to call the function assigned to the variable view
and get the return value.
It's worth noting that ** all view functions now take the same arguments (method, path, http_version, request_header, request_body
), which abstracts the view function. ** **
Previously, the arguments were different for each function, so even if you said "call the view function", you couldn't call it correctly unless you knew "what kind of function the function is". However, by unifying the arguments (= unifying the interface), ** "I don't know what the function is, but I can call it anyway" **.
This eliminates the need for if branching according to path (or function) within workerthread
.
In this way, "it is possible to avoid having to deal with concrete things by extracting only some of the common properties from concrete things" is called ** abstraction **. , It is a very important technique in programming.
In this case, by unifying the interface from concrete functions such as now ()
show_rewuest ()
parameters
"Takes 5 arguments method, path, http_version, request_header, request_body
and returns 2 values response_body, response_line
"
By extracting (= abstracting) only the property, the caller
"I don't know how many functions it is, but I call it with 5 arguments."
It means that it can be handled like this.
Or you could say "unified interface for abstraction".
It's good that the interface of the view function is standardized and the caller's view is better, but there are many five arguments.
The fact that an HTTP request has a lot of information is an unavoidable fact, but it's awkward to have it distributed and stored in disparate variables.
So, let's create a class that expresses the HTTP request and put the information together there.
This also simplifies the interface of the view function.
Chapter "Create Request / Response / View class to improve visibility"
Recommended Posts