Fog Creek Software
Discussion Board




Design question

I work by myself and have no one to bounce ideas off of so here goes...
We have an email app that sends notifications to clients from an e-commerce site (shipping notification, approval notification, etc).  Because of custom features that clients have asked for it has become a monster.  It actually has client specific functions commented with "Don't use this for anyone except client X".  So I am refactoring (don't tell me not to 'cause I'm going to do it anyway!).

I now have things like:
Public Sub assembleShippingMessage(ByVal printID, ByVal prefix)
    m_utility.Visit.Postmaster.assembleHeader()
    m_utility.Visit.Postamaster.assembleTableHeader()
    m_utility.Visit.Postmaster.assembleStaticMessage()
    m_utility.Visit.Postmaster.assembleFooter()
End Sub

The problem now is when we call "assembleShippingMessage", we still need to pass the client specific parameters to assemble the message.  This does not eliminate the problem of "client X wants widget info in their shipping email while client Y does not".  How do you make it generic yet customizable without calling all function body lines from the client ASP page?  Is that even possible to avoid this problem?

Thanks in advance :)  I hate being alone :(

shiggins
Thursday, August 14, 2003

I'd recommend not doing it in the ASP pages.  Can you set up some triggering mechanisms on your database and integrate that with some messaging system?  You'd be able to develop a more extendable system that way.

chris
Thursday, August 14, 2003

I'm actually not doing it in the ASP pages.  We wrote a COM object that is called from the ASP pages.  The reason we custom wrote it is that the clients are VERY particular about the look and feel of the emails and we wanted to be sure to have full control of the messaging system.  I looked into SQL Mail but couldn't get it configured.  It initially took me less time to write the COM object that it would to configure SQL Mail (just because I'm not familiar with it).  But now the COM object is bloated as h*ll.

shiggins
Thursday, August 14, 2003

You don't need to use SQL Mail, just use the database to build up your event/messaging system.  Instead of doing the messages from the asp pages, add a triggering device on the database which is fired whenever an order is created.  You can use com objects to hit the database and do the actual building and sending of the messages based on data from the DB.  This allows you to build a data driven messaging system which will eventually be more flexible, extendible, scalable and recoverable.

chris
Thursday, August 14, 2003

Just to continue a bit...  So for instance every time an order is commited you'd insert a record into a table in your event database.  Then your com object would poll the event database looking for messages to send out.  The com object could use data from other tables in your event database to build the message based on the users preferences.

This puts all your customization stuff in the database instead of hard codings.

chris
Thursday, August 14, 2003

Ok.  Forgive me if this is a stupid question.  I like what you're saying, I'm just trying to understand it better.

I will have a message events table that would say things like whether the message is sent or not, etc.  Then my com object pulls from say my Inventory table to generate a message.  Then where are the user preferences defined?  How does the com object know to sometimes pull a column called inventory_desc and sometimes don't pull it?  Or do I set some flags in my message table.  Like a column called pull_inventory_desc.  If it's yes I pull it if no don't. 

Thanks for your advice...if I am understanding it.

shiggins
Thursday, August 14, 2003

You are on exactly the right track.  As for the customization aspect there are a number of ways to go depending on how extensible you want it.  You could do as you say and have a list of preferences mapped to each client (ClientEmailPreferences table).  Then you'd dynamically build the email based on those preferences from that table for the specific client. 

So your polling object would continually look for new messages that need to be send out and then gather the data necessary to send those messages based on the clients preferences and the data stored in your order database.  Then after sending the message you'd mark it as sent in your polled table.

You can make this type of thing generic enough to work for events other than email messages, such as fax messages, messages displayed to your web page based on the login info of the client etc etc.  You can also spread the work load over different machines as you grow etc.  You'd also be able to recover if the server crashed, or email stopped working or whatever and still see all the messages you need to send.

You sound like you've go the general idea though.

chris
Thursday, August 14, 2003

Yipee!!!!  I like it.  I'm off to dive into some tables :)

shiggins
Thursday, August 14, 2003

I've been using CDO to send email from SQL Server - it works just great. With SQL Server 2k (don't know about previous versions) you can instatiate, set properties, and call methods on *any* COM object.

That's how I get my daily error reports. :-)

Philo

Philo
Thursday, August 14, 2003

Rather than assembling the message in the code, wouldn't you be better of using templates?

Ged Byrne
Thursday, August 14, 2003

C++ templates?

shiggins
Thursday, August 14, 2003

I think he means "text templates."

As in a text file with replacement markers. When you generate the email, you do it by pulling up the template and filling in the replacements with the right data.

A lot like an ASP page, actually. ;-)

Chris Tavares
Thursday, August 14, 2003

BTW, what is it about the process that creating the text in code comes to mind first, then building a template is like a flash of insight later? I've gone through it, and I've seen others - "build the text in code" seems to be the knee-jerk response...

Philo

Philo
Thursday, August 14, 2003

What I've done in the past is have a table like:

tbl_client_features
(client_id, feature)

Presence in the table means that a client has that feature enabled. So "select feature where client_id ='MyClient'" gives a definitive configuration.

Code then becomes

if (features.contains("ship-date-notif")) {
  // build and send e-mail to customer with ship date
}

and not

if (clientID.equals("ClientX") || clientID.equals("ClientW")) {
  // build and send e-mail to customer with ship date
}

Code only needs to change when you add a new feature and not whenever there is a new combination of clients and features. You can add a level of indirection to make it very flexible:

tbl_feature_items
(feature_id, feature_item)

Then you can define a feature as a standard set of items:

insert into tbl_feature_items values ('auto-mailer', 'ship-date-notif')
insert into tbl_feature_items values ('auto-mailer', 'detailed-invoice')
insert into tbl_feature_items values ('auto-mailer', 'payment-auth-notif')
insert into tbl_feature_items values ('auto-mailer', 'package-confirm')

and assign the standard feature set to a client in one step:

insert into tbl_client_features values ('ClientX', 'auto-mailer')

and you can still be fine grained as needed, i.e. Client Y gets just the shipping date confirmation

insert into tbl_client_features values ('ClientY', 'ship-date-notif')

Note that tbl_client_features may have a feature_item token or a feature_id token. Your selects have to be smart enough to expand a feature_id into the relevant feature_items. I'm not sure if I got the idea across clearly or if it helps at all.

Jim S.
Thursday, August 14, 2003

Jim is getting really close on the object-orientedness of this thing, but he's missing just one more dimension.

If you can, you want to define all the components that can go into an email (such as 'ship-date-notif' and 'package-confirm'), so that they aren't listed in the code either.

Then the email assembly just becomes:

select feature where client_id=$ClientID ;
<expand tbl_feature_items>

And then assemble like thus:

foreach $feature in (<selected features>)
  add text to email from template[$feature]
endeach

Please excuse the horrible nomenclature.

Derek W

Derek Woolverton
Friday, August 15, 2003

hmmm... i'd go with a text based template. maybe even xsl, especially now that xsl designers are finally becoming realistic.

then build one xsl per customer at first.
later you can use includes to include the appropriate chunks of xsl.
later you can use xsl to build xsl or other template-bulding techniques. but for this purpose this probably should be done at build-time, not run-time.

mind you any text-based template can be built similarly.

why am i advocating a system like this versus the 'for-each feature add featuretext' approach? because it's more flexible in terms of ordering, etc--if customer A wants shipping address followed by form of payment, and customer B wants form of payment in a column to the left of shipping address... you'll need something more complex than the simple loop.

mb
Friday, August 15, 2003

You can solve this problem by creating a customer class which knows how to compose a message to each customer.
Each INSTANCE of the class knows how to read the message feature based on its customer id from some sort of database (e.g XML, SQL) .
You can use the decorator pattern to assmble message features in runtime.
When it is time to create a message, just call client.assambleMessage.

For example :

Customer c = new Customer("HP")
Message m = c.assambleMessage();
m.send()


....
class Customer {

public Message assambleMessage() {
      // read the metadata
      ...
      //assamble
        return new Message(new HeaderFotter(new Widget()));


}

tomer
Friday, August 15, 2003

I really do think your easiest approach is to have text templates.  Have one generic template (acknowlegement.template) and then append specific templates with the customer id (acknowledgement_Cust001.template).

The generater searches for the customers specific template, and if that doesn't exist uses the default.

This way admin can do any text changes and simple updates without having to involve a programmer or DBA.

Is there a compelling reason to build the text in the code?

Ged Byrne
Friday, August 15, 2003

I don't know if I can use a text template because the table in the email is built dynamically.  So client X may have 5 columns and client Y may have 2.  If I would have to create a new template for each possible scenario that would defeat the purpose of taking it out of the code.  My email features are very rarely the same for any client. 

So now I am doing what Derek & chris said.  I have a client prefs table that is just:

KEY    TABLE_NAME    MSG_TYPE          CLIENT
qty      Orders              Shipping            Client X
date    Inventory        Shipping            Client X
qty      Orders              Shipping            Client Y

So when I build an email I just loop through.  Client X's email gets the qty and date and client Y's only gets the qty.  I may be missing something with the templates.  I haven't had a chance to research it.  Using xml sounds like a possibility, however, I don't know xml well enough to say "yes" or "no".

Also,  thanks all for such good input!  This really got my brain cranking about different ways to do things.  Sometimes when you are by yourself, you do it the way you always do it.  It helps to have someone say "hey what about this..."

Thanks ;)

shiggins
Friday, August 15, 2003

OK.

So my point above is that the code has no specific client info anymore.  It has "generateHeader", "addTableRow", etc.

shiggins
Friday, August 15, 2003

Stephanie,

XML is definately one option for you.

You can take an ADO recordset and save it straight to XML.

Pass that XML to an XSL stylesheet and voila you can generate whatever you want.

This does have the overhead of learn XSLT though.  Take a look at www.w3cschools.com/xsl if your interested.

Ged Byrne
Friday, August 15, 2003

erm...

http://www.w3schools.com/xsl/

Ged Byrne
Friday, August 15, 2003

Thanks GED.  However, the link takes me to Nester?  Can you double check.  I'll google and see if I can find it.

shiggins
Friday, August 15, 2003

Got it.

shiggins
Friday, August 15, 2003

Sorry about that.  I always mistype it and netster is such a pain.

The command to say a recordset to xml is rs.save StreamObject, adPersistXML.  The format takes a little getting used to, but once your there it is SO useful.

http://www.devguru.com/Technologies/ado/quickref/recordset_save.html

The stream can then be loaded straight into msxml.

Ged Byrne
Friday, August 15, 2003

*  Recent Topics

*  Fog Creek Home