SOLID Principles: Dependency Inversion

The Dependency Inversion Principle “D”

High Level Modules should not depend on low level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Overview

The word inversion is a misfit in the 2020s but it has some history around it. Most software were developed using procedural methodologies in the 80s and 90s; in this approach high level business functionalities are broken into low level reusable functions. Although this approach creates good reusable low level functions but it makes high level business functions to require rework when any change is done in low level functions. The term dependency ‘inversion’ was used in the late 90s to say that low level modules should depend on high level modules (main business function) via abstraction in Object Oriented Analysis & Design.

Thus, inverting the way dependencies should be defined is called Dependency Inversion.

Layered Design

A good software architecture defines different layers with specific responsibilities. The bottom layers are more generic & reusable and functionalities tend to become more specific to business use cases as we move up the layers. E.g.

Figure-1 Layered Design
  • The lowest layer contains Infrastructure or Utilities like file handling, DB repo, security.
  • The middle layer contains reusable business services or models. The functionalities defined here are business functions which are independent of business use cases.
  • The top layer contains business use cases which are orchestrated using the middle layer. These are the functionalities understood by users of the application.

The different layers are defined in different classes, packages/namespaces but they are still tightly coupled. To reduce the coupling and enable higher reuse of Business Services, Different layers can invoke the concrete classes via abstraction, like in the Figure-2:

Figure-2 Layered Design

Conclusion

Defining module dependencies via Abstraction helps in maintaining low coupling while keeping the code reusable.

Proper implementation of Dependency Inversion principle requires a broad understanding of the Domain for which application is being developed. The key decision is to segregate functionality between middle layer (reusable business services) and top layer (business user cases).

Git Overview

Git is a distributed Version Control System.

This video explains (with demo) basic operations to start using Git:

  1. Comparison with Centralized VCS
  2. Staging Area and Lifecycle of a File
  3. Commonly used commands
  4. Branching and Merging

Tools used:

  1. Git v2.34.1.windows.1 on MS Windows 10
  2. Git Extensions v3.4.3.9999
  3. kdiff3 v0.9.97
  4. gitlab.com account
Git Basics Demo

SOLID Principles: Interface Segregation

Interface segregation is another technique that helps in keeping the system loosely coupled by dividing the functionality into small components.

The Interface Segregation Principle “I”

It states that no client should be forced to depend on methods it does not use. In other words, only relevant methods and functionalities should be exposed to clients and clients should have ability to pick and choose what’s relevant to them.

The problem with creating large Interfaces and hence heavy classes is that every client may not require all the functionality offered by the large Interface but they end up including things they don’t use. This leads to a very common issue in the Software Life-cycle where things break as a side-effect of some other change. Unexpected errors may occur for a client when there are changes in certain areas of the underlying application (which are not even being used by the client).

Interface Segregation can be considered an extension of Single Responsibility Principle which helps in segregating related but different functionalities.

Consider the example of a generalized E-Commerce platform which can be utilized for sale & delivery of physical goods and electronic subscription.

Classes which handle Delivery can be defined like this:

Figure 1

Some of OrderDelivery methods like assignPostalServiceProvider() may not be applicable for ElectronicDelivery. Clients that only require ElectronicDelivery will unnecessarily get the APIs specific to Physical Delivery.

class ElectronicDelivery implements OrderDelivery {

  @Override
  public void assignPostalServiceProvider(String provider) {
    //Do nothing
  }
}

Any changes related to Physical Delivery in the OrderDelivery interface, would require updates in the ElectronicDelivery concrete implementation too. It is useless but necessary change that can be avoided with proper segregation of Interfaces like:

Figure 2

The way classes are defined in Figure 2 is much cleaner and each class contains only relevant behavior.

Conclusion

When each class contains only relevant functionality, then client applications are never impacted by unused dependencies. Interface Segregation helps in reducing side-effect bugs during the maintenance of Application.

SOLID Principles: Behavioral Subtyping

Also known as Liskov substitution principle, it states that a derived entity should be behavioral extension of the base entity and not just syntactical extension.

The Liskov Substitution Principle “L”

This is an often overlooked principle while defining class hierarchies. Base & derived classes should have behavioral relation between them and Inheritance should not be applied just for the sake of reusing code.

As per LSP, objects of Base class should be replaceable with objects of Derived class without any change in the behavior or correctness of the application.

Inheritance signifies an Is-A relation between the derived class and the base class but at times, it can be misleading. Couple of classic examples:

  1. Square extends Rectangle: In basic geometric terms a square is a specific form of rectangle, so it might seem right for Square class to derive from Rectangle.
    • In object oriented design terms, a Rectangle has 2 sides but a Square has only 1.
    • When Square class extends Rectangle, it gets more variables than it needs and thus would need to do some work-around to achieve true behavior of a single side square.
    • Consider the following code snippet defying Behavioral Subtyping. setHeight() of Rectangle changes the value of variable height but setHeight() of Square changes both height & width. If clients replaces a Rectangle object with a Square object, then they will observe (unexpectedly) that setHeight() changes both height & width.
public class Rectangle {

  protected int height;
  protected int width;

  public int getHeight() {
    return height;
  }

  public void setHeight(int height) {
    this.height = height;
  }

  public int getWidth() {
    return width;
  }

  public void setWidth(int width) {
    this.width = width;
  }
}

public class Square extends Rectangle {

  @Override
  public int getHeight() {
    return super.getHeight();
  }

  @Override
  public void setHeight(int height) {
    super.setHeight(height);
    super.setWidth(height);
  }

  @Override
  public int getWidth() {
    return super.getWidth();
  }

  @Override
  public void setWidth(int width) {
    super.setHeight(width);
    super.setWidth(width);
  }

}
  1. Stack extends Queue: Both of these data structures have put() and get() methods. The put() method for them is same because it adds an element at the end of the data structure. The get() method is different; it implements FIFO in case of Queue and LIFO in case of Stack.
    • Defining Stack to extend Queue does provides code re usability but breaks the Liskov substitution principle because a Queue cannot be replaced with Stack without changing the underlying behavior.
    • A better class design would be something like the following. An abstract class AbstractCollection contains the common code of put() method and both Stack and Queue extend the abstract class to add concrete definition of get() method.
Figure 1

As per Wikipedia, Liskov substitution principle says “if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program“.

In practice, applying this principle helps in deciding between Inheritance (Is-a) and Composition (Has-a) relation between classes.

Consider the following example of Rolling File Logger.

The objective is to add log messages to a file and roll the file based on the size of file. The library needs to support another functionality of rolling the log file based on time interval.

In this first approach (Figure 2) which defies Behavioral Subtyping, these two functionalities can be achieved by TimeRollingFileLogger extending SizeRollingFileLogger and overriding the method isRolloverRequired() which determines when to roll the log file.

Figure 2

A better design is to separate the Rolling Strategy from the FileLogger like:

Figure 3

In the second approach of Figure-3, FileLogger has-a RollingStrategy and Interface RollingStrategy is implemented by three different concrete classes for three specific strategies viz None, SizeBased and TimeBased.

The second approach better adheres to the behavioral relation between FileLogger and it’s rolling strategies in term of Object Oriented Design.

Conclusion

Behavioral Subtyping OR Liskov Substitution Principle helps in defining proper Object Oriented Design relation between classes by distinguishing Composition and Inheritance.

SOLID Principles: Open-Closed

The Open-Closed Principle “O”

“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.

This principle advocates Inheritance and Polymorphism. The way to achieve this contradictory suggestion is to design loosely coupled classes which bind together using Interfaces or Abstraction.

Functionality can be extended by Inheriting a class OR by providing concrete implementation for an Interface or Abstract class. These can be done without changing anything in the existing classes; in fact existing classes can be in separate library.

Consider this class diagram for Logger:

Figure 1

The class LogManager is defying Open-Closed principle by containing functionalities for logToFile() and logToDB(). In future, there may be a requirement to log messages using webservice call. This new functionality would require changes in existing LogManager class; but it’s bad design if an existing class is modified to add new functionality.

A good design is to have separate classes for logging to File and DB:

Figure 2

The LogManager class doesn’t know how to write a String and it depends on derived classes for writing string. It has log(Object) method to serialize object into string and log(String) abstract method for concrete implementation:

    public void log(Object obj) {

      String message = serializer.serialize(obj);

     log(message);

    }

    public abstract void log(String msg); 

With this design, any new type of logger like web-service or message queue can be added by providing another concrete implementation of LogManager. It will not require any change in the existing classes.

Conclusion

The Open-Closed principle helps in Object Oriented class designing. It enforces Abstraction and increases code reusability.

SOLID Principles: Single Responsibility

Introduction

SOLID Design Principles are tips and tricks to organize the code in different classes which helps in keeping the code clean, maintainable and scalable (or extensible).

The way software modules & classes are designed & organized have a major impact on the people associated with it:

  1. Programmers: The application can be easy to manage and extends.
  2. Business owners: The application can be quickly enhanced to add business critical and compliance mandatory functionalities.
  3. Users: The application can provide improved experience and help achieve more.

The Single Responsibility Principle “S”

As the name suggests, a class should be responsible for one and only one thing. In words of Robert C. Martin “A class should have only one reason to change”.

Although it seems pretty obvious & it is basic Object Oriented Design, but still it gets defied many times.

Consider this example of LogManager class having a field Enum LogType with possible values of FileBased, DBBased:

Figure 1

The log() method converts Object to string and logs the string to File or DB conditionally:

    void log(Object obj) {

      String message = convertToJson(obj);

      switch(logType) {
        case FileBased:
          logToFile(message);
          break;

        case DBBased:
          logToDB(message);
          break;
      }

    }

The purpose of above code is to perform logging, it seems to be doing fine by logging into a File or DB and it includes some helpers like converting an Object to JSON string.

However, this class has two reasons to change:

  1. Changes in the way messages are logged
  2. Changes in the way String is created

In future, it may be decided to log messages as xml or csv or any other format. OR a different library may be used for converting Object to json string. But it should not impact the way strings are being persisted to storage (either File or DB).

Doing multiple things in a single class makes it tightly-coupled:

  • Some variable may be reused
  • Low level and Mid level methods may get mixed. e.g. log(String msg) should have been a low level method (only writing the string to storage) but LogManager class has log(Object obj) which is mid level method invoking convertToJson(Object).

In a class like this, it becomes very difficult to change something in isolation. A fix in one part of class impacts other parts too and might break something unintentionally.

A better design is to create a Serializer Interface for converting Object to string. Implement a JSONSerializer for json string conversion.

Use abstraction (Serializer) in the LogManager class:

Figure 2

The LogManager class doesn’t need to know how serialization is happening and the log() method should depend on Abstraction for Serialization:

    void log(Object obj) {

      String message = serializer.serialize(obj);

      switch(logType) {
        case FileBased:
          logToFile(message);
          break;

        case DBBased:
          logToDB(message);
          break;
      }

    } 

Conclusion

Single Responsibility principle helps in :

  1. Making the classes loosely-coupled
  2. Minimizing the impact of a change (bug-fix or enhancement)
  3. Making the code easy to understand

Code Refactoring

Introduction

Software developers strive to write functionally working code by self-analysis and brainstorming with peers to cover all possible use-cases which may occur in real world (post the deployment of the application).

In Enterprise Application development there are many methodologies & processes to ensure that the application works as per the Requirements & Functional Specifications. Assuming, functional specifications are as per User requirements.

Enterprise Applications are long living creatures expected to survive years and sometimes decades. They are expected to go through cycles of evolution using bug-fixes, enhancements, integration with other applications, removal of obsolete functionalities.

Therefore, it is important to develop a code that is:

  1. Maintainable
  2. Robust
  3. Scalable

At times, these aspects of the code are not obvious but can be achieved by asking the right questions.

Example Application (Step 1)

Let’s start with a functionally working piece of code and improve it step by step.

Please refer to the two classes Person and PersonManager:

public class Person {


  private String firstName;
  private String lastName;
  private String gender;

  public Person(String firstName, String lastName, String gender) {
	super();
	this.firstName = firstName;
	this.lastName = lastName;
	this.gender = gender;
  }

  /*
   * Getters and Setters
   */

}

public class PersonManager {

  public static void main(String[] args) {

	List<Person> people = new ArrayList<>();

	Person p1 = new Person("Alan", "Turing", "Male");
	people.add(p1);

	Person p2 = new Person("Joan", "Clarke", "Female");
	people.add(p2);

	Person p3 = new Person("James", "Gosling", "Male");
	people.add(p3);

	for(Person p : people) {
	  System.out.print(getValue(p.getGender()));
	  System.out.println(" " + p.getFirstName() + " " + p.getLastName());
	}

  }

  private static String getValue(String input) {

	if(input.equalsIgnoreCase("Male")) {
	  return "Mr";
	} else {
	  return "Ms";
	}

  }

}

Although an application which doesn’t take any input from Users or External sources like a DB is not very useful, but let’s stick to it for the sake of simplicity and being to the point on writing the clean code.

For complete code of the initial application, please refer https://gitlab.com/saurabhjain1537/refactoring_workshop/-/tree/master/workshop/src/main/java/com/citra/workshop/person/step1

Maintainability (Step 2)

Let’s think about few questions.

  1. Is this code easy to understand for someone else ?
  2. If I come back to this code after 6 months, can I recall what I did with a quick glance ?

Although these questions seem easy but practically it takes a bit of experience to answer them honestly. Initially it’s worth taking help from peers & colleagues and get honest feedback from some one trustworthy.

Another way is to self-review a functionality which was developed a few months ago.

Let’s analyze this method:

  private static String getValue(String input) {

	if(input.equalsIgnoreCase("Male")) {
	  return "Mr";
	} else {
	  return "Ms";
	}

  }

This method is taking Gender as input and returning a salutation based on the value. This method will be easier to understand if we rename it to getSalutation and rename the method argument to gender. Something like:

  private static String getSalutation(String gender) {

	String salutation = null;
	if(gender.equalsIgnoreCase("Male")) {
	  salutation = "Mr";
	} else {
	  salutation = "Ms";
	}

	return salutation;

  }

Analyze this loop:

	for(Person p : people) {
	  System.out.print(getValue(p.getGender()));
	  System.out.println(" " + p.getFirstName() + " " + p.getLastName());
	}

This loop is printing Full Name with salutation, it makes sense to create a method getFullName and invoke it from the loop:

	for(Person p : people) {
	  System.out.println(getFullName(p));
	}

  }

  private static String getFullName(Person p) {
	return getSalutation(p.getGender()) + " " + p.getFirstName() + " " + p.getLastName();
  }

The complete PersonManager class after refactoring (from Step 1 to Step 2) to make the code more readable, understandable and maintainable:

public class PersonManager {

  public static void main(String[] args) {

	List<Person> people = new ArrayList<>();

	Person alan = new Person("Alan", "Turing", "Male");
	people.add(alan);

	Person joan = new Person("Joan", "Clarke", "Female");
	people.add(joan);

	Person james = new Person("James", "Gosling", "Male");
	people.add(james);

	for(Person p : people) {
	  System.out.println(getFullName(p));
	}

  }

  private static String getFullName(Person p) {
	return getSalutation(p.getGender()) + " " + p.getFirstName() + " " + p.getLastName();
  }

  private static String getSalutation(String gender) {

	String salutation = null;
	if(gender.equalsIgnoreCase("Male")) {
	  salutation = "Mr";
	} else {
	  salutation = "Ms";
	}

	return salutation;
  }

}

For complete code after refactoring for Maintainability, please refer https://gitlab.com/saurabhjain1537/refactoring_workshop/-/tree/master/workshop/src/main/java/com/citra/workshop/person/step2

Robustness (Step 3)

Enterprise Applications interact with multiple Actors like:

  1. Users via Web interface or Notifications
  2. Storage systems like DB or File system
  3. Other applications via messaging queues

A robust application is the one which handles errors due to User input, failure in other systems and report back to the Actors meaningfully.

Few questions to improve robustness:

  1. Does the application validates every input from Actors ?
  2. For Input and Internal processing issues, does it responds with meaningful messages ?
  3. Can anyone easily identify the Module & Functionality causing an issue by analyzing the logs ?

Let’s analyze this method:

  private static String getSalutation(String gender) {

	String salutation = null;
	if(gender.equalsIgnoreCase("Male")) {
	  salutation = "Mr";
	} else {
	  salutation = "Ms";
	}
	return salutation;
  }

It returns the salutation “Ms” for every gender string that is not “Male”. Even in case of misspelled gender or garbage input like “xyz”, it assumes non-Male and return the salutation “Ms” .

It will make more sense if it validates the Gender to be either “Male” or “Female”. There can be a Gender enum containing valid values and a custom exception class PersonValidationException to specify error scenario:

public enum Gender {

  Male,
  Female;
}

public class PersonValidationException extends RuntimeException{

  public PersonValidationException(String arg0, Throwable arg1) {
	super(arg0, arg1);
  }

  public PersonValidationException(String arg0) {
	super(arg0);
  }

}

Now getSalutation method can be refactored as:

  private static String getSalutation(Gender gender) {

	String salutation = null;
	if(Gender.Male == gender) {
	  salutation = "Mr";
	} else if(Gender.Female == gender) {
	  salutation = "Ms";
	} else {
	  throw new PersonValidationException("Unsupported Gender " + gender);
	}

	return salutation;

  }

Person class can be refactored to validate the mandatory fields and dis-allow changes in the member variables values by declaring them as final:

public class Person {

  private final String firstName;
  private final String lastName;
  private final Gender gender;

  public Person(String firstName, String lastName, Gender gender) {

	if(firstName == null || lastName == null || gender == null) {
	  throw new IllegalArgumentException("Mandatory Parameter missing.");

	}
	this.firstName = firstName;
	this.lastName = lastName;
	this.gender = gender;
  }

  /*
   * Getters
   */
}

For complete code after refactoring for Robustness, please refer https://gitlab.com/saurabhjain1537/refactoring_workshop/-/tree/master/workshop/src/main/java/com/citra/workshop/person/step3

Scalability (Step 4)

Enterprise Applications evolve very fast with lots of enhancements. Developers cannot foresee what is going to come-up in future. Still, they can design the application in ways that (a) makes adding of new features & functionalities simple and (b) decreases the impact on already-working features.

There are lots of techniques that can help in designing scalable software like SOLID principles and Design Patterns. Each one of those deserve a separate article, so to keep things simpler in this article we can make just one improvement.

Let’s analyze the following two methods in PersonManager class:

  private static String getFullName(Person p) {
	return getSalutation(p.getGender()) + " " + p.getFirstName() + " " + p.getLastName();
  }

  private static String getSalutation(Gender gender) {

	String salutation = null;
	if(Gender.Male == gender) {
	  salutation = "Mr";
	} else if(Gender.Female == gender) {
	  salutation = "Ms";
	} else {
	  throw new PersonValidationException("Unsupported Gender " + gender);
	}

	return salutation;

  }

These methods have improved a lot and they seem good now. Still, few questions to consider:

  1. Should these methods be part of PersonManager class?
  2. What if some other class using Person class needs to represent the Person data in similar manner? Will it make sense to duplicate the code ?
  3. Is getFullName() performing any business function in PersonManager? Isn’t it just another way Person data is represented?

To allow re-usability of code and to keep only relevant code in every class, these methods can be moved from PersonManager to Person class:

public class Person {


  private final String firstName;
  private final String lastName;
  private final Gender gender;

  public Person(String firstName, String lastName, Gender gender) {

	if(firstName == null || lastName == null || gender == null) {
	  throw new IllegalArgumentException("Mandatory Paramater missing.");

	}

	this.firstName = firstName;
	this.lastName = lastName;
	this.gender = gender;
  }

  /*
   * Getters
   */
  public String getSalutation() {

	String salutation = null;
	if(Gender.Male == gender) {
	  salutation = "Mr";
	} else if(Gender.Female == gender) {
	  salutation = "Ms";
	} else {
	  throw new PersonValidationException("Unsupported Gender " + gender);
	}

	return salutation;
  }

  public String getFullName( ) {

	return getSalutation() + " " + getFirstName() + " " + getLastName();
  }
}

For complete code after refactoring for Scalability, please refer https://gitlab.com/saurabhjain1537/refactoring_workshop/-/tree/master/workshop/src/main/java/com/citra/workshop/person/step4

Conclusion

In this Example application, we started with a piece of code which was functionally working and was providing valid & expected output. Step by step, we improved the quality of code while keeping the same functionality working all the time.

While refactoring an existing code, it is a prerequisite to understand the functionality completely and write comprehensive automated test cases. Execute the automated tests at each step to be sure of retaining the functionality while making the implementation future ready.

In-memory unit test for .Net Entity Framework Application

Objective

Creating repeatable Unit Test cases for a .Net applications which uses Entity Framework Core for database operations.

It is recommended to write independent and repeatable test cases for the business logic. We need to handle certain cases where logic depends upon the data already existing in database. In such scenarios we can use an in-memory database like SQLite.

Setup

Following tech stack is used in this example

  1. .Net Core 3.1
  2. NUnit 3.12
  3. EntityFrameworkCore Sqlite 3.1.5
  4. Visual Studio Code

Application

Let’s build a school management application which is going to check if there is vacancy for admission in a class.

Create a new dotnet console application SchoolApp:

Add Model classes for Student and Class information:

This class contains School-Class details including Max Number of Students allowed
This class contains Student details

These classes are associated with database tables and have Foreign Key relationship.

Add AppDBContext extending EntityFrameworkCore DbContext.

This class contains the DbSets for Model classes

Override DBContextOptions constructor and OnConfiguring() so that the application uses in-memory SQLite for unit testing but a different Relational database like Oracle OR SQL Server during actual Production deployment

Define Primary Key for each Model class

Complete Class https://gitlab.com/saurabhjain1537/nunit-sqlite-schoolapp/-/blob/master/SchoolApp/Data/AppDBContext.cs

    public class AppDBContext:DbContext
    {
        public DbSet<SchoolClass> SchoolClasses { get; set;}
        public DbSet<Student> Students {get; set;}

        public AppDBContext() : base()
        {
        }

        public AppDBContext(DbContextOptions<AppDBContext> options) : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if(!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseOracle("TODO - connection string");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<SchoolClass>(s =>
                {
                    s.HasKey(k => new {k.Id});
                });

            modelBuilder.Entity<Student>(s =>
                {
                    s.HasKey(k => new {k.Id});
                });
        }

    }

Add Admission class having appDBOptions variable.

This class has a constructor to initialize DbContextOptions. The method getAppDBContext() is responsible for providing the appropriate DBContext considering if the application is actually deployed OR running unit tests.

Add a hasVacancy() method to determine if we can allow new admission in a particular school-class.

Complete Class https://gitlab.com/saurabhjain1537/nunit-sqlite-schoolapp/-/blob/master/SchoolApp/Admission.cs

    public class Admission
    {
        private DbContextOptions<AppDBContext> appDBOptions;

        public Admission()
        {
            this.appDBOptions = null;
        }

        public Admission(DbContextOptions<AppDBContext> appDBOptions)
        {
            //This constructor is used for Unit testing
            this.appDBOptions = appDBOptions;
        }

        private AppDBContext getAppDBContext()
        {
            if(this.appDBOptions == null)
            {
                return new AppDBContext();
            }
            else
            {
                return new AppDBContext(this.appDBOptions);
            }

        }

        public bool hasVacancy(string Standard)
        {
            bool hasVacancy = false;

            using(var context = getAppDBContext())
            {
                hasVacancy = context.SchoolClasses
                                        .Where( c => c.Standard == Standard && c.MaxNumStudents > c.Students.Count)
                                        .Count()
                              > 0;

            }

            return hasVacancy;

        }

    }

Test Project

Create a new dotnet nunit application SchoolApp.Tests

Add reference to SchoolApp project

Add Test class AdmissionTest.

This class uses in-memory SQLite to initialize an Object of Admission class.

SQLite connection DataSource=:memory: signifies in-memory usage

Add a test case:

  1. Inserts a new SchoolClass in database
  2. Checks for Vacancy in the SchoolClass
  3. Expects availability to be True

Add a test case:

  1. Inserts a new SchoolClass in database
  2. Adds Max allowed number of students to the SchoolClass
  3. Checks for Vacancy in the SchoolClass
  4. Expects availability to be False

Complete Test Class https://gitlab.com/saurabhjain1537/nunit-sqlite-schoolapp/-/blob/master/SchoolApp.Tests/AdmissionTest.cs

    public class AdmissionTest
    {
        DbContextOptions<AppDBContext> configOptions = null;
        private Admission admission;

        [SetUp]
        public void Setup()
        {
            var connection = new SqliteConnection("DataSource=:memory:");

            configOptions = new DbContextOptionsBuilder<AppDBContext>().UseSqlite(connection).EnableSensitiveDataLogging().Options;

            admission = new Admission(configOptions);

        }

        [Test]
        public void hasVacancy_True()
        {
            using (var context = new AppDBContext(configOptions))
            {
                context.Database.OpenConnection();
                context.Database.EnsureCreated();

                context.SchoolClasses.Add( new SchoolClass{ Id = 1, Standard = "6", Section = "B", MaxNumStudents = 10 });

                context.SaveChanges();

            }

            var hasVacancy = admission.hasVacancy("6");

            Assert.AreEqual(true, hasVacancy);

        }

        [Test]
        public void hasVacancy_False()
        {
            using (var context = new AppDBContext(configOptions))
            {
                context.Database.OpenConnection();
                context.Database.EnsureCreated();

                var schoolClass6B = new SchoolClass{ Id = 1, Standard = "6", Section = "B", MaxNumStudents = 5 };
                context.SchoolClasses.Add(schoolClass6B );
                context.Students.Add( new Student { Id = 1, Name = "a", schoolClass = schoolClass6B});
                context.Students.Add( new Student { Id = 2, Name = "b", schoolClass = schoolClass6B});
                context.Students.Add( new Student { Id = 3, Name = "c", schoolClass = schoolClass6B});
                context.Students.Add( new Student { Id = 4, Name = "d", schoolClass = schoolClass6B});
                context.Students.Add( new Student { Id = 5, Name = "e", schoolClass = schoolClass6B});

                context.SaveChanges();

            }

            var hasVacancy = admission.hasVacancy("6");

            Assert.AreEqual(false, hasVacancy);

        }

    }

Execute Tests

Result

We have successfully created a SchoolApp to validate school Admissions and prepared repeatable test cases using SQLite in-memory database.

The complete code this Example project is available at https://gitlab.com/saurabhjain1537/nunit-sqlite-schoolapp/-/tree/master

Inner Classes in Java

An Inner class is the one which is defined inside another class. This technique doesn’t seem appealing in the beginning but becomes interesting once we start diving deep to explore features like

  1. Connection of inner class with outer class
  2. Multiple inheritance
  3. Anonymous class

Connection with outer class

Inner class can access fields and methods of outer class as if they are its own.

inner-class

In above example, inner class ‘Engine’ can invoke private fields & methods of outer class ‘Car’ like it would access its own members. This is possible because inner class keeps a reference of outer class object which is used to instantiate the inner class object. Like any other class keyword new is used to create object of inner class but there is a difference here – for inner class we need to specify object of outer class dot new keyword, e.g. c.new in this example.

Multiple inheritance

Inner class is second technique to achieve multiple inheritance in Java, first one being interfaces. There are real life problems where logical solution lies in a single class inheriting from multiple classes or abstract classes; this can be achieved by outer class extending one class and multiple inner classes extending different classes or multiple inner classes extending same base class differently. For example,

inner-class-multiple-inheritance

Anonymous inner class

It is a technique using which an object can be created of a class definition by skipping class name. It can be useful in cases where we need only one object of a class.

inner-class-anonymous

In above example, getFuel() method of RacingCar returns an anonymous class object by implementing Fuel interface. Also, note the semi-colon to end the return statement.

Interfaces in Java

Interface in Java is a concept to separate contract (what) from implementation (how).

The keyword interface creates an entity which defines how a class is going to look like with method names, their list & type of arguments and their return type. Interface does not define any method body.

One or more classes can implement the interface to provide the actual method body for all the methods declared in the interface . Since interfaces do not contain any method body, Java does not allow creation of objects of interface. Objects can be created for class that implements the interface.

interface

Similar to inheritance, when defining class for interface we specify name of the interface it is implementing. Here we use the keyword implements to define the class.

A class can implement multiple interfaces by separating Interface names using comma after the implements keyword. An object of such class can be upcasted to any of the interfaces. This is Java’s way of multiple inheritance which is directly not allowed with extends keyword.

multiple-inheritance

Abstract Class

We have discussed about classes and interfaces, there is another entity in Java called Abstract Class which falls into same category but is midway between class and interface. Mid-way because we can provide definition of some methods (like classes) and leave some as abstract or as declaration  (like interfaces). Java does not allow instantiation of abstract class but allows it to be extended like any other class using extends keyword.

abstract-class

In this example, we have created a class TennisPlayer by inheriting from abstract class Person and implementing interface Player. TennisPlayer gets eat() capability from Person but is forced to define work() capability as mandated in same Person abstract class. Similarly, TennisPlayer is forced to implement play() capability of Player interface. Since playing tennis is the work for a tennis player, we reused play as work.

Benefits of using Interfaces & Abstract classes-

  • Re-usability: Using inheritance we can design in a way such that code is reused to its maximum. In above example – common activity of a Human like eat is defined in the abstract class.
  • Extensibility – Using interfaces we can design applications that are easily extensible in future. In above example – SoccerPlayer can be added easily by implementing Player and extending Person.

Word of caution: Using Interfaces is somewhat tricky because it makes sense to use them only when we have multiple implementations either currently or in future. If there is no scope of multiple implementation in future then its wiser to avoid interface and use a class directly.