次の方法で共有


Implementing the "Only Talk to Friends" pattern using the InternalsVisibleTo Attribute

Did your parents ever tell you "don't talk to strangers?"  They were trying to protect you, and even though you are all grown up now, that principle is still relevant--to the design of your software, that is.  The basic idea is that classes should limit their surface area and not expose that which reveals implementation details.  Let's go though an example to illustrate these concepts.

In a traditional layered architecture you logically divide components by responsibility.  In this example, we will use a domain, business and data access logic layer each realized as a Visual Studio 2008 project.  In our contrived architecture, we want the business layer to be the "gatekeeper" for retrieving customer domain objects.  The business layer uses the data access logic layer to fetch a customer, so we might expose the data access class as a property of the business class as demonstrated here:

DontTalkToStrangersIteration1

Here's the code to use this design: 

DontTalkToStrangersCodeIteration1

So what's wrong with this approach?  We have violated a core principal of object oriented design by exposing implementation details.  Just like your parents protected you from strangers, classes should not expose their children or any other details about implementation.  You may have heard of this concept by another name such as:

  • Don't Talk to Strangers
  • Principal of Least Knowledge
  • Demeter's Law

Consumers don't need to know how the CustomerBusiness fetches a Customer, it only needs to know that CustomerBusiness fetches a customer.  So, let's revise the design using a technique called interface promotion.  This involves hiding a child object and promoting all or some of its interface to the parent.  The new design looks like this:

DontTalkToStrangersIteration2

Here's the code to use the new design:

DontTalkToStrangersCodeIteration2

Only Talk to Friends

The responsibility of the business layer is to act as a "gatekeeper" and control access to the underlying layers.  Although it might seem that we have accomplished our goal with this design, there is another issue that needs to be addressed.  Remember the layered architecture?  Business logic is in one project/assembly and data access logic is in another.  In order for CustomerBusiness to use CustomerData, the methods in CustomerData must be public.  The implication of this is a consumer can reference the data access layer and call it's public methods, bypassing any logic in the business layer.  To solve this problem, I would like to introduce a technique that I call only talk to friends.

The intent of the only talk to friends pattern is provide a mechanism for an assembly to expose it's interface to specified friend assemblies.  In our example, the data access logic layer would only expose it's interface to the business layer.  In order to accomplish this, let's revisit the design and make the CustomerData class and its methods internal:

CustomerData

Now for the magic. Add the InternalsVisibleTo attribute to the AssemblyInfo.cs file in the Data project thereby exposing anything internal in the data access logic layer to the business layer.

image

The InternalsVisibleTo attribute exposes anything internal in one assembly to the specified friend assembly.  It would be nice if we could be more granular than the entire assembly, but this approach will work well for most circumstances.

Sometimes It's OK to Talk to Strangers

There is one exception for breaking the "do not talk to strangers" principle, and that is unit testing.  When we write the unit test for CustomerBusiness, we only want to test CustomerBusiness, not CustomerData or the database.  In order to do this, we mock the CustomerData class.  For this work, CustomerBusiness will need to provide a mechanism that allows the unit test to replace the CustomerData instance in CustomerBusiness.  There are a few techniques for doing this such as dependency injection (and I do have an article planned on this subject), but one way is to allow CustomerData to be passed into the CustomerBusiness constructor as follows.

image

Note that the constructor that allows the data access object to be specified is internal.  Once again, you use the InternalsVisibleTo attribute granting access only to the business layer unit tests.

Summary

I hope this article has reinforced the object-oriented principle "don't talk to strangers," and provided a modern spin, "only talk to friends," that you can add to your toolbox.

References

Comments