Leveraging Template Method Design Pattern In Logger Example

Here rather than explaining what this pattern does, let’s understand this with an example. We will be implementing Logger which is capable of logging in multiple places like, database, file or sending logs in email. We will start with one simple solution and will refactor it gradually to see how template method pattern can be useful for us. 

This pattern falls under behavioral design patterns, as the name suggests it defines template which can be used further to create something by using it. You can think of  it like stencils, you can create designs on the wall or other surfaces without much effort, you just need to choose color and apply pigment.

(P.S: Logger is taken only as an example to show use of Template Method Design Pattern. It's not the best solution for an actual Logger)

Examples are written in C#, but easily understandable for anyone who knows basic OOPS concept.

Approach 1 - Create different classes for each type of Logger

We have 3 classes for each type of logger; i.e., FileLogger, EmailLogger & DatabaseLogger. All have implemented their own logic.

  1. public class FileLogger  
  2. {  
  3.     public void Log(object message) {  
  4.         string messageToLog = SerializeMessage(message);  
  5.         OpenFile();  
  6.         WriteLogMessage(messageToLog);  
  7.         CloseFile();  
  8.     }  
  9.   
  10.     private string SerializeMessage(object message) {  
  11.         WriteLine("Serializing message");  
  12.         return message.ToString();  
  13.     }  
  14.   
  15.     private void OpenFile() {  
  16.         WriteLine("Opening File.");  
  17.     }  
  18.   
  19.     private void WriteLogMessage(string message) {  
  20.         WriteLine("Appending Log message to file : " + message);  
  21.     }  
  22.   
  23.     private void CloseFile() {  
  24.         WriteLine("Close File.");  
  25.     }  
  26. }  
  27.   
  28. public class EmailLogger  
  29. {  
  30.     public void Log(object message) {  
  31.         string messageToLog = SerializeMessage(message);  
  32.         ConnectToMailServer();  
  33.         SendLogToEmail(messageToLog);  
  34.         DisposeConnection();  
  35.     }  
  36.   
  37.     private string SerializeMessage(object message) {  
  38.         WriteLine("Serializing message");  
  39.         return message.ToString();  
  40.     }  
  41.   
  42.     private void ConnectToMailServer() {  
  43.         WriteLine("Connecting to mail server and logging in");  
  44.     }  
  45.   
  46.     private void SendLogToEmail(string message) {  
  47.         WriteLine("Sending Email with Log Message : " + message);  
  48.     }  
  49.   
  50.     private void DisposeConnection() {  
  51.         WriteLine("Dispose Connection");  
  52.     }  
  53. }  
  54.   
  55. public class DatabaseLogger  
  56. {  
  57.     public void Log(string message) {  
  58.         string messageToLog = SerializeMessage(message);  
  59.         ConnectToDatabase();  
  60.         InsertLogMessageToTable(messageToLog);  
  61.         CloseDbConnection();  
  62.     }  
  63.   
  64.     private string SerializeMessage(object message) {  
  65.         WriteLine("Serializing message");  
  66.         return message.ToString();  
  67.     }  
  68.   
  69.     private void ConnectToDatabase() {  
  70.         WriteLine("Connecting to Database.");  
  71.     }  
  72.   
  73.     private void InsertLogMessageToTable(string message) {  
  74.         WriteLine("Inserting Log Message to DB table : " + message);  
  75.     }  
  76.   
  77.     private void CloseDbConnection() {  
  78.         WriteLine("Closing DB connection.");  
  79.     }  
  80. }  
  81.   
  82. class MainClass  
  83. {  
  84.     static void Main(string[] args) {  
  85.         FileLogger fileLogger = new FileLogger();  
  86.         fileLogger.Log("Message to Log in File.");  
  87.         WriteLine();  
  88.         EmailLogger emailLogger = new EmailLogger();  
  89.         emailLogger.Log("Message to Log via Email.");  
  90.         WriteLine();  
  91.         DatabaseLogger databaseLogger = new DatabaseLogger();  
  92.         databaseLogger.Log("Message to Log in DB.");  
  93.     }  
  94. }  

Reviewing approach 1

No Code reusability, SerializeMessage() has the same implementation in each class.

Approach 2 - Move duplicate code to a common place

We have created class AbstractLogger, and moved common code (i.e. SerializeMessage here) here. All class requiring this code will be inheriting this class now to reuse code.

  1. public abstract class AbstractLogger  
  2. {  
  3.     protected string SerializeMessage(object message) {  
  4.         WriteLine("Serializing message");  
  5.         return message.ToString();  
  6.     }  
  7. }  
  8.   
  9. public class FileLogger : AbstractLogger  
  10. {  
  11.     public void Log(object message) {  
  12.         string messageToLog = SerializeMessage(message);  
  13.         OpenFile();  
  14.         WriteLogMessage(messageToLog);  
  15.         CloseFile();  
  16.     }  
  17.   
  18.     private void OpenFile() {  
  19.         WriteLine("Opening File.");  
  20.     }  
  21.   
  22.     private void WriteLogMessage(string message) {  
  23.         WriteLine("Appending Log message to file : " + message);  
  24.     }  
  25.   
  26.     private void CloseFile() {  
  27.         WriteLine("Close File.");  
  28.     }  
  29. }  
  30.   
  31. public class EmailLogger : AbstractLogger  
  32. {  
  33.     public void Log(object message) {  
  34.         string messageToLog = SerializeMessage(message);  
  35.         ConnectToMailServer();  
  36.         SendLogToEmail(messageToLog);  
  37.         DisposeConnection();  
  38.     }  
  39.   
  40.     private void ConnectToMailServer() {  
  41.         WriteLine("Connecting to mail server and logging in");  
  42.     }  
  43.   
  44.     private void SendLogToEmail(string message) {  
  45.         WriteLine("Sending Email with Log Message : " + message);  
  46.     }  
  47.   
  48.     private void DisposeConnection() {  
  49.         WriteLine("Dispose Connection");  
  50.     }  
  51. }  
  52.   
  53. public class DatabaseLogger : AbstractLogger  
  54. {  
  55.     public void Log(string message) {  
  56.         string messageToLog = SerializeMessage(message);  
  57.         ConnectToDatabase();  
  58.         InsertLogMessageToTable(messageToLog);  
  59.         CloseDbConnection();  
  60.     }  
  61.   
  62.     private void ConnectToDatabase() {  
  63.         WriteLine("Connecting to Database.");  
  64.     }  
  65.   
  66.     private void InsertLogMessageToTable(string message) {  
  67.         WriteLine("Inserting Log Message to DB table : " + message);  
  68.     }  
  69.   
  70.     private void CloseDbConnection() {  
  71.         WriteLine("Closing DB connection.");  
  72.     }  
  73. }  
  74.   
  75. class MainClass  
  76. {  
  77.     static void Main(string[] args) {  
  78.         FileLogger fileLogger = new FileLogger();  
  79.         fileLogger.Log("Message to Log in File.");  
  80.         WriteLine();  
  81.         EmailLogger emailLogger = new EmailLogger();  
  82.         emailLogger.Log("Message to Log via Email.");  
  83.         WriteLine();  
  84.         DatabaseLogger databaseLogger = new DatabaseLogger();  
  85.         databaseLogger.Log("Message to Log in DB.");  
  86.     }  
  87. }   
Reviewing approach 2

All loggers are having 3 kinds of operations now, Opening Connection/File, Writing log message & Closing/destroying File/object/connection. So we can assume a typical Logger will always use these kind of operations, but still a person who is implementing some new Logger in  the future has to remember and implement those operations. Shouldn’t that be enforced?

Log() is doing nothing fancy, just calling all other methods in sequence, isn’t it?

Approach 3 - Force user to implement required step, and move responsiblity to call them in base class

We have added abstract methods in abstract class with generalized name i.e OpenDataStoreOperation(), LogMessage() & CloseDataStoreOpreation() which are representing 3 operations mentioned above in Approach 2. So all the loggers have to implement them.

They are adding one more advantage, that we can also move Log() to Abstract class, becuase all the methods which are being called in sequence in child classes are available in parent one.

Both problems of above approach are solved in this approach, this is how we implement Template Method Design Pattern. Any class inheriting AbstractLogger class just has to implement a few methods, and they will already get some concrete methods like SerializeMessage() In this case we can even provide optional implementation by using virtual keyword in concrete methods.

  1. public abstract class AbstractLogger  
  2. {  
  3.     protected string SerializeMessage(object message) {  
  4.         WriteLine("Serializing message");  
  5.         return message.ToString();  
  6.     }  
  7.   
  8.     protected abstract void OpenDataStoreOperation();  
  9.   
  10.     protected abstract void LogMessage(string messageToLog);  
  11.   
  12.     protected abstract void CloseDataStoreOpreation();  
  13.   
  14.     public void Log(object message) {  
  15.         string messageToLog = SerializeMessage(message);  
  16.         OpenDataStoreOperation();  
  17.         LogMessage(messageToLog);  
  18.         CloseDataStoreOpreation();  
  19.     }  
  20. }  
  21.   
  22. public class FileLogger : AbstractLogger  
  23. {  
  24.     protected override void OpenDataStoreOperation() {  
  25.         WriteLine("Opening File.");  
  26.     }  
  27.   
  28.     protected override void LogMessage(string messageToLog) {  
  29.         WriteLine("Appending Log message to file : " + messageToLog);  
  30.     }  
  31.   
  32.     protected override void CloseDataStoreOpreation() {  
  33.         WriteLine("Close File.");  
  34.     }  
  35. }  
  36.   
  37. public class EmailLogger : AbstractLogger  
  38. {  
  39.     protected override void OpenDataStoreOperation() {  
  40.         WriteLine("Connecting to mail server and logging in");  
  41.     }  
  42.   
  43.     protected override void LogMessage(string messageToLog) {  
  44.         WriteLine("Sending Email with Log Message : " + messageToLog);  
  45.     }  
  46.   
  47.     protected override void CloseDataStoreOpreation() {  
  48.         WriteLine("Dispose Connection");  
  49.     }  
  50. }  
  51.   
  52. public class DatabaseLogger : AbstractLogger  
  53. {  
  54.     protected override void OpenDataStoreOperation() {  
  55.         WriteLine("Connecting to Database.");  
  56.     }  
  57.   
  58.     protected override void LogMessage(string messageToLog) {  
  59.         WriteLine("Inserting Log Message to DB table : " + messageToLog);  
  60.     }  
  61.   
  62.     protected override void CloseDataStoreOpreation() {  
  63.         WriteLine("Closing DB connection.");  
  64.     }  
  65. }  
  66.   
  67. class MainClass  
  68. {  
  69.     static void Main(string[] args) {  
  70.         FileLogger fileLogger = new FileLogger();  
  71.         fileLogger.Log("Message to Log in File.");  
  72.         WriteLine();  
  73.         EmailLogger emailLogger = new EmailLogger();  
  74.         emailLogger.Log("Message to Log via Email.");  
  75.         WriteLine();  
  76.         DatabaseLogger databaseLogger = new DatabaseLogger();  
  77.         databaseLogger.Log("Message to Log in DB.");  
  78.     }  
  79. }   
Reviewing approach 3

Here all steps of our algo/program will be executed for sure, but I have some optional steps which I wish to let user choose whether to call or not.

Approach 4 - Let the Caller decide some of the things

Suppose in our example along with logging to data store, I optionally want to let user choose whether to log in console or not. To achieve this I have added on Boolean property in base class ConsoleLogging and one virtual method LogToConsole(). In Log() I have added condition based on ConsoleLogging value whether to execute LogToConsole() or not(see code). Now if user wants to log in to the console also he just needs to pass true in ConsoleLogging property(see in Main() EmailLogger).

  1. public abstract class AbstractLogger  
  2. {  
  3.     public bool ConsoleLogging { getset; }  
  4.   
  5.     protected string SerializeMessage(object message) {  
  6.         WriteLine("Serializing message");  
  7.         return message.ToString();  
  8.     }  
  9.   
  10.     protected abstract void OpenDataStoreOperation();  
  11.   
  12.     protected abstract void LogMessage(string messageToLog);  
  13.   
  14.     protected abstract void CloseDataStoreOpreation();  
  15.   
  16.     protected virtual void LogToConsole(string messageToLog) {  
  17.         WriteLine("Writing in Console : " + messageToLog);  
  18.     }  
  19.   
  20.     public void Log(object message) {  
  21.         string messageToLog = SerializeMessage(message);  
  22.         OpenDataStoreOperation();  
  23.         LogMessage(messageToLog);  
  24.         CloseDataStoreOpreation();  
  25.         if (ConsoleLogging) {  
  26.             LogToConsole(messageToLog);  
  27.         }  
  28.     }  
  29. }  
  30.   
  31. public class FileLogger : AbstractLogger  
  32. {  
  33.     protected override void OpenDataStoreOperation() {  
  34.         WriteLine("Opening File.");  
  35.     }  
  36.   
  37.     protected override void LogMessage(string messageToLog) {  
  38.         WriteLine("Appending Log message to file : " + messageToLog);  
  39.     }  
  40.   
  41.     protected override void CloseDataStoreOpreation() {  
  42.         WriteLine("Close File.");  
  43.     }  
  44. }  
  45.   
  46. public class EmailLogger : AbstractLogger  
  47. {  
  48.     protected override void OpenDataStoreOperation() {  
  49.         WriteLine("Connecting to mail server and logging in");  
  50.     }  
  51.   
  52.     protected override void LogMessage(string messageToLog) {  
  53.         WriteLine("Sending Email with Log Message : " + messageToLog);  
  54.     }  
  55.   
  56.     protected override void CloseDataStoreOpreation() {  
  57.         WriteLine("Dispose Connection");  
  58.     }  
  59. }  
  60.   
  61. public class DatabaseLogger : AbstractLogger  
  62. {  
  63.     protected override void OpenDataStoreOperation() {  
  64.         WriteLine("Connecting to Database.");  
  65.     }  
  66.   
  67.     protected override void LogMessage(string messageToLog) {  
  68.         WriteLine("Inserting Log Message to DB table : " + messageToLog);  
  69.     }  
  70.   
  71.     protected override void CloseDataStoreOpreation() {  
  72.         WriteLine("Closing DB connection.");  
  73.     }  
  74. }  
  75.   
  76. class MainClass  
  77. {  
  78.     static void Main(string[] args) {  
  79.         FileLogger fileLogger = new FileLogger();  
  80.         fileLogger.Log("Message to Log in File.");  
  81.         WriteLine();  
  82.         EmailLogger emailLogger = new EmailLogger();  
  83.         emailLogger.ConsoleLogging = true;  
  84.         emailLogger.Log("Message to Log via Email.");  
  85.         WriteLine();  
  86.         DatabaseLogger databaseLogger = new DatabaseLogger();  
  87.         databaseLogger.Log("Message to Log in DB.");  
  88.     }  
  89. }   

Conclusion

You can see in approach 4 code looks better, whenever you see your code/algo is having same steps with minor configurable changes like the given example, Template method can be useful.

Complete Source Code Visual Studio Solution

Template Method Pattern / Logger