长内容分页的面向对象实现
背景:
现在在网上看到一页数屏的超长文章已经显得太不友好了,因此出现了很多对超长文章进行分页的做法。主流的做法都是通过某种方式将原文章读取后再对其截取一定长度的内容,有的甚至手动插入特殊的记号作为分页的标识,这些做法在逻辑上破坏了文章的完整性,更严重的,因为无法处理双字节字符和单字节字符而导致内容不再可读,另外,从数据库搜索性能的角度来看,超长内容通常存储在NTEX字段,而这种字段的数据是很难进行搜索的。
在这里,我认为文章的分页要保留其完整性、可读性及可搜索性,并且提供更自然直观的分页录入界面,如同在WORD中编辑多页内容一样,而不是一次录入超长的内容并在读取时做截断处理。
一点点面向对象分析:
重新思考一篇文章和一页文章的关系:仅有一页内容的文章自成一篇,而有多页内容的文章也是一篇。我们将一篇文章逻辑上定义为一个Article类型,每一页内容也是一个Article类型,将它们集合到一起而成为一篇带有分页的文章,每一个分页拥有自己的内容和标题,一旦它们连贯起来就是完整的长篇内容。
Article对象设计:
public class Article : IArticle
{
#region 字段
private ISet<IArticle> m_Pages;
private IArticle m_SeparatedFrom;
private Int32 m_PageNo;
private String m_Title = String.Empty;
private String m_SecondTitle = String.Empty;
private String m_Author = String.Empty;
private String m_Keywords = String.Empty;
private String m_TopicImageSrc = String.Empty;
private String m_Body = string.Empty;
private DateTime m_PostDate = DateTime.Now;
private Guid m_ID;
#endregion
#region 属性
#region IArticle成员
/// <summary>
/// 获取或设置预设关键词以逗号隔(全角半角都可以)
/// </summary>
public String Keywords { get { return m_Keywords; } set { m_Keywords = value; } }
/// <summary>
/// 获取提交日期,更新无效
/// </summary>
public DateTime PostDate { get { return m_PostDate; } }
/// <summary>
/// 获取或设置文章ID,更新无效
/// </summary>
public Guid ID { get { return m_ID; } set { m_ID = value; } }
/// <summary>
/// 获取或设置文章头图链接
/// </summary>
public String TopicImageSrc { get { return m_TopicImageSrc; } set { m_TopicImageSrc = value; } }
/// <summary>
/// 获取或设置主标题
/// </summary>
public String Title
{
get { return m_Title; }
set
{
if (value == String.Empty) { throw new ArgumentNullException("Title", "主标题不能为空"); }
m_Title = value;
}
}
/// <summary>
/// 获取或设置副标题
/// </summary>
public String SecondTitle { get { return m_SecondTitle; } set { m_SecondTitle = value; } }
/// <summary>
/// 获取或设置分页内容
/// </summary>
public ISet<IArticle> Pages
{
get { return m_Pages; }
set { m_Pages = value; }
}
/// <summary>
/// 获取或设置当前内容(页)源自哪一篇,由该篇的第一页为依据
/// </summary>
public IArticle SeparatedFrom { get { return m_SeparatedFrom; } set { m_SeparatedFrom = value; } }
/// <summary>
/// 获取或设置分页编号
/// </summary>
public Int32 PageNo
{
get { return m_PageNo; }
set
{
if (value < 1) { throw new ArgumentOutOfRangeException("PageNo", "页号必须大于0"); }
m_PageNo = value;
}
}
/// <summary>
/// 获取或设置内容主体
/// </summary>
public String Body
{
get { return m_Body; }
set
{
if (value == String.Empty) { throw new ArgumentNullException("body", "文章内容不能为空"); }
if (value.Length > 4000) { throw new ArgumentOutOfRangeException("body", "文章内容篇幅不能超过4000(一个汉字为2个字母长度),请尝试将文章分为多页"); }
m_Body = value;
}
}
/// <summary>
/// 获取或设置原文作者
/// </summary>
public String Author { get { return m_Author; } set { m_Author = value; } }
#endregion
#endregion
#region 构造
public Article() { }
/// <summary>
/// 构造
/// </summary>
/// <param name="pageNo">文章页编码</param>
/// <param name="title">主标题</param>
/// <param name="secondTitle">副标题</param>
/// <param name="author">原文作者</param>
/// <param name="keywords">预设关键词以逗号隔(全角半角都可以)</param>
/// <param name="topicImageSrc">主题头图</param>
/// <param name="body">内容主体</param>
/// <param name="id">GUID</param>
public Article
(
Int32 pageNo,
String title,
String secondTitle,
String author,
String keywords,
String topicImageSrc,
String body,
Guid id,
)
{
if (pageNo < 1) { throw new ArgumentOutOfRangeException("pageNo", "页号必须大于0"); }
if (title == String.Empty) { throw new ArgumentNullException("title", "主标题不能为空"); }
if (poster == String.Empty) { throw new ArgumentNullException("author", "提交人不能为空"); }
if (body == String.Empty) { throw new ArgumentNullException("body", "文章内容不能为空"); }
if (body.Length > 4000) { throw new ArgumentOutOfRangeException("body", "文章内容篇幅不能超过4000(一个汉字为2个字母长度),请尝试将文章分为多页"); }
m_PageNo = pageNo;
m_Title = title;
m_SecondTitle = secondTitle;
m_Author = author;
m_Keywords = keywords;
m_TopicImageSrc = topicImageSrc;
m_Body = body;
m_ID = id;
}
#endregion
}
通过NHibernate进行映射:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false">
<class name="DAL.Model.Article, DataAccessLayer" table="Article">
<id name="ID" column="ID" type="Guid" length="36" >
<generator class="guid" />
</id>
<!--注意SeparatedFromID在数据库中是约束自Article表自身的ID列-->
<many-to-one name="SeparatedFrom" class="DAL.Model.Article" column="SeparatedFromID" />
<!--长内容分页,每个分页也是一个Article实例-->
<set name="Pages" table="Article" generic="true" cascade="all-delete-orphan" >
<key column="SeparatedFromID" foreign-key="FK-Article-Article" />
<one-to-many class="DAL.Model.Article, DataAccessLayer" />
</set>
<property name="Title" column="Title" type="String" length="200" not-null="true" />
<property name="SecondTitle" column="SecondTitle" length="200"/>
<property name="PageNo" column="PageNo" type="Int32"/>
<property name="Author" column="Author" type="String" length="50"/>
<property name="Keywords" column="Keywords" type="String" length="300"/>
<property name="Body" column="Body" type="String" length="4000" not-null="true"/>
<property name="PostDate" column="PostDate" type="DateTime" update="false" access="nosetter.pascalcase-m-underscore"/>
<property name="TopicImageSrc" column="TopicImageSrc" type="String" length="200"/>
</class>
</hibernate-mapping>
最后一点提示:
通过上面NHibernate的映射,上述对象完全可以工作在持久化的基础之上,因为Article表实现了自身的级联约束,所以在删除完整的一篇文章时,将第一页做最后的删除,因为其它分页会引用到它。
现在在网上看到一页数屏的超长文章已经显得太不友好了,因此出现了很多对超长文章进行分页的做法。主流的做法都是通过某种方式将原文章读取后再对其截取一定长度的内容,有的甚至手动插入特殊的记号作为分页的标识,这些做法在逻辑上破坏了文章的完整性,更严重的,因为无法处理双字节字符和单字节字符而导致内容不再可读,另外,从数据库搜索性能的角度来看,超长内容通常存储在NTEX字段,而这种字段的数据是很难进行搜索的。
在这里,我认为文章的分页要保留其完整性、可读性及可搜索性,并且提供更自然直观的分页录入界面,如同在WORD中编辑多页内容一样,而不是一次录入超长的内容并在读取时做截断处理。
一点点面向对象分析:
重新思考一篇文章和一页文章的关系:仅有一页内容的文章自成一篇,而有多页内容的文章也是一篇。我们将一篇文章逻辑上定义为一个Article类型,每一页内容也是一个Article类型,将它们集合到一起而成为一篇带有分页的文章,每一个分页拥有自己的内容和标题,一旦它们连贯起来就是完整的长篇内容。
Article对象设计:
public class Article : IArticle
{
#region 字段
private ISet<IArticle> m_Pages;
private IArticle m_SeparatedFrom;
private Int32 m_PageNo;
private String m_Title = String.Empty;
private String m_SecondTitle = String.Empty;
private String m_Author = String.Empty;
private String m_Keywords = String.Empty;
private String m_TopicImageSrc = String.Empty;
private String m_Body = string.Empty;
private DateTime m_PostDate = DateTime.Now;
private Guid m_ID;
#endregion
#region 属性
#region IArticle成员
/// <summary>
/// 获取或设置预设关键词以逗号隔(全角半角都可以)
/// </summary>
public String Keywords { get { return m_Keywords; } set { m_Keywords = value; } }
/// <summary>
/// 获取提交日期,更新无效
/// </summary>
public DateTime PostDate { get { return m_PostDate; } }
/// <summary>
/// 获取或设置文章ID,更新无效
/// </summary>
public Guid ID { get { return m_ID; } set { m_ID = value; } }
/// <summary>
/// 获取或设置文章头图链接
/// </summary>
public String TopicImageSrc { get { return m_TopicImageSrc; } set { m_TopicImageSrc = value; } }
/// <summary>
/// 获取或设置主标题
/// </summary>
public String Title
{
get { return m_Title; }
set
{
if (value == String.Empty) { throw new ArgumentNullException("Title", "主标题不能为空"); }
m_Title = value;
}
}
/// <summary>
/// 获取或设置副标题
/// </summary>
public String SecondTitle { get { return m_SecondTitle; } set { m_SecondTitle = value; } }
/// <summary>
/// 获取或设置分页内容
/// </summary>
public ISet<IArticle> Pages
{
get { return m_Pages; }
set { m_Pages = value; }
}
/// <summary>
/// 获取或设置当前内容(页)源自哪一篇,由该篇的第一页为依据
/// </summary>
public IArticle SeparatedFrom { get { return m_SeparatedFrom; } set { m_SeparatedFrom = value; } }
/// <summary>
/// 获取或设置分页编号
/// </summary>
public Int32 PageNo
{
get { return m_PageNo; }
set
{
if (value < 1) { throw new ArgumentOutOfRangeException("PageNo", "页号必须大于0"); }
m_PageNo = value;
}
}
/// <summary>
/// 获取或设置内容主体
/// </summary>
public String Body
{
get { return m_Body; }
set
{
if (value == String.Empty) { throw new ArgumentNullException("body", "文章内容不能为空"); }
if (value.Length > 4000) { throw new ArgumentOutOfRangeException("body", "文章内容篇幅不能超过4000(一个汉字为2个字母长度),请尝试将文章分为多页"); }
m_Body = value;
}
}
/// <summary>
/// 获取或设置原文作者
/// </summary>
public String Author { get { return m_Author; } set { m_Author = value; } }
#endregion
#endregion
#region 构造
public Article() { }
/// <summary>
/// 构造
/// </summary>
/// <param name="pageNo">文章页编码</param>
/// <param name="title">主标题</param>
/// <param name="secondTitle">副标题</param>
/// <param name="author">原文作者</param>
/// <param name="keywords">预设关键词以逗号隔(全角半角都可以)</param>
/// <param name="topicImageSrc">主题头图</param>
/// <param name="body">内容主体</param>
/// <param name="id">GUID</param>
public Article
(
Int32 pageNo,
String title,
String secondTitle,
String author,
String keywords,
String topicImageSrc,
String body,
Guid id,
)
{
if (pageNo < 1) { throw new ArgumentOutOfRangeException("pageNo", "页号必须大于0"); }
if (title == String.Empty) { throw new ArgumentNullException("title", "主标题不能为空"); }
if (poster == String.Empty) { throw new ArgumentNullException("author", "提交人不能为空"); }
if (body == String.Empty) { throw new ArgumentNullException("body", "文章内容不能为空"); }
if (body.Length > 4000) { throw new ArgumentOutOfRangeException("body", "文章内容篇幅不能超过4000(一个汉字为2个字母长度),请尝试将文章分为多页"); }
m_PageNo = pageNo;
m_Title = title;
m_SecondTitle = secondTitle;
m_Author = author;
m_Keywords = keywords;
m_TopicImageSrc = topicImageSrc;
m_Body = body;
m_ID = id;
}
#endregion
}
通过NHibernate进行映射:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false">
<class name="DAL.Model.Article, DataAccessLayer" table="Article">
<id name="ID" column="ID" type="Guid" length="36" >
<generator class="guid" />
</id>
<!--注意SeparatedFromID在数据库中是约束自Article表自身的ID列-->
<many-to-one name="SeparatedFrom" class="DAL.Model.Article" column="SeparatedFromID" />
<!--长内容分页,每个分页也是一个Article实例-->
<set name="Pages" table="Article" generic="true" cascade="all-delete-orphan" >
<key column="SeparatedFromID" foreign-key="FK-Article-Article" />
<one-to-many class="DAL.Model.Article, DataAccessLayer" />
</set>
<property name="Title" column="Title" type="String" length="200" not-null="true" />
<property name="SecondTitle" column="SecondTitle" length="200"/>
<property name="PageNo" column="PageNo" type="Int32"/>
<property name="Author" column="Author" type="String" length="50"/>
<property name="Keywords" column="Keywords" type="String" length="300"/>
<property name="Body" column="Body" type="String" length="4000" not-null="true"/>
<property name="PostDate" column="PostDate" type="DateTime" update="false" access="nosetter.pascalcase-m-underscore"/>
<property name="TopicImageSrc" column="TopicImageSrc" type="String" length="200"/>
</class>
</hibernate-mapping>
最后一点提示:
通过上面NHibernate的映射,上述对象完全可以工作在持久化的基础之上,因为Article表实现了自身的级联约束,所以在删除完整的一篇文章时,将第一页做最后的删除,因为其它分页会引用到它。