Single Responsibility Principle
Ignacy Sokołowski
Ignacy Sokołowski
Every class, method, function should be responsible for only one thing.
Robert C. Martin a.k.a. Uncle Bob, Principles of Object Oriented Programming
Part of SOLID principles.
What does it mean to be responsible for only one thing?
It will always do more than one thing.
One function can invoke several other functions
but it will not be responsible for how they are implemented.
def send_email_report(user, category, time_period):
data = get_report_data(category, time_period)
report_body = format_report(data)
send_email(user.email, report_body)
If you'd like to change the way reports are formatted,
you should not need to change the class responsible for preparing the report
data.
If you'd like to change the database for articles,
you should not need to change the controller for displaying articles.
If it does more than one thing, you need to write lots of different tests for it.
Each test executes the whole body. It takes time. Or you need to mock a lot.
You don't write sufficent amount of tests simply because it's too much testing.
def validate_signature(xml, developer, app, secret):
timestamp = get_xpath(xml, './Timestamp/text()')
sig = ''.join([timestamp, developer, app, secret])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
xml_sig = get_xpath(xml, './Signature/text()')
if xml_sig != b64sig:
raise SignatureError('XML signature is invalid.')
def validate_signature(xml, developer, app, secret):
timestamp = get_xpath(xml, './Timestamp/text()')
xml_sig = get_xpath(xml, './Signature/text()')
sig = ''.join([timestamp, developer, app, secret])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
if xml_sig != b64sig:
raise SignatureError('XML signature is invalid.')
def validate_signature(xml, developer, app, secret):
timestamp = get_xpath(xml, './Timestamp/text()')
xml_sig = get_xpath(xml, './Signature/text()')
# Calculate valid signature.
sig = ''.join([timestamp, developer, app, secret])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
if xml_sig != b64sig:
raise SignatureError('XML signature is invalid.')
def validate_signature(xml, developer, app, secret):
timestamp = get_xpath(xml, './Timestamp/text()')
xml_sig = get_xpath(xml, './Signature/text()')
valid_sig = calculate_valid_signature(timestamp, developer, app, secret)
if xml_sig != valid_sig:
raise SignatureError('XML signature is invalid.')
def calculate_valid_signature(timestamp, developer, app, secret):
sig = ''.join([timestamp, developer, app, secret])
md5sig = hashlib.md5(sig.encode()).digest()
return base64.b64encode(md5sig).decode()
class User(object):
# [...]
def send_report(self, category, start_date, end_date):
email_subject = '{} report for {} - {}'.format(
category,
start_date.strftime('%Y-%m-%d'),
end_date.strftime('%Y-%m-%d'),
)
offers = db_session.query(Offer).filter(
Offer.category == category,
Offer.when_created.between(start_date, end_date),
).order_by(
Offer.when_created.desc(),
).all()
template_path = 'report_{}.html'.format(self.report_type)
template_context = {
'user': self,
'category': category,
'offers': offers,
'start_date': start_date,
'end_date': end_date,
}
email_body = render_template(template_path, template_context)
email_message = email.Message(email_subject, email_body)
smtp = smtplib.SMTP(config.smtp_uri)
smtp.sendmail(config.sender_email, self.email, email_message.as_string())
class EmailReporter(object):
def send_report(self, user, category, start_date, end_date):
email_subject = '{} report for {} - {}'.format(
category,
start_date.strftime('%Y-%m-%d'),
end_date.strftime('%Y-%m-%d'),
)
offers = db_session.query(Offer).filter(
Offer.category == category,
Offer.when_created.between(start_date, end_date),
).order_by(
Offer.when_created.desc(),
).all()
template_path = 'report_{}.html'.format(user.report_type)
template_context = {
'user': user,
'category': category,
'offers': offers,
'start_date': start_date,
'end_date': end_date,
}
email_body = render_template(template_path, template_context)
email_message = email.Message(email_subject, email_body)
smtp = smtplib.SMTP(config.smtp_uri)
smtp.sendmail(config.sender_email, user.email, email_message.as_string())
class Report(object):
def __init__(self, user, category, offers, period):
self.user = user
self.category = category
self.offers = offers
self.period = period
class Period(object):
def __init__(self, start, end):
self.start = start
self.end = end
def __str__(self):
return '{} - {}'.format(
self.start.strftime('%Y-%m-%d'),
self.end.strftime('%Y-%m-%d'),
)
class ReportPreparator(object):
def __init__(self, offers_finder):
self._offers_finder = offers_finder
def prepare(self, user, category, period):
offers = self._offers_finder.find(category, period)
return Report(user, category, offers, period)
class OffersFinder(object):
def find(self, category, period):
return db_session.query(Offer).filter(
Offer.category == category,
Offer.when_created.between(period.start, period.end),
).order_by(
Offer.when_created.desc(),
).all()
class EmailReporter(object):
def __init__(self, report_preparator):
self._report_preparator = report_preparator
def send_report(self, user, category, period):
email_subject = '{} report for {}'.format(category, period)
report = self._report_preparator.prepare(user, category, period)
template_path = 'report_{}.html'.format(user.report_type)
template_context = {
'user': report.user,
'category': report.category,
'offers': report.offers,
'period': report.period,
}
email_body = render_template(template_path, template_context)
email_message = email.Message(email_subject, email_body)
smtp = smtplib.SMTP(config.smtp_uri)
smtp.sendmail(config.sender_email, user.email, email_message.as_string())
class HTMLReportFormatter(object):
def format(self, report):
template_path = 'report_{}.html'.format(report.user.report_type)
template_context = {
'user': report.user,
'category': report.category,
'offers': report.offers,
'period': report.period,
}
return render_template(template_path, template_context)
class EmailReporter(object):
def __init__(self, report_preparator, report_formatter):
self._report_preparator = report_preparator
self._report_formatter = report_formatter
def send_report(self, user, category, period):
email_subject = '{} report for {}'.format(category, period)
report = self._report_preparator.prepare(user, category, period)
email_body = self._report_formatter.format(report)
email_message = email.Message(email_subject, email_body)
smtp = smtplib.SMTP(config.smtp_uri)
smtp.sendmail(config.sender_email, user.email, email_message.as_string())
class ReportEmailPreparator(object):
def __init__(self, report_formatter):
self._report_formatter = report_formatter
def prepare(self, report):
subject = self._prepare_subject(report)
body = self._report_formatter.format(report)
return email.Message(subject, body)
def _prepare_subject(self, report):
return '{} report for {}'.format(report.category, report.period)
class EmailReporter(object):
def __init__(self, report_preparator, report_email_preparator):
self._report_preparator = report_preparator
self._report_email_preparator = report_email_preparator
def send_report(self, user, category, period):
report = self._report_preparator.prepare(user, category, period)
email_message = self._report_email_preparator.prepare(report)
smtp = smtplib.SMTP(config.smtp_uri)
smtp.sendmail(config.sender_email, user.email, email_message.as_string())
class EmailSender(object):
def __init__(self, smtp_uri, sender_address):
self._smtp_uri = smtp_uri
self._sender_address = sender_address
def send(self, address, message):
smtp = smtplib.SMTP(self._smtp_uri)
smtp.sendmail(self._sender_address, address, message.as_string())
class EmailReporter(object):
def __init__(self, report_preparator, report_email_preparator, email_sender):
self._report_preparator = report_preparator
self._report_email_preparator = report_email_preparator
self._email_sender = email_sender
def send_report(self, user, category, period):
report = self._report_preparator.prepare(user, category, period)
email_message = self._report_email_preparator.prepare(report)
self._email_sender.send(user.email, email_message)
User
Report
Period
EmailReporter
ReportPreparator
OffersFinder
HTMLReportFormatter
ReportEmailPreparator
EmailSender
class User(object):
# [...]
def send_report(self):
pass
def prepare_report(self):
pass
def make_report_email_message(self):
pass
def make_report_email_message_subject(self):
pass
def make_report_email_message_body(self):
pass
def render_report_email_template(self):
pass
def get_report_email_template_path(self):
pass
def get_report_email_template_context(self):
pass
def get_offers_for_report(self):
pass
def get_offers_search_query_for_report(self):
pass
def send_email(self):
pass
# [...]
def write_lines(self, lines, encoding=None, errors='strict',
linesep=os.linesep, append=False):
if append:
mode = 'ab'
else:
mode = 'wb'
f = self.open(mode)
try:
for line in lines:
isUnicode = isinstance(line, unicode)
if linesep is not None:
# Strip off any existing line-end and add the
# specified linesep string.
if isUnicode:
if line[-2:] in (u'\r\n', u'\x0d\x85'):
line = line[:-2]
elif line[-1:] in (u'\r', u'\n',
u'\x85', u'\u2028'):
line = line[:-1]
else:
if line[-2:] == '\r\n':
line = line[:-2]
elif line[-1:] in ('\r', '\n'):
line = line[:-1]
line += linesep
if isUnicode:
if encoding is None:
encoding = sys.getdefaultencoding()
line = line.encode(encoding, errors)
f.write(line)
finally:
f.close()
def article_controller(request):
article_id = request.GET['id']
article = get_article(article_id)
return (
'<html><head><title>{article.title}</title></head>'
'<body><h1>{article.title}</h1>{article.content}</body></html>'
).format(article=article)
def article_controller(request):
article_id = request.GET['id']
article = get_article(article_id)
css_class = 'new' if article.is_new else ''
context = {'article': article, 'class': css_class}
return render_template('article.html', context)
def article_controller(request):
article_id = request.GET['id']
article = cursor.execute(
'SELECT * FROM articles WHERE id = %s', article_id
).fetchone()
return render_template('article.html', {'article': article})