2

A scenario often arises when attempting to make some existing code reusable. I introduce an interface to represent the commonality between some new feature I'm implementing and some existing functionality, but I become stuck when trying to access the existing concrete object.

Hopefully the following C# example can explain my problem.

Let's say I have the following existing code:

public class Program
{
    public static void Main()
    {
        var books = new[]
        {
            new Book { Title = "Clean Code", Author = "Robert Martin" },
            new Book { Title = "The Pragmatic Programmer", Author = "Dave Thomas" },
            new Book { Title = "Design Patterns: Elements of Reusable Object-Oriented Software", Author = "Erich Gamma" },
        };
        
        var sortedBooks = BooksSorter.Sort(books);
        BooksProcessor.Process(sortedBooks);
    }
}

internal class Book
{
    public String Title { get; set; }
    public String Author { get; set; }
}

internal static class BooksSorter
{
    public static IEnumerable<Book> Sort(IEnumerable<Book> books) 
           => books.OrderBy(book => book.Title);
}

// Imagine this class is actually dozens of classes 
// that all use the concrete Book class...
internal static class BooksProcessor
{
    public static void Process(IEnumerable<Book> books)
    {
        foreach (Book book in books)
            Console.WriteLine(book.Title + " by " + book.Author);
    }
}

My new feature involves adding support for the sorting of videos.

internal class Video
{
    public String Title { get; set; }
    public String Rating { get; set; }
}

Since we already have a class that sorts books by their title, let's extract an interface and use that instead of the concrete Book class.

internal interface IHasTitle
{
    String Title { get; }
}

internal static class Sorter
{
    public static IEnumerable<IHasTitle> Sort(IEnumerable<IHasTitle> items) => items.OrderBy(item => item.Title);
}

Now we can use the sorter to sort either books or videos. Great! But what about this code?

var sortedBooks = Sorter.Sort(books);
BooksProcessor.Process(sortedBooks);

The BooksProcessor wants concrete Book objects, but our refactoring has changed the sorter's return type.

Is there a clean way to achieve this reuse without having to cast to a concrete Book object after using the refactored Sorter class?

1
  • 1
    This has nothing to do with Interface Segregation Principle. ISP boils down to "don't make one interface that actually contains two distinct responsibilities", which isn't what's happening here.
    – Flater
    Commented Jan 18 at 1:37

3 Answers 3

14

This is solved with generics.

internal static class Sorter
{
    public static IEnumerable<T> Sort(IEnumerable<T> items) where T : IHasTitle
    {
        return items.OrderBy(item => item.Title);
    }
}

By writing it this way, the Sort method will return the same concrete type that was passed in.

Now this code will compile and run fine:

var sortedBooks = Sorter.Sort(books);
BooksProcessor.Process(sortedBooks);
4

JohnWu's approach of using templates is fine. Alternatively, instead of avoiding casts at any price, one could encapsulate them at a single place inside the BookSorter class, implemented in terms of a Sorter class:

internal static class BooksSorter
{
    public static IEnumerable<Book> Sort(IEnumerable<Book> books)
    {
         return Sorter.Sort(books).Cast<Book>();
    }
}

Note Sorter.Sort has an implicit contract to return the same objects which where given as input, so this kind of cast isn't an issue here and unlikely to produce any maintenance issues.

Code which uses the BookSorter class now does not have to worry about the casts any more, and it can stay unchanged.

2
  • That implicit contract can be relaxed by using OfType<T> instead of Cast<T>, but it will throw out all the non-Books.
    – xofz
    Commented Jan 18 at 9:02
  • @xofz: a Sort function which does not exactly spit out the objects I passed into it is clearly buggy, and I would expect an exception to be thrown then, which my code does. Using OfType<T> instead would sweep that issue under the rug.
    – Doc Brown
    Commented Jan 18 at 9:08
-2

Your Processor should not rely on the concrete type Book to extract Title and Author; instead both books and videos should contain a method self.describe() which differs between concrete classes, so that the Processor need only know that it receives a Describable and not necessarily a Book.

1
  • 1
    This is IMHO missing the point. The OP wants a generic Sorter, not a generic "Processor". The "BookProcessor" is just an example of what could happen with the sorted result in case of books; videos might be processed completely differently.
    – Doc Brown
    Commented Jan 17 at 6:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.