Practical .NET

Who's in Charge Now? UI Control and the Interface Segregation Principle

If you adopt the Interface Segregation principle, then you can eliminate (or, at least, control) one of the most annoying problems in creating very useful objects. But following that principle through to its conclusion also inverts the traditional hierarchy of the development team.

The current "best practices" for application development is to follow the five principles summarized in the SOLID acronym: Separation of concerns, Open to extension but not modification, the Liskov substitutability principle, Interface segregation and Dependency inversion. While all of these principles are valuable, the last two form the greatest challenge to developers. Taken together, interface segregation and dependency inversion suggest that the best process for designing applications is the reverse of the process that developers have traditionally followed.

Limiting Access, Limiting Damage
The Interface Segregation Principle (ISP) addresses a problem common to objects that are used in multiple ways in multiple places -- that is to say, a problem that occurs with objects that are so useful that they're used in many places. When it becomes necessary to change the interface for one of those useful objects (for example, by adding a new parameter to an existing method), all the classes that use that object have to be recompiled even if they don't use that method. ISP says that's dumb: It should only be necessary to recompile a program if a method that the program actually uses changes.

To solve this problem, ISP mandates that the list of members (methods/properties/events) exposed to any particular program by some object should be limited to only the members that the program actually uses. This means that a useful object that's used by multiple programs will implement a number of interfaces, with each of the interfaces exposing only the members necessary for, potentially, one of those programs.

For example, there are probably lots of programs that use the Customer object: some programs use the Customer object as a way to update customer information; others use the object as part of creating a sales order; still, others use the Customer object as part of running a credit check on the customer. Each of these programs has different needs. The programs that update Customer information need access to the Customer's properties (name, address, preferences and so on) but don't need any of the Customer object's methods; the program that creates sales orders only needs access to those properties required by a sales order; a program that does credit checks only needs Customer object's CustomerId property and SummarizeCreditHistory method.

To implement ISP, you'd design a separate interface for each task. The interface for credit checking (which I've cleverly called ICustomerCreditCheck) might look like this:

Public Interface ICustomerCreditCheck
  Property ReadOnly CustomerId() As String
  Function SummarizeCreditHistory() As CreditEnum
End Interface

The Customer class would use that interface, along with the other interfaces, like this to expose different interfaces for different programs:

Public Class Customer()
  Implements ICustomerCreditCheck
  Implements ICustomerSalesOrder
  Implements ICustomerInfoUpdate

  ' ...actual methods/properties/events that support the interfaces' members
End Class

The program that performs credit checks would now declare any variable that uses the Customer object using the ICustomerCreditCheck interface, thereby limiting the program's access (and exposure) to just the members of the Customer object it requires:

Dim cust As ICustomerCreditCheck
cust = CustomerRepository.GetCustomerForCreditCheck("A123")
Dim creditRating As CreditEnum
creditRating = cust.SummarizeCreditHistory()
Post(cust.CustomerId, creditRating)

The benefit here is that changes to Customer members outside of the ones listed in ICustomerCreditCheck interface have no effect on the credit-checking program. You can change the definitions of those other members (modify their parameter lists, change their return type, even rename or delete the member) without having to recompile the credit-checking program.

If it turns out that the credit-checking program needs more members in the ICustomerCreditSummary interface, then you have two options: Either the ICustomerCreditCheck interface can be expanded to include the additional members, or the credit checking program can take advantage of some other interface on the Customer object that includes the members. The ability in the Microsoft .NET Framework to mix and match interfaces to give a program access to all and only the members it needs is an incentive to create interfaces with very few members.

Of course that leads to the question: How do you decide what members go into each interface?

Inverting the Process
That's where the Dependency Inversion Principle (DIP) comes into play. DIP says that the program that uses the object should decide what members the interface should have. In other words, when designing the credit-checking application, the designer of that program -- not the designer of the Customer class -- would determine what members should make up the ICustomerCreditCheck interface.

To put it more bluntly: The developer of the credit-checking application would say, "You know what methods I'd like to have access to? These two. Now, make it work."

It would then be the responsibility of the developer of the Customer class to decide how to implement the interface specified by the credit-checking application. But, in fact, it might not even be the designer of the Customer class who has to figure that out.

If you look at my last code sample there's no place where I specify the class of the object that the credit-checking program uses (though, I admit, the names CustomerRepository and GetCustomerForCreditCheck do suggest that you're getting a Customer object). The GetCustomerForCreditCheck method could return any class it pleases … as long as that class implements the ICustomerCreditCheck interface. It could be this CreditInfoManagement class, for example:

Public Class CreditInfoManagement
  Implements ICustomerCreditCheck

  Private custId As String
  Public Sub New(custId As String)
    Me.custId = custId
  End Sub
  '...code to implement the ICustomerCreditCheck interface
End Class

Of course, you may already have a class that handles summarizing customer credit info (called something like LegacyCreditSummary). But, unfortunately, you don't have access to the source code for the class so you can't slap the ICustomerCreditCheck interface on it. In that case you'd create an adapter object that implements the ICustomerCreditCheckInterface and, in that adapter's methods, instantiate LegacyCreditSummary and do whatever's necessary to make the class do the work specified in the interface. Your GetCustomerForCreditCheck method would then return that adapter object (which would, in turn, instantiate the LegacyCreditSummary object it needs to do the real work). You might also use an adapter object if the classes behind the ICustomerCreditCheck interface evolve away from the interface (if, for example, you replace one credit summary class with three or four more focussed classes).

Together, SIP and DIP trigger the reverse of the process in place when I started programming. Back in those bad old days, you wrote a class to expose the functionality you'd built into the class -- it was the responsibility of the application that used the class to work with whatever members you exposed. Often, therefore, those members reflected low-level implementation details that the class developer had struggled with when creating the class.

In this new world, though, the application that calls the members of the interface becomes the controlling owner of the interface. The interface now reflects a design that makes the application developer's (not the class developer's) life easier. The interface has become a very abstract description (more tied to the business than the underlying implementation) of what the application needs to have concretely implemented in some class. The programmers that implement these interfaces must now find a way to "make the interface that the application wants work."

The objection to this design process is that the application might specify an interface that is, in practice, impossible to implement. I think that the best response to that concern is the motto of developers everywhere: "Hey, we're programmers. We can make anything work."

But now let's carry this new world order to its logical conclusion: Who decides what functions the application needs to carry out? This is important because those decisions will determine what interfaces the classes used by the application will need (decisions that will, in turn, determine the interfaces of the classes in the next layer down, and so on down through the calling chain).

It seems to me that the answer is obvious: The UI/UX designer decides what the application has to do in order to meet the goals of the user (and the organization). This person (or team), who used to come in at the end of the process to "make it pretty," now comes in at the start to drive the design of the application and, by implication, all of the classes it calls.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube