我正在为其他开发人员使用开源C#库。我关注的主要问题是易用性。这意味着使用直观的名称,直观的方法使用等。
这是我第一次与其他人一起做事,所以我真的很关心架构的质量。另外,我不介意学习一两件事。 :)
我有三节课: 下载程序,解析程序和电影
我当时认为最好只公开我的库的Movie类,并且让Downloader和Parser在调用时保持隐藏状态。
最终,我看到我的图书馆被这样使用了。
使用FreeIMDB;
public void Test()
{
var MyMovie = Movie.FindMovie("The Matrix");
//Now MyMovie would have all it's fields set and ready for the big show.
}
你能否回顾一下我的计划方式,并指出我所做的任何错误判断,以及我可以改进的地方。
请记住,我主要关心的是易用性。
Movie.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace FreeIMDB
{
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<string> Writers { get; set; }
public List<string> Genres { get; set; }
public string Tagline { get; set; }
public string Plot { get; set; }
public List<string> Cast { get; set; }
public string Runtime { get; set; }
public string Country { get; set; }
public string Language { get; set; }
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.
}
}
}
Parser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using HtmlAgilityPack;
namespace FreeIMDB
{
/// <summary>
/// Provides a simple, and intuitive way for searching for movies and actors on IMDB.
/// </summary>
class Parser
{
private Downloader downloader = new Downloader();
private HtmlDocument Page;
#region "Page Loader Events"
private Parser()
{
}
public static Parser FromMovieTitle(string MovieTitle)
{
var newParser = new Parser();
newParser.Page = newParser.downloader.FindMovie(MovieTitle);
return newParser;
}
public static Parser FromActorName(string ActorName)
{
var newParser = new Parser();
newParser.Page = newParser.downloader.FindActor(ActorName);
return newParser;
}
public static Parser FromMovieID(string MovieID)
{
var newParser = new Parser();
newParser.Page = newParser.downloader.FindKnownMovie(MovieID);
return newParser;
}
public static Parser FromActorID(string ActorID)
{
var newParser = new Parser();
newParser.Page = newParser.downloader.FindKnownActor(ActorID);
return newParser;
}
#endregion
#region "Page Parsing Methods"
public string Poster()
{
//Logic to scrape the Poster URL from the Page element of this.
return null;
}
public string Title()
{
return null;
}
public DateTime ReleaseDate()
{
return null;
}
#endregion
}
}
你们是否认为我正朝着一条好路走去,或者我是否会为以后的世界做好准备?
我最初的想法是将下载,解析和实际填充分开,以便轻松拥有可扩展的库。想象一下,如果有一天网站改变了HTML,那么我只需修改解析类而不触及Downloader.cs或Movie.cs类。
感谢阅读和帮助!
还有其他想法吗?
答案 0 :(得分:5)
您的API基本上是静态的,这意味着您将来可能会遇到可维护性问题。这是因为静态方法实际上是单例,which have some significant drawbacks。
我建议争取更多基于实例的解耦方法。这自然会将每个操作的定义与其实现分开,为可扩展性和配置留出空间。 API的易用性不仅取决于其公共表面,还取决于其适应性。
以下是我将如何设计此系统。首先,定义负责获取电影的内容:
public interface IMovieRepository
{
Movie FindMovieById(string id);
Movie FindMovieByTitle(string title);
}
接下来,定义负责下载HTML文档的内容:
public interface IHtmlDownloader
{
HtmlDocument DownloadHtml(Uri uri);
}
然后,定义使用下载程序的存储库实现:
public class MovieRepository : IMovieRepository
{
private readonly IHtmlDownloader _downloader;
public MovieRepository(IHtmlDownloader downloader)
{
_downloader = downloader;
}
public Movie FindMovieById(string id)
{
var idUri = ...build URI...;
var html = _downloader.DownloadHtml(idUri);
return ...parse ID HTML...;
}
public Movie FindMovieByTitle(string title)
{
var titleUri = ...build URI...;
var html = _downloader.DownloadHtml(titleUri);
return ...parse title HTML...;
}
}
现在,您需要下载电影的任何地方,您可以完全依赖IMovieRepository
而不直接与其下的所有实施细节相关联:
public class NeedsMovies
{
private readonly IMovieRepository _movies;
public NeedsMovies(IMovieRepository movies)
{
_movies = movies;
}
public void DoStuffWithMovie(string title)
{
var movie = _movies.FindMovieByTitle(title);
...
}
}
此外,您现在可以轻松测试解析逻辑,而无需进行Web调用。只需保存HTML并创建一个下载器,将其提供给存储库:
public class TitleHtmlDownloader : IHtmlDownloader
{
public HtmlDocument DownloadHtml(Uri uri)
{
return ...create document from saved HTML...
}
}
[Test]
public void ParseTitle()
{
var movies = new MovieRepository(new TitleHtmlDownloader());
var movie = movies.GetByTitle("The Matrix");
Assert.AreEqual("The Matrix", movie.Title);
...assert other values from the HTML...
}
答案 1 :(得分:1)
以下是一些建议,没什么大不了的,只是需要考虑的一些事情。
我知道您希望保持API最小化,从而使Parser和Downloader成为私有/内部,但您可能还是想考虑将它们公之于众。最大的原因是,由于这将是一个开源项目,你很可能会得到那些黑客,好吧,黑客。如果他们想要做一些你所提供的API不直接支持的东西,他们会很感激你可以让他们自己做这些。使“标准”用例尽可能简单,但也让人们可以轻松地随心所欲地做任何事情。
看起来您的Movie类和Parser之间存在一些数据重复。具体来说,解析器正在获取由Movie定义的字段。将Movie作为数据对象(只是属性)似乎更有意义,并让Parser类直接对其进行操作。所以你的解析器FromMovieTitle
可以返回一个Movie而不是Parser。现在提出了如何处理Movie class FindMovie
和FindKnownMovie
上的方法的问题。我会说你可以创建一个MovieFinder
类,其中包含这些方法,他们会利用Parser返回一部电影。
一般来说,如果您始终牢记Single Responsibility Principle和Open/Closed Principle以及保持标准使用容易的目标,那么您最终应该找到一些人们会发现易于使用的东西我们想到了支持,并且很容易扩展你没有的东西。
答案 2 :(得分:0)
我只会暴露有意义的物品。为您编码,最终结果是电影信息。下载器和解析器是无用的,除非用于获取电影信息,因此没有理由公开它们。
同样在你的Movie课程中,我只会将信息设为Getable,而不是Setable。该类没有“保存”功能,因此没有理由在获得信息后对其进行编辑。
除此之外,如果这是针对其他人的,我会评论每个类,成员和每个公共/私有类变量的用途。对于Movie类,我可能会在类注释中包含一个如何使用它的示例。
最后一件事,如果两个私有类中存在错误,则需要以某种方式通知Movie类的用户。可能是一个名为success的公共bool变量?
在个人偏好笔记中,对于您的Movie类,我会将您的两个函数作为构造函数,以便我可以按如下方式构建类。
电影myMovie =新电影(“名字”); 要么 电影myMovie =新电影(1245);
答案 3 :(得分:0)
嗯,首先,我认为你的主要担忧是错误的。根据我的经验,设计一个“易用性”的架构,虽然很好地看待所有封装的功能,但往往是高度相互依赖和僵化的。随着基于此类主体构建的应用程序的增长,您将遇到严重的依赖关系问题(类最终会直接依赖于越来越多,并最终间接依赖于系统中的所有内容。)这会导致真正的维护噩梦,使您可能获得的“易用性”优势相形见绌。
两个最重要的架构规则是Separation of Concerns和Single Responsibility。这两条规则决定了将基础设施问题(数据访问,解析)与业务问题(查找电影)分开,并确保您编写的每个类只负责一件事(代表电影信息,搜索单个电影)。
您的体系结构虽然规模较小,但已经违反了单一责任。您的电影课虽然优雅,有凝聚力且易于使用,但它混合了两个职责:表示电影信息和服务电影搜索。这两项责任应该在不同的类别中:
// 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<string> Writers { get; set; }
public List<string> Genres { get; set; }
public string Tagline { get; set; }
public string Plot { get; set; }
public List<string> 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.
}
}
这看似微不足道,但随着系统的发展,将行为与数据分开会变得至关重要。通过为电影搜索服务创建界面,您可以提供解耦和灵活性。如果您出于某种原因需要添加另一种提供相同功能的电影搜索服务,您可以在不打破消费者的情况下这样做。 Movie数据类型可以重用,您的客户端绑定到IMovieSearchService接口而不是具体类,允许实现互换(或同时使用多个实现。)最好将IMovieSearchService接口和Movie数据类型放在一个单独的项目比MovieSearchService类。
您通过编写解析器类并将解析与电影搜索功能分开来做出了很好的举动。这符合关注分离规则。但是,您的方法将导致困难。首先,它基于静态方法,这些方法非常不灵活。每次需要添加新类型的解析器时,都必须添加一个新的静态方法,并更新需要使用该特定解析类型的任何代码。一种更好的方法是利用多态性的力量和沟渠静态:
public abstract class Parser
{
public abstract IEnumerable<Movie> Parse(string criteria);
}
public class ByTitleParser: Parser
{
public override IEnumerable<Movie> 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<Movie> 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<Movie> 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
}
}
最后,另一个有用的原则是依赖注入。而不是直接创建依赖项的新实例,而是通过类似工厂的方式抽象它们的创建,并将依赖项和工厂注入需要它们的服务中:
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;
}
}
这个改进版本有几个好处。首先,它不负责创建ParserFactory的实例。这允许使用ParserFactory的多个实现。在早期,您可能只搜索IMDB。将来,您可能希望搜索其他站点,并且可以提供替代解析器以用于IMovieSearchService接口的替代实现。