Click here to Skip to main content
15,881,588 members
Articles / Web Development / ASP.NET

Using Internal Interfaces While Preserving Encapsulation

Rate me:
Please Sign up or sign in to vote.
4.80/5 (10 votes)
28 Oct 2009CPOL10 min read 41.2K   206   39   3
Discusses use of interfaces to recover encapsulation where the internal keyword is used

Introduction

The internal keyword of C# allows access to non-public types and members provided that the access is from within the same assembly or from within a friend assembly as specified using the "assembly:InternalsVisibleTo" attribute. According to MSDN:

A common use of internal access is in component-based development because it enables a group of components to cooperate in a private manner without being exposed to the rest of the application code. For example, a framework for building graphical user interfaces could provide Control and Form classes that cooperate using members with internal access. Since these members are internal, they are not exposed to code that is using the framework.

Within a single, well understood and well documented component assembly, with well defined separation between component code and client code, this makes sense. However, developers should nonetheless exercise caution because encapsulation is broken with respect to internal types and members. In fact, I find it helpful to think of the internal keyword not so much as an "access modifier", but more like an "access allower". It is a selective encapsulation breaking device.

Examples of this situation can be seen in many applications where a clean separation between business objects and service objects is desired, but the business objects often must carry around some serviceful bookkeeping attributes that are not part of the business model, like data access ids or modified times. Should the implementation allow internal access to these attributes (hidden but not encapsulated)? Should the attributes be accessible through a public interface available to the client code (well encapsulated but hidden)? Should serviceful attributes be located outside of business objects altogether? There are many design approaches one could take, but this situation can also be addressed by using internal interfaces. In this article, I will show how internal interfaces could be used to enforce encapsulation while exposing internal functionality to service code. It is not claimed that this solution is ideal for all situations, but even where it may not fit a particular design situation, using internal interfaces is still an interesting strategy to consider and is a great way for designers to document their design intentions in the code.

A Brief History of internal

First a few words about the internal keyword. The internal keyword of C# can be used to allow access to otherwise non-public types and members from within the same assembly or specially declared friend assemblies. In spite of this compact description, it has surprisingly rich semantics.

  • A top-level type definition (interface, class or struct) can be marked as either public or internal. If no access modifier is given for a top-level type, the default is internal. Metadata for top-level types that are marked internal are available anywhere within the defining assembly or within referencing friend assemblies.
  • A member (field, method, property, indexer, or event) can be marked with any of the possible access modifiers: public, protected, protected internal, internal, or private. If no access modifier is given for a member definition, the default is private. Metadata for type members that are marked internal are available anywhere within the defining assembly or within referencing friend assemblies provided that the metadata for the enclosing type is also available. (Note that in a struct, a member can be marked as neither protected nor protected internal because a struct is implicitly sealed.)
  • A nested type definition (a type defined within another type) can be marked with any of the possible access modifiers: public, protected, protected internal, internal, or private. If no access modifier is given for a nested type definition, the default is private as for members. Metadata for nested types that are internal are available anywhere within the defining assembly or within referencing friend assemblies provided that the metadata for the enclosing type is also available.

In C#, the interface keyword is used to define a type without implementation. An interface can contain signatures for the following kinds of members: methods, properties, indexers, and events. Member definitions occurring within an interface definition must all be public. (In fact, the C# compiler will not accept any access modifier on interface members and just makes them all public implicitly.)

When a class implements a public or internal interface, the class may be either public or internal. (However, when a class extends a base class or when an interface extends a base interface, the extending type cannot be less accessible than the base type.) In the implementation itself, the implementing members must be marked public. (The only exception to this rule is when using explicit interface method implementations (EIMI), which have no accessibility modifiers and have compiler-fixed accessibility rules.)

A "friend assembly" is declared using an assembly attribute specified within the granting assembly. In many Visual Studio project types (but not web site projects) this attribute is defined within a file named Properties\AssemblyInfo.cs. The syntax is:

C#
[assembly: InternalsVisibleTo("AssemblyName, PublicKey="XYZ123ABC456")]

The granting assembly needs to include this attribute. The friend assembly can be specified using only the name if it is a weakly named assembly, or it can be specified by name plus public key (as above) if it is a strongly named assembly. It is recommended for production code to use strongly named assemblies when granting friendship, because a weakly named assembly can be easily spoofed with the effect of granting internal access to malicious code. To get the public key of a strongly named assembly, use the sn.exe tool provided with Visual Studio as described elsewhere. (Note in .NET 2.0, the somewhat shorter hash "PublicKeyToken" is specified instead of the full PublicKey.)

The Example

The example is the backend of a small hypothetical N-tier application that tracks course enrollment for reporting at some college. Being an example, it is lean on the domain detail and has only one entity: a college course with some simple properties and some simple enrollment logic. It nonetheless has a business logic layer, a data access layer, and a factory for creating business objects. We would like to maintain a clean separation between business logic on one hand and service logic (data access and object creation) on the other.

The ICollegeCourse and ICollegeFactory interfaces contain simple, business-only definitions.

C#
namespace CollegeCourseClient
{
    // A college course interface.
    public interface ICollegeCourse
    {
        string CourseName { get; set; } 
        string CourseNumber { get; set; } 
        string Term { get; set; } 
        string Instructor { get; set; }
        string CourseDescription { get; set; }
        int CreditHours { get; set; } 
        string[] GetEnrolledStudents();
        void EnrollStudent(string student);
        void UnEnrollStudent(string student);
    }
    // Interface for concrete factories supporting CollegeCourse 
    public interface ICollegeFactory
    {
        ICollegeCourse GetNewCourse();
    }
}

Likewise, the data access layer exposes a simple interface as well.

C#
namespace DataAccessClient
{
    // Interface for data access layer implementations
    public interface IDataAccess
    {
        long Save(ICollegeCourse course);
        ICollegeCourse Load(long id);
        void Delete(ICollegeCourse course);
        void AcceptChanges();
        void RejectChanges();
        // Property which specifies which factory implementation to use.
        ICollegeFactory Factory { get; set; } 
    }

All of these interfaces are collected into an "interface" assembly. While not a hard and fast rule, I find that keeping the interfaces reasonably grouped and away from implementations helps cut down on the number of references I need. (See for example, Agile Principles, Patters and Practices in C# by Martin and Martin.)

The data access service will assume only the following very general and simple bookkeeping properties and methods are available on the business objects.

  • AUID: Some object ID that is considered temporary until the first time the object is saved, and then it is an application-wide unique ID after the object is saved.
  • IsDirty: A flag which tells the data access layer if the object has been modified since the last time it was saved.
  • Modified, Created: Some bookkeeping attributes tracking date and time the object was (last) touched by the data access layer.
  • ToStringRep(), FromStringRep(): Some methods for serializing the object to/from a string. (These are actually not for bookkeeping; more on this below in the Conclusion.)

At this point, a simple and quick anti solution for this backend might be to create public interfaces for the business object assembly with special "data access aware" internal properties and methods accessible by a friend data access assembly. This leads to several problems, some of which were outlined above.

  • Encapsulation is broken. The data access assembly now has access to whatever internals happen to be defined in the business object assembly, including ones not meant for data access.
  • A reference to the business object assembly must be added to the data access assembly.
  • There may now be a chicken and egg reference problem if the business implementation needs also to be aware of definitions in the data access assembly (assuming they are in different assemblies.)

Alternatively, these attributes could be defined in a separate public interface. While much better, it still has the undesirable aspect that client code has full access to the service functionality. While this may not be an issue for some designs, let's create an internal interface.

C#
namespace DataAccessClient
{
    // A service interface that supports data access and object creation.
    internal interface IDataAccessService
    {
        long AUID { get; set; }
        bool IsDirty { get; set; }
        DateTime? Modified { get; set; }
        DateTime? Created { get; set; }

        // Gets a string representation
        string ToStringRep();
        // Loads from string representation
        void FromStringRep(string rep);
    }

This is the interface that will be exposed to friend assemblies, so wherever it goes it will require the assembly: InternalsVisibleTo attribute. But for this simple case, it can just go into the same assembly as the other interfaces. By making the implementation and the service objects adhere to the same interface, one can make them agree on a contract for handling business objects without concerning the client.

Just for kicks, in the example, there are two CollegeCourse implementations in namespaces Impl1 and Impl2. Each implementation is intentionally, and somewhat artificially, different and each also has its own concrete factory class. (The code as shown below is abridged for clarity.) Note that the implementations are also internal. Although not required, this is consistent with the factory pattern and it requires client access through the public interfaces.

C#
namespace CollegeCourseImpl.Impl1
{
    // Toy Implementation number 1
    //
    // This implementation is a straightforward implementation of the
    // interfaces
    internal class CollegeCourse : ICollegeCourse, IDataAccessService
    {
        // etc...  
        internal string courseName;
        internal string courseNumber;
        internal string term;
        internal string courseDescription;
        internal string instructor;
        internal int creditHours = 0;
        internal List<string> enrolledStudents = new List<string>();
        // etc... 
    }

    // Concrete factory for Implementation 1. 
    //
    // The principal distinguishing feature of this implementation is that 
    // the initial auid is set to a negatively incrementing number.
    internal class CollegeCourseImplFactory : ICollegeFactory
    {
        private long currentAUID = 0L;
        // Singleton code, etc ... 
        public ICollegeCourse GetNewCourse()
        {
            CollegeCourse retval = new CollegeCourse();
            currentAUID = currentAUID - 1;
            retval.AUID = currentAUID;
            return retval;
        }
    }
}

namespace CollegeCourseImpl.Impl2
{
    // Toy Implementation number 2
    //
    // This implementation stores all fields locally in a string array
    internal class CollegeCourse : ICollegeCourse, IDataAccessService
    {
        // etc...
        private static readonly string MinorFieldSeparator = "++";
        internal string[] fields = new string[] { "", "", "", "", "", "0", "" };
        // etc...
    }

    // Concrete factory for Implementation 2. 
    //
    // The principal distinguishing feature of this implementation is that 
    // the initial auid is set to some constant negative number.
    internal class CollegeCourseImplFactory : ICollegeFactory
    {
        // Singleton code, etc ... 
        public ICollegeCourse GetNewCourse()
        {
            CollegeCourse retval = new CollegeCourse();
            retval.AUID = -999L;
            return retval;
        }
    }
}

In the above, many of the fields are marked internal. These could easily be private (and probably should be) but are here internal because:

  1. It is a frequent usage of internal to allow access to unit testing assemblies and these would likely be internal for that reason, and
  2. To illustrate and underline the obvious danger to encapsulation if access to these fields were not controlled.

Similarly, there are two data access implementations, each one is again intentionally different, and are shown abridged for clarity.

C#
namespace DataAccessImpl.Impl1
{
    // A mock data access class FOR DEMO PURPOSES ONLY
    //
    // The principal distinguishing feature of this implementation is that 
    // the data store is an in-memory  data table with schema matching the 
    // business object.  This class was developed very quickly as part 
    // of a demonstration of using internal interfaces.  
    internal class DataAccess : IDataAccess
    {
        // Singleton code, etc ... 
        private DataTable mockDatabase;
        // etc...
    }
}

namespace DataAccessImpl.Impl2
{
    // A toy data access class FOR DEMO PURPOSES ONLY.
    //
    // The principal distinguishing feature of this implementation is that 
    // the data store is an in-memory dictionary storing string 
    // representations of the given objects. This class was developed 
    // very quickly as part of a demonstration of using internal interfaces.
    internal class DataAccess : IDataAccess
    {
        // Singleton code, etc ... 
        private long insertionCount = 0L;
        private Dictionary<long> mockDatabase;
        private Dictionary<long> insertionList;
        private Dictionary<long> updateList;
        // etc...
    }
}

The data access implementations are kept in an assembly separate from the college course implementations, and there are no references between the two implementations. Each implementation assembly has exactly one reference to the interface assembly, and the implementation assemblies are friends of the interface assembly so that they can have access to the IDataAccessService interface, but not to any implementation details. A client assembly may now reference all three assemblies without being a friend of any assembly, is constrained to use business objects and invoke services through the public interfaces only. And if it is desired to grant internal access to unit test assemblies, these need be the only friends of the implementations.

Using the Code

The code was developed in C# on Visual Studio 2005 using .NET 2.0. It consists of three library projects and one console project. Simply download the code example, compile, and run the console application. The console application will run through a test which tests each of the two business implementations against each of the two data access implementations using only the public client interfaces. This is the test provided.

C#
// A test method which within a single implementation, instantiates 
// a college course, saves it, retrieves it and checks fields.
static void RunTest(Implementation impl)
{
    ICollegeCourse course = impl.Factory.GetNewCourse();
    course.CourseDescription = "This is a College Course";
    course.CourseName = "A College Course";
    course.CourseNumber = "CC 101";
    course.Instructor = "George Washington";
    course.Term = "Fall 2009";
    course.EnrollStudent("Moe");
    course.EnrollStudent("Larry");
    course.EnrollStudent("Curly");

    long courseID = impl.DataAccess.Save(course);
    impl.DataAccess.AcceptChanges();

    System.Threading.Thread.Sleep(50);
    course.Instructor = "Thomas Jefferson";
    impl.DataAccess.Save(course);
    impl.DataAccess.AcceptChanges();

    ICollegeCourse course2 = impl.DataAccess.Load(courseID);
    Debug.Assert(course.CourseName == course2.CourseName);
    Debug.Assert(course.CourseNumber == course2.CourseNumber);
    Debug.Assert(course.Term == course2.Term);
    Debug.Assert(course.Instructor == course2.Instructor);
    Debug.Assert(course.CourseDescription == course2.CourseDescription);
    Debug.Assert(course.GetEnrolledStudents().Length == 
				course2.GetEnrolledStudents().Length);
}

Conclusion

Using internal interfaces, the above code allows for both toy business implementations to work with both toy data access implementations. No references exist between the assemblies containing the implementations. The client code furthermore has no access to the internal interface used in data access and object creation. Encapsulation has been maintained and internal functionality hidden from the client code.

A common use of the internal keyword is to allow unit test code to access implementation code encapsulation for more detailed testing or to use popular frameworks externally (such as nUnit). This solution supports those cases by making it much safer: Since the internal interfaces are located in another assembly, the implementation assemblies do not need to have any other friend assemblies, so the testing assemblies can be the only ones. (This is the most popular use as documented at Stack Overflow.)

In the above example data access implementations, there is a subtle design problem that is interesting to point out. The Impl2 data access implementation uses the ToStringRep() and FromStringRep() methods, thus delegating some data access formatting responsibility back to the CollegeCourse implementations. This has an unfortunate side effect: An Impl1 CollegeCourse object is not directly transformable to an Impl2 CollegeCourse object across the Impl2 data access layer, because the Impl1 FromStringRep() may not parse an Impl2 ToStringRep() string. Note that I did not have to change the interface to break this, only the implementation! There is no such problem with the Impl1 data access layer, which sticks to the simple public interface of ICollegeCourse to accomplish data access.

History

  • 19th October, 2009: Initial post
  • 27th Otober, 2009: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey9-Feb-12 23:01
professionalManoj Kumar Choubey9-Feb-12 23:01 
GeneralMistake Pin
Richard Deeming27-Oct-09 5:00
mveRichard Deeming27-Oct-09 5:00 
GeneralRe: Mistake Pin
ggraham41227-Oct-09 6:27
ggraham41227-Oct-09 6:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.