Separating the code from domain logic and mutable shell

Sometimes, when our code processes a business transaction, it mutates some data several times. In the world of object-oriented programming languages, this is quite a common pattern. We can then separate our code into domain logic and the mutable shell. In domain logic, we simplify the code and write the business logic in a functional way using mathematical functions. As a result, this domain logic will become easy to test. In the mutable shell, we place a mutable expression; we will do this after we finish with the business logic.

Examining the code containing side-effects

Now, let's examine the following code, which contains many side-effects that we are going to refactor, and we can find it in the DomainLogicAndMutatingState.csproj project:

public class Librarianship 
{ 
  private readonly int _maxEntriesPerFile; 
  public Librarianship( 
    int maxEntriesPerFile) 
  { 
    _maxEntriesPerFile = 
    maxEntriesPerFile; 
  } 
 
  public void AddRecord( 
    string currentFile, 
    string visitorName, 
    string bookTitle, 
    DateTime returnDate) 
  { 
     // The rest of code can be found  
     // in the downloaded source code  
  } 
 
  private string GetNewFileName( 
        string existingFileName) 
  { 
    // The rest of code can be found  
    // in the downloaded source code  
  } 
 
  public void RemoveRecord( 
      string visitorName,  
      string directoryName) 
  { 
    foreach (string fileName in Directory.GetFiles( 
            directoryName)) 
    { 
      // The rest of code can be found  
      // in the downloaded source code  
    } 
  } 
} 

As you can see in the preceding code, it is written in a straightforward way. We are going to separate its responsibilities into two parts: an immutable core that contains all the domain logic and a mutable shell that contains all the mutable expression.

The Librarianship class will keep track of all the borrowers in a library and takes note of the book-returning date. The class uses a log file to store the borrower's name, the title of the borrowed book, and the returning date. The pattern of the log file content is the index number, a semicolon, the borrower name and then the semicolon again, the book title and then the semicolon, and lastly, the returning date. The following is a sample of the log file content:

1;Arthur Jackson;Responsive Web Design;9/26/2016 
2;Maddox Webb;AngularJS by Example;9/27/2016 
3;Mel Fry;Python Machine Learning;9/28/2016 
4;Haiden Brown;Practical Data Science Cookbook;9/29/2016 
5;Sofia Hamilton;DevOps Automation Cookbook;9/30/2016 

The class must be able to add a new line in the log file, such as what we can see in the AddRecord() method. But before we invoke the method, we have to specify the value for the _maxEntriesPerFile field when we construct the class.

The value of the _maxEntriesPerFile field will be used when we invoke the AddRecord() method. If _maxEntriesPerFile is greater than the current total lines of the log file, it will insert the visitor identity into the log file using the following code:

if (lines.Length < _maxEntriesPerFile) 
{ 
  int lastIndex = int.Parse( 
    lines.Last() 
    .Split(';')[0]); 
 
  string newLine = 
    String.Format( 
    "{0};{1};{2};{3}", 
    (lastIndex + 1), 
    visitorName, 
    bookTitle, 
    returnDate 
    .ToString("d") 
  ); 
 
  File.AppendAllLines( 
    currentFile, 
    new[] { 
    newLine }); 
} 

Otherwise, if the current total number of lines of the log file has reached _maxEntriesPerFile, then AddRecord() method creates a new log file, as shown in the following code:

else 
{ 
  string newLine = 
    String.Format( 
    "1;{0};{1};{2}", 
    visitorName, 
    bookTitle, 
    returnDate 
    .ToString("d") 
    ); 
  string newFileName = 
    GetNewFileName( 
    currentFile); 
  File.WriteAllLines( 
    newFileName, 
    new[] { 
    newLine }); 
  currentFile = newFileName; 
} 

From the preceding code snippet, we find the GetNewFileName() method to generate a new log file name based on the current log file name. The implementation of the GetNewFileName() method is as follows:

private string GetNewFileName( 
  string existingFileName) 
{ 
  string fileName =  
    Path.GetFileNameWithoutExtension( 
      existingFileName); 
  int index = int.Parse( 
    fileName 
    .Split('_')[1]); 
 
  return String.Format( 
    "LibraryLog_{0:D4}.txt", 
    index + 1); 
} 

From the preceding GetNewFileName() method's implementation, we can see that the pattern of the log file name is LibraryLog _0001.txt, LibraryLog _0002.txt, and so on.

The AddRecord() method will also create a new log file if the specified log file name is not found. The implementation of this task is as follows:

if (!File.Exists(currentFile)) 
{ 
  string newLine = 
    String.Format( 
    "1;{0};{1};{2}", 
    visitorName, 
    bookTitle, 
    returnDate 
    .ToString("d") 
    ); 
 
  File.WriteAllLines( 
    currentFile, 
    new[] { 
    newLine }); 
} 

The class also has the RemoveRecord() method to remove the visitor identity from the log file. The implementation of the method is as follows:

public void RemoveRecord( 
    string visitorName,  
    string directoryName) 
{ 
    foreach (string fileName in Directory.GetFiles( 
        directoryName)) 
    { 
        string tempFile = Path.GetTempFileName(); 
        List<string> linesToKeep = File 
            .ReadLines(fileName) 
            .Where(line => !line.Contains(visitorName)) 
            .ToList(); 
 
        if (linesToKeep.Count == 0) 
        { 
            File.Delete( 
                fileName); 
        } 
        else 
        { 
            File.WriteAllLines( 
                tempFile,  
                linesToKeep); 
 
            File.Delete( 
                fileName); 
 
            File.Move( 
                tempFile,  
                fileName); 
        } 
    } 
} 

In the RemoveRecord() method's implementation, you can see that it removes the selected visitor from the available log file in the selected directory, as shown in the following code snippet:

List<string> linesToKeep = File
    .ReadLines(fileName)
    .Where(line => !line.Contains(visitorName))
    .ToList();

If linesToKeep contains no data, we can securely delete the file using the following code:

if (linesToKeep.Count == 0)
{
    File.Delete(
        fileName);
}

Otherwise, we just need to remove the visitor identity from the log file using the following code:

else
{
    File.WriteAllLines(
        tempFile, 
        linesToKeep);
    File.Delete(
        fileName);
    File.Move(
        tempFile, 
        fileName);
}

Now it's time to try our Librarianship class. First, we will prepare a data list that contains the author and the title of the books, as shown in the following code:

public partial class Program 
{ 
    public static List<Book> bookList = 
        new List<Book>() 
        { 
            new Book( 
                "Arthur Jackson", 
                "Responsive Web Design"), 
            new Book( 
                "Maddox Webb", 
                "AngularJS by Example"), 
            new Book( 
                "Mel Fry", 
                "Python Machine Learning"), 
            new Book( 
                "Haiden Brown", 
                "Practical Data Science Cookbook"), 
            new Book( 
                "Sofia Hamilton", 
                "DevOps Automation Cookbook") 
        }; 
} 

And we have the Book structure as follows:

public struct Book 
{ 
    public string Borrower { get; } 
    public string Title { get; } 
 
    public Book( 
        string borrower, 
        string title) 
    { 
        Borrower = borrower; 
        Title = title; 
    } 
} 

We will invoke the following LibrarianshipInvocation() method to consume the Librarianship class:

public partial class Program 
{ 
    public static void LibrarianshipInvocation() 
    { 
        Librarianship librarian =  
            new Librarianship(5); 
 
        for (int i = 0; i < bookList.Count; i++) 
        { 
            librarian.AddRecord( 
                GetLastLogFile( 
                    AppDomain.CurrentDomain.BaseDirectory), 
                bookList[i].Borrower, 
                bookList[i].Title, 
                DateTime.Now.AddDays(i)); 
        } 
    } 
} 

As you can see in the preceding LibrarianshipInvocation() method, we call the GetLastLogFile() method to find the last available log file. The implementation of the method is as follows:

public partial class Program 
{ 
    public static string GetLastLogFile( 
        string LogDirectory) 
    { 
        string[] logFiles = Directory.GetFiles( 
            LogDirectory,  
            "LibraryLog_????.txt"); 
 
        if (logFiles.Length > 0) 
        { 
            return logFiles[logFiles.Length - 1]; 
        } 
        else 
        { 
            return "LibraryLog_0001.txt"; 
        } 
    } 
} 

When we call the GetLastLogFile() method, it will look for all files that have the LibraryLog_????.txt pattern in the directory we specified. It will then return the last member of the string array. If the string array contains no data, it will return LibraryLog_0001.txt as the default log file name.

If we run the LibrarianshipInvocation() method, we will see nothing, but we will get a new LibraryLog_0001.txt file containing the following text:

Examining the code containing side-effects

From the preceding output file log, we can see that we have successfully created the Librarianship class as expected.

Refactoring the AddRecord() method

Now it's time to refactor the Librarianship class for it to become immutable. First, we will make the AddRecord() method a mathematical function. To do that, we have to make sure that it doesn't access the disk directly, which we do when we use the File.Exists(), File.ReadAllLines(), File.AppendAllLines(), and File.WriteAllLines() methods. We will refactor the AddRecord() method as follows:

public FileAction AddRecord( 
    FileContent currentFile,  
    string visitorName, 
    string bookTitle, 
    DateTime returningDate) 
{ 
    List<DataEntry> entries = Parse(currentFile.Content); 
 
    if (entries.Count < _maxEntriesPerFile) 
    { 
        entries.Add( 
            new DataEntry( 
                entries.Count + 1,  
                visitorName,  
                bookTitle,  
                returningDate)); 
 
        string[] newContent =  
            Serialize( 
                entries); 
 
        return new FileAction( 
            currentFile.FileName,  
            ActionType.Update,  
            newContent); 
    } 
    else 
    { 
        var entry = new DataEntry( 
            1, 
            visitorName, 
            bookTitle, 
            returningDate); 
 
        string[] newContent =  
            Serialize( 
                new List<DataEntry> { entry }); 
 
        string newFileName =  
            GetNewFileName( 
                currentFile.FileName); 
 
        return new FileAction( 
            newFileName,  
            ActionType.Create,  
            newContent); 
    } 
} 

As you can see in the preceding code, we modify the AddRecord() method signature so that it doesn't pass any filenames now and passes a FileContent data type instead, which is structured with the following implementation:

public struct FileContent 
{ 
    public readonly string FileName; 
    public readonly string[] Content; 
 
    public FileContent( 
        string fileName, 
        string[] content) 
    { 
        FileName = fileName; 
        Content = content; 
    } 
} 

As you can see, the FileContent structure will now handle the filename and its content. And the AddRecord() method also returns the FileAction data type now. The implementation of the FileAction data type is as follows:

public struct FileAction 
{ 
    public readonly string FileName; 
    public readonly string[] Content; 
    public readonly ActionType Type; 
 
    public FileAction( 
        string fileName,  
        ActionType type,  
        string[] content) 
    { 
        FileName = fileName; 
        Type = type; 
        Content = content; 
    } 
} 

And the ActionType enumeration is as follows:

public enum ActionType 
{ 
    Create, 
    Update, 
    Delete 
} 

We also have a new data type, which is DataEntry. The implementation of the DataEntry structure is as follows:

public struct DataEntry 
{ 
    public readonly int Number; 
    public readonly string Visitor; 
    public readonly string BookTitle; 
    public readonly DateTime ReturningDate; 
 
    public DataEntry( 
        int number,  
        string visitor, 
        string bookTitle, 
        DateTime returningDate) 
    { 
        Number = number; 
        Visitor = visitor; 
        BookTitle = bookTitle; 
        ReturningDate = returningDate; 
    } 
} 

The DataEntry structure will handle all the data that we want to write in the log file. And if we examine the AddRecord() method again, we don't find the procedure to make sure the log file exists since that will be done in a separate process.

We notice that the AddRecord() method invokes two new methods: the Parse() and Serialize() methods. The Parse() method is used to parse all the lines in the log file content and then form the list of DataEntry based on the content of the log file. The implementation of the method is as follows:

private List<DataEntry> Parse( 
    string[] content) 
{ 
    var result = new List<DataEntry>(); 
 
    foreach (string line in content) 
    { 
        string[] data = line.Split(';'); 
        result.Add( 
            new DataEntry( 
                int.Parse(data[0]),  
                data[1],  
                data[2], 
                DateTime.Parse(data[3]))); 
    } 
 
    return result; 
} 

On the other hand, the Serialize() method is used to serialize the DataEntry list into the string array. The implementation of the method is as follows:

private string[] Serialize( 
    List<DataEntry> entries) 
{ 
    return entries 
        .Select(entry =>  
            String.Format( 
                "{0};{1};{2};{3}", 
                entry.Number, 
                entry.Visitor, 
                entry.BookTitle, 
                entry.ReturningDate 
                    .ToString("d"))) 
        .ToArray(); 
} 

Refactoring the RemoveRecord() method

Now, we go back to our Librarianship class and refactor the RemoveRecord() method. The implementation will be as follows:

public IReadOnlyList<FileAction> RemoveRecord( 
  string visitorName, 
  FileContent[] directoryFiles) 
{ 
  return directoryFiles 
    .Select(file => 
    RemoveRecordIn( 
      file, 
      visitorName)) 
  .Where(action => 
  action != null) 
  .Select(action => 
  action.Value) 
  .ToList(); 
} 

The RemoveRecord() method now has a new signature. It passes an array of FileContent instead of the directory name only. It also returns a read-only list of FileAction. The RemoveRecord() method also needs an additional RemoveRecordIn() method, which is used to get the specified filename and file content to target the record that will be removed. The implementation of the RemoveRecordIn() method is as follows:

private FileAction? RemoveRecordIn( 
    FileContent file,  
    string visitorName) 
{ 
    List<DataEntry> entries = Parse( 
        file.Content); 
    List<DataEntry> newContent = entries 
        .Where(x =>  
            x.Visitor != visitorName) 
        .Select((entry, index) =>  
            new DataEntry( 
                index + 1,  
                entry.Visitor,  
                entry.BookTitle, 
                entry.ReturningDate)) 
        .ToList(); 
    if (newContent.Count == entries.Count) 
        return null; 
    if (newContent.Count == 0) 
    { 
        return new FileAction( 
            file.FileName, 
            ActionType.Delete, 
            new string[0]); 
    } 
    else 
    { 
        return new FileAction( 
            file.FileName,  
            ActionType.Update,  
            Serialize( 
                newContent)); 
    } 
} 

And now, we have the domain logic code, which is totally immutable and can run this domain logic in a unit testing environment.

Running domain logic in unit testing

Domain logic is an immutable source that is a pure function, so we can run the unit testing over and over again without having to change the testing rules. Here, we are going to test the AddRecord() and RemoveRecord() methods in the LibrarianshipImmutable class. We will have five tests for these two methods. For the AddRecord() method, we will test if the file is overflow. For the RemoveRecord() method, we will test whether the selected record that we want to remove is available. Then, the file becomes empty if the selected record is empty or if the selected record is not available.

Testing the AddRecord() method

Now let's take a look at the following AddRecord_LinesIsLowerThanMaxEntriesPerFileTest() test method, which will add a record to the existing log file:

[TestMethod] 
// Add record to existing log file  
// but the lines is lower then maxEntriesPerFile  
public void AddRecord_LinesIsLowerThanMaxEntriesPerFileTest() 
{ 
    LibrarianshipImmutable librarian =  
        new LibrarianshipImmutable(5); 
 
    FileContent file = new FileContent( 
        "LibraryLog_0001.txt",  
        new[]{ 
            "1;Arthur Jackson;Responsive Web Design;9/26/2016" 
        }); 
 
    FileAction action = librarian.AddRecord( 
        file, 
        "Maddox Webb", 
        "AngularJS by Example", 
        new DateTime( 
            2016, 9, 27, 0, 0, 0)); 
 
    Assert.AreEqual( 
        ActionType.Update,  
        action.Type); 
    Assert.AreEqual( 
        "LibraryLog_0001.txt",  
        action.FileName); 
    CollectionAssert.AreEqual( 
        new[]{ 
            "1;Arthur Jackson;Responsive Web Design;9/26/2016", 
            "2;Maddox Webb;AngularJS by Example;9/27/2016" 
        }, 
        action.Content); 
} 

In the AddRecord_LinesIsLowerThanMaxEntriesPerFileTest() test method, first, we create a LibraryLog_0001.txt file containing 1;Arthur Jackson;Responsive Web Design;9/26/2016 and then we add a new record, as shown in the following code:

FileAction action = librarian.AddRecord( 
  file, 
  "Maddox Webb", 
  "AngularJS by Example", 
  new DateTime( 
    2016, 9, 27, 0, 0, 0)); 

From now on, we have to ensure that action.The type has to be ActionType.Update, action.FileName has to be LibraryLog_0001.txt, and the action.Content has to be two lines, with the first line as 1;Arthur Jackson;Responsive Web Design;9/26/2016 and the second line as 2;Maddox Webb;AngularJS by Example;9/27/2016.

Tip

The Assert.AreEqual() method is used to verify that the specified values are equal. Unfortunately, the use of this method will not override the array data. To compare the array, we need to use the CollectionAssert.AreEqual() method, which will verify that two specified collections are equal.

Another unit testing is the AddRecord_LinesHasReachMaxEntriesPerFileTest() testing method. The implementation of this testing is as follows:

[TestMethod] 
// Add record to a new log file  
// becausecurrent log file has reached maxEntriesPerFile  
public void AddRecord_LinesHasReachMaxEntriesPerFileTest() 
{ 
    LibrarianshipImmutable librarian =  
        new LibrarianshipImmutable(3); 
 
    FileContent file = new FileContent( 
        "LibraryLog_0001.txt",  
        new[]{ 
            "1;Arthur Jackson;Responsive Web Design;9/26/2016", 
            "2;Maddox Webb;AngularJS by Example;9/27/2016", 
            "3;Mel Fry;Python Machine Learning;9/28/2016" 
        }); 
 
    FileAction action = librarian.AddRecord( 
        file, 
        "Haiden Brown", 
        "Practical Data Science", 
        new DateTime(2016, 9, 29, 0, 0, 0)); 
 
    Assert.AreEqual( 
        ActionType.Create,  
        action.Type); 
    Assert.AreEqual( 
        "LibraryLog_0002.txt",  
        action.FileName); 
    CollectionAssert.AreEqual( 
        new[]{ 
            "1;Haiden Brown;Practical Data Science;9/29/2016" 
        },  
        action.Content); 
} 

In this testing method, we want to ensure that a new log file is created if the current log file lines have reached maxEntriesPerFile. First, we instantiate LibrarianshipImmutable and fill the maxEntriesPerFile field with 3 and then we fill the log file with the three visitors, as shown in the following code:

LibrarianshipImmutable librarian =  
  new LibrarianshipImmutable(3); 
FileContent file = new FileContent( 
  "LibraryLog_0001.txt", 
  new[]{ 
    "1;Arthur Jackson;Responsive Web Design;9/26/2016", 
    "2;Maddox Webb;AngularJS by Example;9/27/2016", 
    "3;Mel Fry;Python Machine Learning;9/28/2016" 
  }); 

After that, we add a new record using the following code:

FileAction action = librarian.AddRecord( 
  file, 
  "Haiden Brown", 
  "Practical Data Science", 
  new DateTime(2016, 9, 29, 0, 0, 0)); 

Now, we have to ensure that action.Type is ActionType.Update, and it creates a new log file named LibraryLog_0002.txt. Also, the content of the new log file is 1;Haiden Brown;Practical Data Science;9/29/2016.

Testing the RemoveRecord() method

As we discussed earlier, we have three tests for the RemoveRecord() method. First, we are going to test removing a record from the files in the directory. The code will be as follows:

[TestMethod] 
// Remove selected record from files in the directory 
public void RemoveRecord_FilesIsAvailableInDirectoryTest() 
{ 
    LibrarianshipImmutable librarian =  
        new LibrarianshipImmutable(10); 
 
    FileContent file = new FileContent( 
        "LibraryLog_0001.txt",  
        new[] 
        { 
            "1;Arthur Jackson;Responsive Web Design;9/26/2016", 
            "2;Maddox Webb;AngularJS by Example;9/27/2016", 
            "3;Mel Fry;Python Machine Learning;9/28/2016" 
        }); 
 
    IReadOnlyList<FileAction> actions =  
        librarian.RemoveRecord( 
            "Arthur Jackson",  
            new[] { 
                file }); 
 
    Assert.AreEqual( 
        1,  
        actions.Count); 
 
    Assert.AreEqual( 
        "LibraryLog_0001.txt",  
        actions[0].FileName); 
 
    Assert.AreEqual( 
        ActionType.Update,  
        actions[0].Type); 
 
    CollectionAssert.AreEqual( 
        new[]{ 
            "1;Maddox Webb;AngularJS by Example;9/27/2016", 
            "2;Mel Fry;Python Machine Learning;9/28/2016" 
        },  
        actions[0].Content); 
} 

In this RemoveRecord_FilesIsAvailableInDirectoryTest() test method, we first create a LibraryLog_0001.txt file containing three records. We then remove the first record and make sure that LibraryLog_0001.txt will contain only two remaining logs with the proper order number.

The other test is RemoveRecord_FileBecomeEmptyTest() with the following implementation:

[TestMethod] 
// Remove selected record from files in the directory 
// If file becomes empty, it will be deleted 
public void RemoveRecord_FileBecomeEmptyTest() 
{ 
 
    LibrarianshipImmutable librarian = 
        new LibrarianshipImmutable(10); 
 
    FileContent file = new FileContent( 
        "LibraryLog_0001.txt", 
        new[] 
        { 
            "1;Arthur Jackson;Responsive Web Design;9/26/2016" 
        }); 
 
    IReadOnlyList<FileAction> actions = 
        librarian.RemoveRecord( 
            "Arthur Jackson",  
            new[] { 
                file }); 
 
    Assert.AreEqual( 
        1,  
        actions.Count); 
 
    Assert.AreEqual( 
        "LibraryLog_0001.txt",  
        actions[0].FileName); 
 
    Assert.AreEqual( 
        ActionType.Delete,  
        actions[0].Type); 
} 

The RemoveRecord_FileBecomeEmptyTest() testing method will make sure that the log file is deleted if it is empty after the record is removed. First, we create a new log file with one record, and then we remove it using the RemoveRecord() method.

The last test for the RemoveRecord() method is RemoveRecord_SelectedRecordIsUnavailableTest(), which will remove nothing if the selected record is unavailable. The implementation of the testing method is as follows:

[TestMethod] 
// Remove nothing if selected record is unavailable 
public void RemoveRecord_SelectedRecordIsUnavailableTest() 
{ 
    LibrarianshipImmutable librarian = 
        new LibrarianshipImmutable(10); 
 
    FileContent file = new FileContent( 
        "LibraryLog_0001.txt", 
        new[] 
        { 
            "1;Sofia Hamilton;DevOps Automation;9/30/2016" 
        }); 
 
    IReadOnlyList<FileAction> actions = 
        librarian.RemoveRecord( 
            "Arthur Jackson", 
            new[] { 
                file }); 
 
    Assert.AreEqual( 
        0,  
        actions.Count); 
} 

As you can see, we create the log file containing Sofia Hamilton as the visitor name, but we try to remove the visitor named Arthur Jackson. In this case, the RemoveRecord() method will remove nothing.

Executing the test

Now, it's time to run the unit testing for all five testing methods. And here is what we will get after we run the test:

Executing the test

Adding the mutable shell into code

So far, we have successfully created the immutable core and covered unit tests. For now, we are ready to a implement the mutable shell for the rest of the code that is accessing the disk. We will create two classes, FileProcessor and AppService. The FileProcessor class will do all the disk interaction. The AppService class will be a bridge between the LibrarianshipImmutable class and the FileProcessor class.

Now, let's take a look at the following FileProcessor class implementation, which we can find in the FileProcessor.cs file:

namespace DomainLogicAndMutatingState 
{ 
    public class FileProcessor 
    { 
        public FileContent ReadFile( 
            string fileName) 
        { 
            return new FileContent( 
                fileName,  
                File.ReadAllLines( 
                    fileName)); 
        } 
 
        public FileContent[] ReadDirectory( 
            string directoryName) 
        { 
            return Directory 
                .GetFiles( 
                    directoryName) 
                .Select(x =>  
                    ReadFile(x)) 
                .ToArray(); 
        } 
 
        public void ApplyChanges( 
            IReadOnlyList<FileAction> actions) 
        { 
            foreach (FileAction action in actions) 
            { 
                switch (action.Type) 
                { 
                    case ActionType.Create: 
                    case ActionType.Update: 
                        File.WriteAllLines( 
                            action.FileName,  
                            action.Content); 
                        continue; 
 
                    case ActionType.Delete: 
                        File.Delete( 
                            action.FileName); 
                        continue; 
 
                    default: 
                        throw new InvalidOperationException(); 
                } 
            } 
        } 
 
        public void ApplyChange( 
            FileAction action) 
        { 
            ApplyChanges( 
                new List<FileAction> { 
                    action }); 
        } 
    } 
} 

There are four methods in the preceding FileProcessor class; they are ReadFile(), ReadDirectory(), and two ApplyChanges() methods with different signatures. The ReadFile() method is used to read the selected file and form it into the FileContent data type. The ReadDirectory() method is used to read all the files in the selected directory and form them into the FileContent data array. The ApplyChanges() method is used to make an execution to the selected file. If the action is Create or Update, then the File.WriteAllLines() method will be called. If the action is Delete, then the File.Delete() method will be invoked. Otherwise, the InvalidOperationException exception will be thrown.

After we finish with the FileProcessor class, it's time to create the AppService class. The implementation of the class is as follows, and we can find it in the AppService.cs file:

namespace DomainLogicAndMutatingState 
{ 
    public class AppService 
    { 
        private readonly string _directoryName; 
        private readonly LibrarianshipImmutable _librarian; 
        private readonly FileProcessor _fileProcessor; 
 
        public AppService( 
            string directoryName) 
        { 
            _directoryName = directoryName; 
            _librarian = new LibrarianshipImmutable(10); 
            _fileProcessor = new FileProcessor(); 
        } 
 
        public void AddRecord( 
            string visitorName, 
            string bookTitle, 
            DateTime returningDate) 
        { 
            FileInfo fileInfo = new DirectoryInfo( 
                _directoryName) 
                    .GetFiles() 
                    .OrderByDescending(x =>  
                        x.LastWriteTime) 
                    .First(); 
 
            FileContent file =  
                _fileProcessor.ReadFile( 
                    fileInfo.Name); 
 
            FileAction action =  
                _librarian.AddRecord( 
                    file,  
                    visitorName, 
                    bookTitle, 
                    returningDate); 
 
            _fileProcessor.ApplyChange( 
                action); 
        } 
 
        public void RemoveRecord( 
            string visitorName) 
        { 
            FileContent[] files =  
                _fileProcessor.ReadDirectory( 
                    _directoryName); 
 
            IReadOnlyList<FileAction> actions = 
                _librarian.RemoveRecord( 
                    visitorName, files); 
 
            _fileProcessor.ApplyChanges( 
                actions); 
        } 
    } 
} 

As we discussed previously, the AppService class is used as the bridge between the LibrarianshipImmutable class and the FileProcessor class. We have the two methods in this AppService class that have completely the same signature with methods in the LibrarianshipImmutable class; they are the AddRecord() and RemoveRecord() methods. And as a bridge, we can see that in the class constructor, the LibrarianshipImmutable and FileProcessor class constructors are invoked to create a new instance. By calling the AddRecord() method in the AppService class, we actually invoke the AddRecord() method in the LibrarianshipImmutable class and then we call the ApplyChange() method in the FileProcessor class. Likewise, the invocation of the RemoveRecord() method in the AppService class will invoke the RemoveRecord() method in the LibrarianshipImmutable class and then the ApplyChange() method in the FileProcessor class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset