在您的架构中,如何将 url 与数据库层和数据库层解耦?业务对象层
背景
我们的网站上有一些链接,其格式如下:
http:// /oursite.com/books/c_sharp_in_depth_12345。
为了处理这个问题,我们使用一个名为 Url
的简单属性:
public class Book
{
public string Url { get; set;}
}
基本上,URL 来自我们网站的域、网站的一部分(即书籍)、页面名称和用于识别的唯一 ID资源。完整的 URL 存储在数据库中。
我们不喜欢我们的数据库存储部分名称这一事实。这是网络层属性的属性,而不是书籍的属性。数据库不应该依赖于Web层。
因此,我们从 URL 中删除该部分并得到以下内容:
public class Book
{
public string UrlWithoutSection { get; set;}
}
好的,这适用于该 URL。但后来我们公司的 SEO 沙皇说我们的 URL 是错误的,只有当我们像这样重写我们的 URL 时,Google,我们的真爱,才会爱我们:
http://oursite.com/programming-books/c-sharp-in-depth-12345
呃,哦,我以为我们已经去掉了对Web层的数据库依赖,但是我们没有。事实证明,我们已经删除了对该部分的依赖,但 URL 的格式仍然存在于数据库中。我们通过将 URL 抽象为一个对象来解决这个问题:
public class OurUrl
{
public string title { get; set; }
public string id { get; set; }
}
酷,现在对 Web 层的依赖消失了。呃哦,这次我们的CEO来找我们了。我们刚刚收购了一家新公司,现在我们正在销售杂志。呃,太好了?杂志 URL 将如下所示:
http ://oursite.com/magazines/computers/stack-overflow-the-magazine/2012/01/01/12345
好的,没问题,只需创建另一个 目的。
public class OurMagazineUrl : OurUrl
{
public DateTime PublishedDate { get; set; }
// Magazine enum will have to be created.
public MagazineType Type { get; set; }
}
它确实有效,但后来我开始意识到我们计划建立一个大网站。很多网址。许多不同格式的 URL。每次创建一个新类似乎都是一件令人头疼的事情。
问题概述
如何处理 URL,以便 Web 层与业务层和数据层正确解耦?我提出了一些关于解决方案的想法:
有关问题的更多信息
我希望这有助于澄清一些困惑。
我们正在使用 ASP.Net MVC。我们使用路线。我们使用助手。我们将扁平化的 DTO 传递到 Web 层,而不是业务对象。这个问题涉及到服务层和DTO的爆炸式增长。
这主要是一个高流量的新闻网站,而不是一个业务网站。它可以有许多不同的 url,并且 url 可以随时更改。它们可能很复杂并且由管理层任意确定。
URL 示例(不是真实的,出于示例目的而编造)。
1. http://oursite.com/news/wi/politics/supreme-court/recent-ruling-someid
2. http://oursite.com/news/wi/politics/election-2012/candidate-x-takes-stand-on-issue-y-someid
3. http://oursite/com/news/politics/mayor-says-things-are-a-ok-someid
4. http://oursite.com/news/milwaukee/local-bar-named-to-HOF-someid
5. http://oursite.com/news/wi/politics/supreme-court-someid
6. http://oursite.com/news/whatever-cat-our-CEO-wants/subcat1/subcat2/etc/2011/10/31/some-story-someid
以上都是“文章”,我们有一个 Article 类。一篇文章有许多导航属性,例如 AuthorObject、RelatedLinksCollection 等。业务对象太重而无法传递给客户端,因此我们传递扁平化信息的 DTO(例如 AuthorName)。然而,上述链接可能需要不同的信息,即使它们都是“文章”。
- 需要类别、子类别、标题和 ID
- 需要类别、子类别、政治类别、标题和 ID
- 需要类别、标题和 ID
- 需要类别、标题和 ID
- 需要类别、子类别、标题和 ID
- 需要 CeoCategory、CeoSubcategory、PublishedDate、标题和 ID
静态编程语言,例如 C#,处理此问题的正常方法是创建单独的 DTO 类。您可以添加继承来减少一些代码,但最终仍然会得到多个“article”dto 类。
public class IArticleDto {
public string title { get; set; }
public string body { get; set; }
public string Category { get; set; }}
public class StateArticleDto: IArticleDto {
public string title { get; set; }
public string body { get; set; }
public string Category { get; set; }}
public string StateCode { get; set; }
public string Subcategory { get; set; }
}
public class SupremeCourtArticleDto: IArticleDto {
public string title { get; set; }
public string body { get; set; }
public string Category { get; set; }}
public string Subcategory { get; set; }
}
public class ArbitraryCeoArticleDto: IArticleDto {
//who knows
}
等等。
以任何可能的方式编写自定义 URL 的能力是不可协商的。如果一篇文章与某些内容(状态、类别等)相关,它可以成为 url 的一部分。
解决方案?
根据需要继续添加
Url
对象。多少?至少有十几个,但命名起来会很麻烦。对每个业务对象执行一个操作可以解决名称问题,但这意味着数十或数百个新对象。糟糕。IOC - 通过配置将路由模式传递到数据访问层。然后数据访问层可以创建完整的 url。 url 模式名称仍然是一个问题。
、 KeyValuePair
等进行拉入。使用
Expando
或DynamicObject
获取 url 详细信息。因此 url 将包含几个基本属性(name
和id
),但必要时可以添加其他属性。
我正在考虑使用 4),因为动态编程似乎比静态语言做得更好。然而,可能只是我最看重它,因为我喜欢玩新玩具(我以前没有使用过expando)。
由于对象爆炸,它比 1) 更好。我不确定 2) 是否适用于复杂的场景。您可以使用 DI 将简单的路由名称 + 路由信息传递到数据层,但在没有额外增益的情况下似乎更难完成。它可能不适用于复杂的路线 - 为此,我认为您需要在 UI 端有一个规则引擎。
与3)相比,我认为4)稍好一些。如果我错了,有人会纠正我,但动态类型似乎只不过是字典顶部的语法糖,但具有更清晰的代码的优点。只是一个想法。
BACKGROUND
We've got some links on our site that have formats that look like:
http://oursite.com/books/c_sharp_in_depth_12345.
To handle this, we use a simple property called Url
:
public class Book
{
public string Url { get; set;}
}
Basically, the URL comes from our website's domain, a section of the site (i.e. books), the name of the page and a unique id to identify the resource. The full URL is being stored in the database.
We don't like the fact that our database is storing the section name. That's a property of web layer property, not a property of the book. The database should not be dependent upon the web layer.
So we remove the section from the URL and get the following:
public class Book
{
public string UrlWithoutSection { get; set;}
}
OK, that works for this URL. But then our company's SEO czar says that our URL is wrong and that Google, our-one-true-love, will only love us if we re-write our urls like this:
http://oursite.com/programming-books/c-sharp-in-depth-12345
Uh, oh, I thought that we had removed the database dependency to the web layer, but we hadn't. Turns out, we had removed the dependency to the section, but the format of the URL still exists in the database. We fix this by abstracting the URL into an object:
public class OurUrl
{
public string title { get; set; }
public string id { get; set; }
}
Cool, now the dependency to the web layer is gone. Uh oh, here this time our CEO comes to us. We just bought a new company and now we're selling magazines. Uh, great? The magazine URLs are going to look like this:
http://oursite.com/magazines/computers/stack-overflow-the-magazine/2012/01/01/12345
OK, no problem, just create another object.
public class OurMagazineUrl : OurUrl
{
public DateTime PublishedDate { get; set; }
// Magazine enum will have to be created.
public MagazineType Type { get; set; }
}
It works, except then I begin to realize that we have plans for a big site. Lots of URLs. Lots of differently formatted URLs. Creating a new class every time seems like a major headache.
THE PROBLEM IN A NUTSHELL
How do you handle URLs so that the web layer is properly decoupled from the business layer and the data layer? I've come up with several thoughts about solutions:
MORE ABOUT THE PROBLEM
I'm hoping that this helps clarify some confusion.
We are using ASP.Net MVC. We use routes. We use helpers. We pass flattened DTOs to our web layer, not business objects. This problem concerns the service layer and an explosion of DTOs.
This is mainly a high traffic news site, not a line of business site. It can have many different urls and the urls can change at any time. They can be complex and arbitrarily determined by management.
URL Examples (not real, made up for example purposes).
1. http://oursite.com/news/wi/politics/supreme-court/recent-ruling-someid
2. http://oursite.com/news/wi/politics/election-2012/candidate-x-takes-stand-on-issue-y-someid
3. http://oursite/com/news/politics/mayor-says-things-are-a-ok-someid
4. http://oursite.com/news/milwaukee/local-bar-named-to-HOF-someid
5. http://oursite.com/news/wi/politics/supreme-court-someid
6. http://oursite.com/news/whatever-cat-our-CEO-wants/subcat1/subcat2/etc/2011/10/31/some-story-someid
All of the above are "articles" and we have an Article class. An article has a number of navigation properties, such as AuthorObject, RelatedLinksCollection, etc. Business objects are far too heavy to pass to a client, so we pass DTOs that flatten information (e.g. AuthorName). The above links, however can require different information, even though they are all "articles".
- needs Category, Subcategory, Title and Id
- needs Category,Subcategory, PoliticsCategory, Title and Id
- needs Category, Title and Id
- needs Category, Title and Id
- needs Category, Subcategory, Title and Id
- needs CeoCategory, CeoSubcategory, PublishedDate,Title and Id
In static programming languages, such as c#, the normal way would be to handle this would be to create separate DTO classes. You could add inheritance to reduce some of the code, but you still end up with multiple "article" dto classes.
public class IArticleDto {
public string title { get; set; }
public string body { get; set; }
public string Category { get; set; }}
public class StateArticleDto: IArticleDto {
public string title { get; set; }
public string body { get; set; }
public string Category { get; set; }}
public string StateCode { get; set; }
public string Subcategory { get; set; }
}
public class SupremeCourtArticleDto: IArticleDto {
public string title { get; set; }
public string body { get; set; }
public string Category { get; set; }}
public string Subcategory { get; set; }
}
public class ArbitraryCeoArticleDto: IArticleDto {
//who knows
}
etc.
The ability to write custom urls in any way possible in not negotiable. If an article relates to something (state, Category, etc.), it can become part of the url.
Solutions?
Continue to add
Url
objects as needed. How many? At least a dozen, but naming them will be troublesome. Doing one per business object solves the name issue, but that means dozens or hundreds of new objects. Yuck.IOC - Pass in the route pattern to the Data Access layer via configuration. The data access layer can then create a full url. The url pattern name is still a problem.
Use a
Dictionary<TKey, TValue>
,KeyValuePair<TKey, TValue>
, etc. to pull in.Use an
Expando
orDynamicObject
for the url details. So url will contain a couple basic properties (name
andid
), but other properties could be added when necessary.
I'm thinking about using 4), because it seems like something that dynamic programming does better than static languages. However, it may just be that I'm looking at it most, because I like new toys to play with (I haven't used expando before).
It's bettern than 1), because of the object explosion. I'm not sure that 2) will work for complex scenarios. You could pass in simple route name + route information to the data layer using DI, but it seems harder to accomplish with no additional gain. And it probably wouldn't work for complex routes -- for that, I think that you need to have a rules engine on the UI side.
As compared to 3), I think that 4) is slightly better. Someone correct me if I'm mistaken, but dynamic types seem to be no more than syntatic sugar on top of a dictionary, but with the advantage of cleaner code. Just a thought.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我要直言不讳地说,事实上,您提出这个问题并基于您提出的解决方案,您并没有以鸟瞰的架构视角来看待当前的问题。我不知道您的应用程序的详细细节,但是能够有一点展望并能够想象它将来可能用于什么,这将有助于 URI 设计。
您提出的解决方案都是围绕非常具体的编程细节展开的,而问题是抽象的,与编程根本无关。解决问题就是写下域中的实体、可以对它们执行的操作以及它们之间的关系。
Bill de hÓra 有一篇关于框架的 Web 资源映射标准 (这是一个缓存版本,如果真实的版本仍然会出现 HTTP 500 错误),Joe Gregorio 已经写过如何RESTify DayTrader 应该能让您正确了解如何设想应用程序的完整 URI 空间。听起来你和你的团队需要做的就是规划、绘图、思考和写作。
当设计完应用程序中 URI 的完整范围和空间后,您就可以实现它了。不过,具体操作取决于您,我建议使用 URI 模板 来定义 URI它们本身和常规代码来映射将处理 URI 的代码(无论是
Controller
、Handler
或其他)。在 ASP.NET MVC 中,配置代码是根据 编写的RouteTable 类,而在 OpenRasta 中,它是针对 ResourceSpace 类。I'm going to be blunt and say that the fact that you're asking this question and based on the solutions you're proposing, you're not taking a birds-eye architectural view of the problem at hand. I don't know the intimate details of your application, but being able to have a bit outlook and the ability to picture what it might be used to in the future will help with the URI design.
The solutions you're proposing are all revolving around very concrete, programmatic details, while the problem is abstract and not related to programming at all. Solving the problem is all about writing down the entities in your domain, the actions that are possible to perform on them and how they relate to each other.
Bill de hÓra has a blog post regarding Web resource mapping criteria for frameworks (here's a cached version, should the real one still give HTTP 500 errors) and Joe Gregorio has written about how to RESTify DayTrader that should give you the right idea around how to envision the complete URI space of your application. Planning, drawing, thinking and writing is what it sounds like you and your team need to do.
When the complete scope and space of the URIs in your application is designed, you're ready to implement it. However you then do it is up to you, and then I'd recommend using URI Templates to define the URIs themselves and regular code to map the code that will handle the URIs (be it
Controller
s,Handler
s or whatever). In ASP.NET MVC, the configuration code is written against the RouteTable class, while in OpenRasta it is done against a ResourceSpace class.您正确地认识到需要将您的域实体(书籍、杂志等)与其 URL 的任何感知脱钩。
访问一本书(例如)所需的唯一标识符是其
Id
- 在您的示例中为 12345。任何其他分类元素都应该在表示逻辑中处理,因为您可能需要为非 WWW 通道使用不同的 URI 结构吗?如果您推出多语言网站呢?将 /magazines/computers/ 保留在数据库中将是一个障碍。随着时间的推移,您的 SEO 沙皇的要求可能会发生变化,因为在搜索引擎中排名更高的技术也会发生变化。
因此,这是一个 URL 路由 的问题。
在 ASP.NET Webforms 解决方案中,您可以将以下条目添加到 Global.asax 文件中:
Books.aspx 和 Magazines.aspx 然后将收集 URL 的相关部分,其中:
当您收集了足够的信息来唯一标识时您想要显示的产品,然后您可以查询您的域/数据层以获取您需要的信息。
当新的产品类型可用时(或者您的利益相关者请求新的 URL 结构),您只需添加另一个路由即可。
You are correct to see the need to decouple your domain entities (books, magazines, etc) from any awareness of their URL.
The only indentifier you should need to access a book (for example) it its
Id
- in your example, 12345. Any other taxonomy elements should be handled in the presentation logic, as might you want a different URI structure for non-WWW channels? And if you launch the site multi-lingual? Having /magazines/computers/ persisted in the database would be a hinderance.The requirements of your SEO czar may change over time, as the techniques to rank higher in search engines change also.
As such, this is a matter of URL routing.
In an ASP.NET Webforms solution, you would add the following entries to your Global.asax file:
Books.aspx and Magazines.aspx would then collect the relevant parts of the URL, with:
and when you have collected enough information to uniquely identify the product you want to display, you can then query your domain / data tier to get the information you need.
When new product types become available (or a new URL structure is requested by your stakeholders), you simply need to add another route.
也许你可以维护一个外部 DSL 的状态机。
等等......
以及生成结构代码的 T4
我认为代码生成在一定程度上缓解了“整体类”反模式。
maybe a state machine you could maintain an external DSL.
ect....
And a T4 to generate struct code
I think the "monolithic class" antipattern is mitigated somewhat by code generation.
这是我第一次尝试#4。它可以工作,并且动态对象类型可以优雅地工作。
然后我创建了一个自定义类来处理 URI 详细信息。我添加了一个带有 name + id 的构造函数,因为我总是希望毫无例外地传递资源名称 + id。
然后,在我的DataAccess代码中
注意一点:这解决了服务层的问题,但是Web层仍然需要知道如何构建URL。这将是某种类型的帮助器类/规则引擎,它将根据属性名称和属性确定使用什么。 UriDetails 中的类型。
让我知道你的想法。
Here is my first go at #4. It works and the dynamic object type works elegantly.
Then I created a custom class to handle the URI details. I added a constructor with name + id, because I always want a resource name + id to be passed with no exceptions.
Then, in my DataAccess code
One note: this solves the problem of the service layer, but the web layer still needs to know how to build the URL. That will be some type of helper class / rules engine that will determine what's used based on the property names & types in UriDetails.
Let me know what you think.