01 - Get-it
Parte 3: Separando algumas responsabilidades
O nosso código já está ficando grande e ele não faz quase nada! Um dos motivos para isso é a falta de coesão desse arquivo (lembra de Ágil?): esse arquivo possui a string HTML da página, que por sua vez contém todos os dados das anotações disponíveis, além do código que trata as conexões, requisições e respostas. Imagine o que aconteceria com uma quantidade razoável de anotações!
Essas três responsabilidades acumuladas no mesmo arquivo estão relacionadas a um conceito chamado Model, View, Controller (MVC). Vocês tiveram um breve contato com esse conceito em Desenvolvimento Colaborativo Ágil, mas nós discutiremos mais a respeito em um futuro próximo.
Modelo
Vamos começar separando a responsabilidade do modelo (lista de anotações) da responsabilidade de visualização (string HTML). Para isso, vamos criar uma lista de dicionários que contém os dados das anotações e a string HTML será gerada dinamicamente a partir desses dados:
import socket
from pathlib import Path
from utils import extract_route, read_file, load_data
CUR_DIR = Path(__file__).parent
SERVER_HOST = '0.0.0.0'
SERVER_PORT = 8080
NOTE_TEMPLATE = ''' <li>
<h3>{title}</h3>
<p>{details}</p>
</li>
'''
RESPONSE_TEMPLATE = '''HTTP/1.1 200 OK
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Get-it</title>
</head>
<body>
<img src="img/logo-getit.png">
<p>Como o Post-it, mas com outro verbo</p>
<ul>
{notes}
</ul>
</body>
</html>
'''
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((SERVER_HOST, SERVER_PORT))
server_socket.listen()
print(f'Servidor escutando em (ctrl+click): http://{SERVER_HOST}:{SERVER_PORT}')
while True:
client_connection, client_address = server_socket.accept()
request = client_connection.recv(1024).decode()
print('*'*100)
print(request)
route = extract_route(request)
filepath = CUR_DIR / route
if filepath.is_file():
response = 'HTTP/1.1 200 OK\n\n'.encode() + read_file(filepath)
else:
# Cria uma lista de <li>'s para cada anotação
# Se tiver curiosidade: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
notes_li = [
NOTE_TEMPLATE.format(title=dados['titulo'], details=dados['detalhes'])
for dados in load_data('notes.json')
]
notes = '\n'.join(notes_li)
response = RESPONSE_TEMPLATE.format(notes=notes).encode()
client_connection.sendall(response)
client_connection.close()
server_socket.close()
Você também vai precisar do arquivo notes.json
(clique aqui para baixar). Coloque-o em uma pasta chamada data
:
- DIRETORIO-DO-SEU-SERVIDOR
|- servidor.py
|- utils.py
|- test_utils.py
|- data
|- notes.json
|- img
|- logo-getit.png
EXERCÍCIO
Implemente a função load_data
, que recebe o nome de um arquivo JSON e devolve o conteúdo do arquivo carregado como um objeto Python (A função deve assumir que este arquivo JSON está localizado dentro da pasta data
). Por exemplo: se o conteúdo do arquivo data/dados.json
for a string {"chave": "valor"}
, sua função deve devolver o dicionário Python {"chave": "valor"}
para a entrada dados.json
(note que o nome da pasta não é enviado como argumento). Dica: já existe uma função Python para isso (e você viu em Design de Software).
Question
No código anterior, estamos utilizando formatação de string
um pouco diferente do que aprendemos em DesSoft.
Vamos relembrar como utilizávamos o método .format
Considere o código a seguir:
x = 3
y = 4
z = x * y
texto = 'O retângulo de lados {0} e {1} tem área {2}'
print(texto.format(x, y, z))
Escolhe o será impresso no terminal:
- O retângulo de lados {0} e {1} tem área {2}
- O retângulo de lados 0 e 1 tem área 2
- O retângulo de lados 3 e 4 tem área 12
- O retângulo de lados 4 e 3 tem área 12
Resposta
Será impresso O retângulo de lados 3 e 4 tem área 12
, pois o método .format
substituirá os valores entre chaves de acordo com a ordem em que os argumentos x
, y
e z
foram passados. Para mais detalhes acesse: https://docs.python.org/3/tutorial/inputoutput.html#the-string-format-method
Question
No código do servidor, utilizamos o método .format
de outra maneira possível.
A maneira utilizada é similar ao código a seguir:
Escolhe o será impresso no terminal:
- This spam is absolutely horrible.
- This {food} is {adjective}.
- This absolutely horrible is spam.
- This food is adjective.
Resposta
Será impresso This spam is absolutely horrible.
, pois o método .format
substituirá os valores entre chaves de acordo com os nomes utilizados food
e adjective
. Para mais detalhes acesse: https://docs.python.org/3/tutorial/inputoutput.html#the-string-format-method
EXERCÍCIO
Tente reescrever o trecho de código abaixo utilizando o loop for
.
Caso não esteja familiarizado com list-comprehensions
acesse: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
# Cria uma lista de <li>'s para cada anotação
# Se tiver curiosidade: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
notes_li = [
NOTE_TEMPLATE.format(title=dados['titulo'], details=dados['detalhes'])
for dados in load_data('notes.json')
]
notes = '\n'.join(notes_li)
response = RESPONSE_TEMPLATE.format(notes=notes).encode()
Visualização
Ufa, já está um pouco melhor. Se quisermos adicionar mais anotações basta modificar o arquivo notes.json
. Lembra da ideia de mantermos um baixo acoplamento? Aqui nós conseguimos melhorar esse ponto. Se eu quero adicionar mais dados eu só modifico o arquivo de dados (notes.json
) e nenhum outro. O resto do código é independente disso.
Mas ainda dá para melhorar. Vamos refatorar um pouco mais o nosso código, separando a responsabilidade de visualização (HTML).
EXERCÍCIO
Crie uma pasta chamada templates
e crie dentro dela um arquivo index.html
com o conteúdo da string RESPONSE_TEMPLATE
a partir da terceira linha, inclusive, ou seja, a partir de <!DOCTYPE html>
. O HTTP/1.1 200 OK
não deve ser incluído. Além disso, não coloque aspas entorno do html.
Ainda dentro da pasta templates
, crie outra pasta chamada components
e dentro dessa nova pasta um arquivo note.html
com o conteúdo da string NOTE_TEMPLATE
.
A sua estrutura de arquivos agora deve ser:
- DIRETORIO-DO-SEU-SERVIDOR
|- servidor.py
|- utils.py
|- test_utils.py
|- data
|- notes.json
|- img
|- logo-getit.png
|- templates
|- index.html
|- components
|- note.html
EXERCÍCIO
Implemente a função load_template
que recebe o nome de um arquivo de template e devolve uma string com o conteúdo desse arquivo. O nome do arquivo não inclui o nome da pasta templates
. Por exemplo: para a entrada index.html
você deve carregar o conteúdo do arquivo templates/index.html
.
Vamos atualizar o código do servidor:
import socket
from pathlib import Path
from utils import extract_route, read_file, load_data, load_template
CUR_DIR = Path(__file__).parent
SERVER_HOST = '0.0.0.0'
SERVER_PORT = 8080
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((SERVER_HOST, SERVER_PORT))
server_socket.listen()
print(f'Servidor escutando em (ctrl+click): http://{SERVER_HOST}:{SERVER_PORT}')
while True:
client_connection, client_address = server_socket.accept()
request = client_connection.recv(1024).decode()
print('*'*100)
print(request)
route = extract_route(request)
filepath = CUR_DIR / route
if filepath.is_file():
response = read_file(filepath)
else:
# Cria uma lista de <li>'s para cada anotação
# Se tiver curiosidade: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
note_template = load_template('components/note.html')
notes_li = [
note_template.format(title=dados['titulo'], details=dados['detalhes'])
for dados in load_data('notes.json')
]
notes = '\n'.join(notes_li)
response = load_template('index.html').format(notes=notes).encode()
client_connection.sendall('HTTP/1.1 200 OK\n\n'.encode() + response)
client_connection.close()
server_socket.close()
Controle de rotas
O código do servidor ainda possui duas responsabilidades diferentes: decidir qual rota seguir e o que fazer em cada rota (o que pode ser tão complexo quanto se queira). Vamos separar a responsabilidade de cada rota em uma função diferente:
import socket
from pathlib import Path
from utils import extract_route, read_file
from views import index
CUR_DIR = Path(__file__).parent
SERVER_HOST = '0.0.0.0'
SERVER_PORT = 8080
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((SERVER_HOST, SERVER_PORT))
server_socket.listen()
print(f'Servidor escutando em (ctrl+click): http://{SERVER_HOST}:{SERVER_PORT}')
while True:
client_connection, client_address = server_socket.accept()
request = client_connection.recv(1024).decode()
print('*'*100)
print(request)
route = extract_route(request)
filepath = CUR_DIR / route
if filepath.is_file():
response = read_file(filepath)
elif route == '':
response = index()
else:
response = bytes()
client_connection.sendall('HTTP/1.1 200 OK\n\n'.encode() + response)
client_connection.close()
server_socket.close()
Você também vai precisar criar o arquivo views.py
com o seguinte conteúdo (note que é exatamente o mesmo código que estava no else
):
from utils import load_data, load_template
def index():
# Cria uma lista de <li>'s para cada anotação
# Se tiver curiosidade: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
note_template = load_template('components/note.html')
notes_li = [
note_template.format(title=dados['titulo'], details=dados['detalhes'])
for dados in load_data('notes.json')
]
notes = '\n'.join(notes_li)
return load_template('index.html').format(notes=notes).encode()
Question
No trecho de código retirado do arquivo servidor.py
:
filepath = CUR_DIR / route
if filepath.is_file():
response = read_file(filepath)
elif route == '':
response = index()
else:
response = bytes()
A condição if filepath.is_file():
será verdadeira se:
- O arquivo
filepath
for um arquivo. - O arquivo
filepath
não for um arquivo. - O arquivo
filepath
for um diretório. - O arquivo
filepath
não for um diretório.
Resposta
A condição if filepath.is_file():
será verdadeira se filepath
for um arquivo. Em alguns casos, filepath
será img/logo-getit.png
e, portanto, a condição será verdadeira, pois logo-getit.png
é um arquivo.
Desta forma, a função read_file
será chamada e o conteúdo do arquivo logo-getit.png
será retornado e enviado como resposta para o navegador.
EXERCÍCIO
Momento de reflexão: Pense em quais casos o servidor entrará no elif route == '':
e no else:
.
Agora o nosso código está pronto para a parte 4 do handout!