Having written and then refactored a python "God object", I sympathize. What I did is break the original object down into sub sections based upon methods. For instance, the original looked like this pseudo code:
method A():
self.bla += 1
method B():
self.bla += 1
do stuff():
self.bla = 1
method A()
method B()
print self.bla
The stuff method is a self contained "unit" of work. I migrated it out to a new class which the original instantiates. This pulled out the necessary properties as well. Some were used only by the sub class and could move straight across. Others were shared, and got moved into a shared class.
The "God object" creates a new copy of the shared class at start up, and each of the new sub classes accepts a pointer as part of their init method. For example, here's a stripped version of the mailer:
#!/usr/bin/env python
# -*- coding: ascii -*-
'''Functions for emailing with dirMon.'''
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
from email import Encoders
import os
import smtplib
import datetime
import logging
class mailer:
def __init__(self,SERVER="mail.server.com",FROM="support@server.com"):
self.server = SERVER
self.send_from = FROM
self.logger = logging.getLogger('dirMon.mailer')
def send_mail(self, send_to, subject, text, files=[]):
assert type(send_to)==list
assert type(files)==list
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(' '.join(("Sending email to:",' '.join(send_to))))
self.logger.debug(' '.join(("Subject:",subject)))
self.logger.debug(' '.join(("Text:",text)))
self.logger.debug(' '.join(("Files:",' '.join(files))))
msg = MIMEMultipart()
msg['From'] = self.send_from
msg['To'] = COMMASPACE.join(send_to)
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = subject
msg.attach( MIMEText(text) )
for f in files:
part = MIMEBase('application', "octet-stream")
part.set_payload( open(f,"rb").read() )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f))
msg.attach(part)
smtp = smtplib.SMTP(self.server)
mydict = smtp.sendmail(self.send_from, send_to, msg.as_string())
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug("Email Successfully Sent!")
smtp.close()
return mydict
It is created once and shared between the different classes that need mailing capabilities.
So for you, create a class larry
with the properties and methods you need. Everywhere the client says larry = blah
replace it with larryObj.larry = blah
. This migrates things to sub projects without breaking the current interface.
The only other thing to do is look for "units of work". If you were going to turn part of the "God Object" into it's own method, do so. But, put the method outside it. This forces you to create an interface between the components.
Laying that groundwork allows everything else to follow it. For example, a piece of the helper object demonstrating how it interfaces with the mailer:
#!/usr/bin/env python
'''This module holds a class to spawn various subprocesses'''
import logging, os, subprocess, time, dateAdditionLib, datetime, re
class spawner:
def __init__(self, mailer):
self.logger = logging.getLogger('dirMon.spawner')
self.myMailer = mailer
Concentrate on the smallest individual unit of work possible, and move it out. This is easier to do, and lets you play with the setup quickly. Don't look at properties for moving stuff, they are ancillary to the tasks being done with them in most cases. Whatever is left over after you have dealt with the methods should probably stay in the original object, since it is part of the shared state.
But, the new objects should now accept the properties they need as init variables, not touching the caller objects property. They then return any necessary values, which the caller can use to update the shared properties as necessary. This helps to decouple the objects and makes for a more robust system.
Basically, you have two issues here:
- E-mails are dispatched from presentation layer,
- The API is tied up to physical e-mail implementation.
The first issue is solved by the first step of moving the code where it belongs: in business layer. You shouldn't save companies from presentation layer, and you shouldn't send e-mails from there: move all this code where it belongs to, and focus presentation layer on presentation, i.e. generation of HTML code or JSON or XML from a model.
Business layer:
public class Example
{
public void ProcessSamplePage(...)
{
CustomerManagement.Create(...);
SmtpService.NotifyCustomerRegistered(...);
var model = ...
Presentation.GenerateHtml(model);
}
}
Moving from ASP.NET to ASP.NET MVC may help avoiding such errors in future.
The next step is Dependency Injection, which also solves the second problem. Instead of using a specific class provided by .NET Framework to send e-mails directly from your business layer, you can create an interface which will have a specific method for dispatching messages, and then using whatever implementation you want. Dispatching message can mean sending an actual e-mail through .NET Framework, sending an e-mail through a third-party library, sending an e-mail by manually connecting to SMTP server, storing a text file in a directory where the SMTP service will pick it or... storing the message in the database.
Business layer:
public class Example
{
public Example(IMessagesDispatcher dispatcher) { ... }
public void ProcessSamplePage(...)
{
CustomerManagement.Create(...);
this.dispatcher.NotifyCustomerRegistered(...);
var model = ...
Presentation.GenerateHtml(model);
}
}
Now that you can store messages in the database instead of just sending them through e-mails, you can imagine more complex scenarios, such as a Windows Service which checks regularly the database and sends pending e-mails. This also allows to control the load of SMTP server: if you have to send hundreds of e-mails per second between 2 PM and 3 PM, but only a few dozen e-mails per minute at night, you can reschedule low-priority e-mails to be actually dispatched at night.
Best Answer
Generally, you use class attributes to provide defaults:
You only need to define a
__init__
if you need to provide a new instance with attributes that have to be uniquely set for that new instance.However, don't define a class just to hold state. Use classes when you are going to provide both state and behaviour. Python's standard types and library is otherwise more than rich enough to cover the just-data needs.