I\'m making an open source C# library for other developers to use. My key concern is ease of use. This means using intuitive names, intuitive method usage and s
Well, first off, I think your primary concern is misguided. In my experience, designing an architecture for "ease of use", while pretty to look at with all their encapsulated functionality, tend to be highly interdependent and rigid. As an application built on such a principal grows, you will run into severe problems with dependencies (classes end up becoming directly dependent on more and more, and indirectly dependent upon, ultimately, everything in your system.) This leads to true maintenance nightmares that dwarf the "ease of use" benefits that you might be gaining.
Two of the most important rules of architecture are Separation of Concerns, and Single Responsibility. These two rules dictate things like keeping infrastructural concerns (data access, parsing) separated from business concerns (finding movies), and making sure each class you write is only responsible for one thing (representing movie information, searching for individual movies.)
Your architecture, while currently small, has violated both Single Responsibility already. Your Movie class, while it is elegant, cohesive, and easy to use, is blending two responsibilities: representing movie information, and servicing movie searches. Those two responsibilities should be in separate classes:
// Data Contract (or Data Transfer Object)
public class Movie
{
public Image Poster { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Rating { get; set; }
public string Director { get; set; }
public List Writers { get; set; }
public List Genres { get; set; }
public string Tagline { get; set; }
public string Plot { get; set; }
public List Cast { get; set; }
public string Runtime { get; set; }
public string Country { get; set; }
public string Language { get; set; }
}
// Movie database searching service contract
public interface IMovieSearchService
{
Movie FindMovie(string Title);
Movie FindKnownMovie(string ID);
}
// Movie database searching service
public partial class MovieSearchService: IMovieSearchService
{
public Movie FindMovie(string Title)
{
Movie film = new Movie();
Parser parser = Parser.FromMovieTitle(Title);
film.Poster = parser.Poster();
film.Title = parser.Title();
film.ReleaseDate = parser.ReleaseDate();
//And so an so forth.
}
public Movie FindKnownMovie(string ID)
{
Movie film = new Movie();
Parser parser = Parser.FromMovieID(ID);
film.Poster = parser.Poster();
film.Title = parser.Title();
film.ReleaseDate = parser.ReleaseDate();
//And so an so forth.
}
}
This may seem trivial, however separating the behavior from your data can become critical as a system grows. By creating an interface for your movie search service, you provide decoupling and flexibility. If you, for whatever reason, need to add another type of movie search service that provides the same functionality, you can do so without breaking your consumers. The Movie data type can be reused, your clients bind to the IMovieSearchService interface rather than a concrete class, allowing the implementations to be interchanged (or multiple implementations used simultaneously.) It is best to put the IMovieSearchService interface and Movie data type in a separate project than the MovieSearchService class.
You made a good move by writing the parser class, and keeping parsing separate from the movie search functionality. That meets the rule of Separation of Concerns. However, your approach is going to lead to difficulty. For one, it is based on static methods, which are very inflexible. Every time you need to add a new type of parser, you have to add a new static method, and update any of the code that needs to use that particular parsing type. A better approach is to utilized the power of polymorphism, and ditch static:
public abstract class Parser
{
public abstract IEnumerable Parse(string criteria);
}
public class ByTitleParser: Parser
{
public override IEnumerable Parse(string title)
{
// TODO: Logic to parse movie information by title
// Likely to return one movie most of the time, but some movies from different eras may have the same title
}
}
public class ByActorParser: Parser
{
public override IEnumerable Parse(string actor)
{
// TODO: Logic to parse movie information by actor
// This one can return more than one movie, as an actor may act in more than one movie
}
}
public class ByIdParser: Parser
{
public override IEnumerable Parse(string id)
{
// TODO: Logic to parse movie information by id
// This one should only ever return a set of one movie, since it is by a unique key
}
}
Finally, another useful principal is Dependency Injection. Rather than directly creating new instances of your dependencies, abstract their creation via something like a factory, and inject your dependencies and factories into the services that need them:
public class ParserFactory
{
public virtual Parser GetParser(string criteriaType)
{
if (criteriaType == "bytitle") return new ByTitleParser();
else if (criteriaType == "byid") return new ByIdParser();
else throw new ArgumentException("Unknown criteria type.", "criteriaType");
}
}
// Improved movie database search service
public class MovieSearchService: IMovieSearchService
{
public MovieSearchService(ParserFactory parserFactory)
{
m_parserFactory = parserFactory;
}
private readonly ParserFactory m_parserFactory;
public Movie FindMovie(string Title)
{
var parser = m_parserFactory.GetParser("bytitle");
var movies = parser.Parse(Title); // Parse method creates an enumerable set of Movies that matched "Title"
var firstMatchingMovie = movies.FirstOrDefault();
return firstMatchingMovie;
}
public Movie FindKnownMovie(string ID)
{
var parser = m_parserFactory.GetParser("byid");
var movies = parser.Parse(Title); // Parse method creates an enumerable set of Movies that matched "ID"
var firstMatchingMovie = movies.FirstOrDefault();
return firstMatchingMovie;
}
}
This improved version has several benefits. For one, it is not responsible for creating instances of the ParserFactory. That allows multiple implementations of the ParserFactory to be used. Early on, you may only search IMDB. In the future, you may wish to search other sites, and alternative parsers for alternative implementations of the IMovieSearchService interface can be provided.