May 5, 2013

Reusable Logic Flow code can be separated from platform depended code implementation

Yesterday I came up with an interesting idea about programming language.
The idea is from a question how I can separate detail code implementation from abstract logic.

For example, Java introduced a powerful concept called Interface. An interface is nothing but a collection of method signature. It cannot contain any implementation or any variables. First time when I saw it, I thought it is useless because it does nothing for me without any implementation. But the important aspect of interface was to separate the abstract layer of programming code from detail implementation. It also allowed us to reuse the abstract layer a lot more and also allowed us to replace old implementation without affecting other code that are relying on the interface.


I didn't extend my idea from interface but later I found the similarity. And it is easier to understand from the idea of Interface. A function is usually mixed with Logic and implementation details. Let me show us a code example first. This code is taken from a random Google search: http://www.programmingsimplified.com/c-program-copy-file

#include "stdio.h"
#include "stdlib.h"
 
int main()
{
   char ch, source_file[20], target_file[20];
   FILE *source, *target;
 
   printf("Enter name of file to copy\n");
   gets(source_file);
 
   source = fopen(source_file, "r");
 
   if( source == NULL )
   {
      printf("Press any key to exit...\n");
      exit(EXIT_FAILURE);
   }
 
   printf("Enter name of target file\n");
   gets(target_file);
 
   target = fopen(target_file, "w");
 
   if( target == NULL )
   {
      fclose(source);
      printf("Press any key to exit...\n");
      exit(EXIT_FAILURE);
   }
 
   while( ( ch = fgetc(source) ) != EOF )
      fputc(ch, target);
 
   printf("File copied successfully.\n");
 
   fclose(source);
   fclose(target);
 
   return 0;
}

Although the program itself is short and easy to understand, the issue is that functional logic flow and implementation details are mixed.This problem may not be obvious for short program like this. But when we want the program to be running on multiple platforms, we have to replace implementation details.

First remedy for multi-platform support is usually "#ifdef PLATFORM_A / #else / #endif".
It gets harder to read and gets longer to read.

The point of my view is that even when the implementation details can vary, the logic flow should stay.

After I apply my idea the code becomes like this:
class ICopyFileLogic
{
public:
 virtual void GetFilenameToCopy();
 virtual void OpenSourceFile();
 virtual bool IsSourceFileOpen();
 virtual void NoticeOpenFailed();
 virtual void GetFilenameToCreate();
 virtual void OpenTargetFileToWrite();
 virtual bool IsTargetFileOpen();
 virtual void NoticeTargetOpenFailed();
 virtual bool GetOneByte();
 virtual void WriteOneByteOnTargetFile();
 virtual void NoticeCopySuccessful();
 virtual void CloseFileHandles();
};
 
int _tmain(int argc, _TCHAR* argv[])
{
 ICopyFileLogic* cf = new PCopyFile_Win32();

 cf->GetFilenameToCopy();
 cf->OpenSourceFile();
 
 if( !cf->IsSourceFileOpen() )
 {
  cf->NoticeOpenFailed();
  exit(EXIT_FAILURE);
 }

 cf->GetFilenameToCreate();
 cf->OpenTargetFileToWrite();
 
 if( !cf->IsTargetFileOpen() )
 {
  cf->NoticeTargetOpenFailed();
  exit(EXIT_FAILURE);
 }
 
 while( cf->GetOneByte()  )
  cf->WriteOneByteOnTargetFile();

 cf->NoticeCopySuccessful();
 cf->CloseFileHandles();
 return 0;
}

class PCopyFile_Win32 : public ICopyFileLogic
{
 char ch, source_file[20], target_file[20];
 FILE *source, *target;

 void GetFilenameToCopy()
 {
  printf("Enter name of file to copy\n");
  gets(source_file);
 }

 void OpenSourceFile()
 {
  source = fopen(source_file, "r");
 }

 bool IsSourceFileOpen()
 {
  return source != NULL;
 }

 void NoticeOpenFailed()
 {
  printf("Press any key to exit...\n");
 }

 void GetFilenameToCreate()
 {
  printf("Enter name of target file\n");
  gets(target_file);
 }

 void OpenTargetFileToWrite()
 {
  target = fopen(target_file, "w");
 }

 bool IsTargetFileOpen()
 {
  return target != NULL;
 }

 void NoticeTargetOpenFailed()
 {
  fclose(source);
  printf("Press any key to exit...\n");
 }

 bool GetOneByte()
 {
  return ( ch = fgetc(source) ) != EOF;
 }

 void WriteOneByteOnTargetFile()
 {
  fputc(ch, target);
 }

 void NoticeCopySuccessful()
 {
  printf("File copied successfully.\n");
 }

 void CloseFileHandles()
 {
  fclose(source);
  fclose(target);
 }
};
  
When the idea was in my head, it was more interesting than after I wrote them down. The new version of the code is longer and also it utilizes stack memory less than before, which I think not preferable. It will also prevent the compiler from optimizing harder.

But the good part is, as I discussed earlier, the logic flow is reusable so that we can replace the platform dependent code implementation without affecting the logic flow. The "main" function doesn't even need to include stdio.h or stdlib.h.

I may need to find a better example than this to convince anybody else of the usefulness of the idea.

2 comments:

Jay said...

The logic function, which was "_tmain" in the example, can be considered as or derived from a state-machine.

If we can have a code generator from state machine to the logic function, the programming process can become:

1. define logic interface.
2. define state machine.
3. implement platform dependent code.

Martin Ecker said...

In C/C++ I would recommend staying away from virtual function calls for platform-dependent code. Platform-dependent code doesn't require runtime polymorphism, so a no-overhead, compile-time solution is preferable.

A good technique is for example described in this post by Charles Bloom: http://cbloomrants.blogspot.com/2013/03/03-16-13-writing-portable-code-rambles.html