Domain Modeling
You've already seen how it makes sense to take the real-world objects, processes, and rules from your software's subject matter and encapsulate(囊括) them in a component called a domain model. This component is the heart of your software; it's your software's universe(体系). Everything else (including controllers and views) is just a technical detail designed to support or permit interaction with the domain model. Eric Evans, a leader in domain-driven design (DDD), puts it well:
"The part of the software that specifically(专门地) solves problems from the domain model usually constitutes(组成) only a small portion of the entire software system, although its importance is disproportionate(不成比例的) to its size. To apply our best thinking, we need to be able to look at the elements of the model and see them as a system. We must not be forced to pick them out of a much larger mix of objects, like trying to identify(识别) constellations(星座) in the night sky. We need to decouple(解耦) the domain objects from other functions of the system, so we can avoid confusing(混淆) domain concepts with concepts related only to software technology or losing sight of the domain altogether in the mass of the system."
ASP.NET MVC contains no specific technology related to domain modeling (instead relying on(依靠) what it inherits(继承) from the .NET Framework and ecosystem), so this book has no chapter on domain modeling. Nonetheless, modeling is the Min MVC, so I cannot ignore the subject altogether. For the next portion of this chapter, you'll see a quick example of implementing a domain model with .NET and SQL Server, using a few of the core techniques from DDD.
An Example Domain Model
No doubt you've already experienced the process of brainstorming a domain model in your previous projects. Typically, it involves(牵涉) one or more developers, one or more business experts, a whiteboard, and a lot of cookies. After a while, you'll pull together a first-draft(草案初稿) model of the business processes you're going to automate. For example, if you were going to implement an online auctions(拍卖) site, you might get started with something like that shown in Figure 3-4.
Figure 3-4. First-draft domain model for an auctions system
This diagram(图解) indicates(指出) that the model contains a set of members who each hold a set of bids(出价), and each bid is for an item. An item can have multiple bids from different members.
Entities and Value Objects
In this example, members and items are entities, whereas(而) bids can be expressed as mere(仅仅) value objects. In case you're unfamiliar with these domain modeling terms, entities have an ongoing(不断前进的) identity throughout their lifetimes, no matter how their attributes vary(改变), whereas value objects are defined purely(完全的) by the values of their attributes. Value objects are logically immutable(不变的), because any change of attribute value would result in a different object. Entities usually have a single unique key (a primary key), whereas value objects need no such thing.
Ubiquitous Language(通用语)
A key benefit of implementing your domain model as a distinct component is the ability to design it according to the language and terminology of your choice. Strive(努力) to find and stick(插入) to a terminology for its entities, operations, and relationships that makes sense not just to developers, but also to your business (domain) experts. Perhaps you might have chosen the terms users and roles, but in fact your domain experts say agents(代理) and clearances(审核). Even when you're modeling concepts that domain experts don't already have words for, come to an agreement about a shared language—otherwise, you can't really be sure that you're faithfully(忠实的) modeling the processes and relationships that the domain expert has in mind. But why is this “ubiquitous language” so valuable?
• Developers naturally speak in the language of the code (the names of its classes, database tables, etc.). Keep code terms consistent with terms used by business experts and terms used in the application's UI, and you'll permit easier communication. Otherwise, current and future developers are more likely to misinterpret(误解) new feature requests or bug reports, or will confuse users by saying “The user has no access role for that node” (which sounds like the software is broken), instead of “The agent doesn't have clearance on that file.”
• It helps you to avoid over generalizing(过度概括) your software. We programmers have a tendency(趋势) to want to model not just one particular business reality, but every possible reality (e.g., in the auctions example, by replacing “members” and “items” with a general notion of “resources” linked not by “bids” but by “relationships”). By failing to constrain a domain model along the same lines that a particular business in a particular industry operates, you are rejecting(拒绝) any real insight into its workings, and will struggle(吃力) in the future to implement features that will seem to you like awkward(尴尬) special cases in your elegant(优雅的) metaworld. Constraints are not limitations; they are insight.
Be ready to refactor your domain model as often as necessary. DDD experts say that any change to the ubiquitous language is a change to the software. If you let the software model drift(漂流) out of sync with your current understanding of the business domain, awkwardly translating concepts in the UI layer despite(尽管) the underlying impedance(阻抗) mismatch(不搭配), your model component will become a real drain(耗尽) on developer effort(努力). Aside(除..以外) from being a bug magnet(磁铁), this could mean that some apparently(表面上看来) simple feature requests turn out to be incredibly(极端) hard to implement, and you won't be able to explain it to your clients.
Aggregates(聚合) and Simplification(简化)
Take another look at the auctions example diagram (Figure 3-4). As it stands, it doesn't offer much guidance(引导) when it comes to implementation with C# and SQL Server. If you load a member into memory, should you also load all their bids, and all the items associated with those bids, and all the other bids for those items, and all the members who have placed all those other bids? When you delete something, how far does that deletion cascade(泻) through the object graph? If you want to impose(强行) validation rules that involve relationships across objects, where do you put those rules? And this is just a trivial(琐碎,无价值的) example—how much more complicated will it get in real life?
The DDD way to break down this complexity is to arrange(安置) domain entities into groups called aggregates. Figure 3-5 shows how you might do it in the auctions example.
Figure 3-5. Auctions domain model with aggregates
Each aggregate has a root entity that defines the identity of the whole aggregate, and acts as the “boss” of the aggregate for the purposes of validation and persistence(持久化). The aggregate is a single unit when it comes to data changes, so choose aggregates that relate logically to real business processes—that is, the sets of objects that tend to change as a group (thereby embedding further insight into your domain model).
Objects outside a particular aggregate may only hold persistent references to the root entity, not to any other object inside that aggregate (in fact, ID values for non-root entities don't even have to be unique outside the scope of their aggregate). This rule reinforces(加强) aggregates as atomic(原子的) units, and ensures that changes inside an aggregate don't cause data corruption(腐烂) elsewhere.
In this example, “members” and “items” are both aggregate roots, because they have to be independently(独立地) accessible, whereas “bids” are only interesting within the context of an item. Bids are allowed to hold a reference to members, but members can't directly reference bids because that would violate(违背) the item's aggregate boundary(界限). Keeping relationships onedirectional(一个方向的), as much as possible, leads to considerable simplification of your domain model and may well reflect(反射) additional insight into the domain. This might be an unfamiliar thought if you've previously thought of a SQL database schema as being your domain model (given that all relationships in a SQL database are bidirectional), but C# can model a wider range of concepts.
A C# representation of our domain model so far looks like this:
Notice that Bid is immutable (that's as close as you'll get to a true value object), and the other classes' properties are appropriately protected. These classes respect(尊重) aggregate boundaries in that no references violate the boundary rule. Note In a sense, a C# struct (as opposed to a class) is immutable, because each assignment(分配) creates a new instance, so mutations(变化) don't affect other instances. However, for a domain value object, that's not always the type of immutability you're looking for; you often want to prevent any changes happening to any instance (after the point of creation), which means all the fields must be read-only. A class is just as good as a struct for that, and classes have many other advantages (e.g., they support inheritance).
Is It Worth Defining Aggregates?
Aggregates bring superstructure(上层建筑) into a complex domain model, adding a whole extra level of manageability. They make it easier to define and enforce data integrity(完整的) rules (an aggregate root can validate the state of the entire aggregate). They give you a natural unit for persistence, so you can easily decide how much of an object graph to bring into memory (perhaps using lazy-loading for references to other aggregate roots). They're the natural unit for cascade deletion(级联删除), too. And since data changes are atomic within an aggregate, they're an obvious unit for transactions(事务). On the other hand, they impose restrictions(约束) that can sometimes seem artificial(人造的)—because they are artificial—and compromise(违背) is painful(痛苦的). They're not a native concept in SQL Server, nor in most ORM tools, so to implement them well, your team will need discipline and effective communication.
Keeping Data Access Code in Repositories
Sooner or later you'll have to think about getting your domain objects into and out of some kind of persistent storage—usually a relational database. Of course, this concern is purely a matter of today's software technology, and isn't part of the business domain you're modeling.
Persistence is an independent concern (real architects say orthogonal(垂直) concern—it sounds much cleverer), so you don't want to mix persistence code with domain model code, either by embedding database access code directly into domain entity methods, or by putting loading or querying code into static methods on those same classes.
The usual way to keep this separation clean is to define repositories. These are nothing more than object-oriented representations of your underlying relational database store (or file-based store, or data accessed over a web service, or whatever), acting as a facade over the real implementation. When you're working with aggregates, it's normal to define a separate repository for each aggregate, because aggregates are the natural unit for persistence logic. For example, continuing the auctions example, you might start with the following two repositories (note that there's no need for a BidsRepository, because bids need only be found by following references from item instances):
Notice that repositories are concerned only with loading and saving data, and contain as little domain logic as is possible. At this point, you can fill in the code for each repository method using whatever data access strategy you prefer. You might call stored procedures, but in this example, you'll see how to use an ORM tool (LINQ to SQL) to make your job easier.
We're relying on these repositories being able to figure out what changes they need to save when we call SubmitChanges() (by spotting what you've done to its previously returned entities—LINQ to SQL and NHibernate both handle this easily), but we could instead pass specific updated entity instances to, say, a SaveMember(member) method if that seems easier for your preferred(首选的) data access technique.
Finally, you can get a whole slew of extra benefits from your repositories by defining them abstractly (e.g., as a .NET interface) and accessing them through the abstract factory pattern, or with an Inversion of Control (IoC) container. That makes it easy to test code that depends on persistence: you can supply a fake or mock repository implementation that simulates any domain model state you like. Also, you can easily swap out the repository implementation for a different one if you later choose to use a different database or ORM tool. You'll see IoC at work with repositories later in this chapter.
Using LINQ to SQL
Microsoft introduced LINQ to SQL in 2007 as part of .NET 3.5. It's designed to give you a strongly typed .NET view of your database schema and data, dramatically reducing the amount of code you need to write in common data access scenarios, and freeing you from the burden of creating and maintaining stored procedures for every type of query you need to perform. It is an ORM tool, not yet as mature(成熟的) and sophisticated as alternatives such as NHibernate, but sometimes easier to use, considering its full support for LINQ and its more thorough documentation.
In recent months, commentators(评论员) have raised fears that Microsoft might deprecate(反对) LINQ to SQL in favor of the Entity Framework. However, we hear that LINQ to SQL will be included and enhanced in .NET 4.0, so these fears are at least partly unfounded. LINQ to SQL is a great straightforward tool, so I will use it in various examples in this book, and I am happy to use it in real projects. Of course, ASP.NET MVC has no dependency on LINQ to SQL, so you're free to use alternative ORMs (such as the popular NHibernate) instead.
Most demonstrations of LINQ to SQL use it as if it were a quick prototyping tool. You can start with an existing database schema and use a Visual Studio editor to drag tables and stored procedures onto a canvas, and the tool will generate corresponding entity classes and methods automatically. You can then use LINQ queries inside your C# code to retrieve instances of those entities from a data context (it converts LINQ queries into SQL at runtime), modify them in C#, and then call SubmitChanges() to write those changes back to the database.
While this is excellent in a Smart UI application, there are limitations in multilayer architectures, and if you start from a database schema rather than an object-oriented domain model, you've already abandoned a clean domain model design.
WHAT'S A DATACONTEXT?
DataContext is your entry point to the whole LINQ to SQL API. It knows how to load, save, and query for any .NET type that has LINQ to SQL mappings (which you can add manually, or by using the visual designer).
After it loads an object from the database, it keeps track of any changes you make to that object's properties, so it can write those changes back to the database when you call its SubmitChanges() method. It's lightweight (i.e., inexpensive to construct); it can manage its own database connectivity, opening and closing connections as needed; and it doesn't even require you to remember to close or dispose of it.
There are many different ways to use LINQ to SQL, some of which are described in
Table 3-1.
Table 3-1. Possible Ways of Using LINQ to SQL
Considering the pros and cons, my preference (in a nontrivial application) is method 3 (code-first, with manual schema creation). It's not very automated, but it's not too much work when you get going. Next, you'll see how to build the auctions example domain model and repositories in this way.
Implementing the Auctions Domain Model
With LINQ to SQL, you can set up mappings between C# classes and an implied(暗示的) database schema either by decorating(装饰) the classes with special attributes or by writing an XML configuration file. The XML option has the advantage that persistence artifacts are totally removed from your domain classes, but the disadvantage that it's not so obvious at first glance(乍看). For simplicity, I'll compromise here and use attributes.
Here are the Auctions domain model classes now fully marked up for LINQ to SQL:
This code brings up several points:
• This does, to some extent, compromise the purity of the object-oriented domain model. In a perfect world, LINQ to SQL artifacts wouldn't appear in domain model code, because LINQ to SQL isn't a feature of your business domain. I don't really mind the attributes (e.g., [Column]), because they're more like metadata than code, but you do also have to use EntityRef<T> and EntitySet<T> to store associations between entities. EntityRef<T> and EntitySet<T> are LINQ to SQL's special way of describing references between entities that support lazy-loading (i.e., fetching the referenced entities from the database only on demand).
• In LINQ to SQL, every domain object has to be an entity with a primary key. That means we need an ID value on everything—even on Bid, which shouldn't really need one. Bid is therefore a value object only in the sense that it's immutable. Similarly, any foreign key in the database has to map to a [Column] in the object model, so it's necessary to add ItemID and MemberID to Bid. Fortunately, you can mark such ID values as internal, so it doesn't expose(暴露) the compromise outside the model layer.
• Instead of using Member.LoginName as a primary key, I've added a new, artificial primary key (MemberID). That will be handy if it's ever necessary to change login names. Again, it can be internal, because it's not important to the rest of the application.
• The Item.Bids collection returns a list in read-only mode. This is vital(至关重要的) for proper(适当的) encapsulation(封装), ensuring that any changes to the Bids collection happens via domain model code that can enforce appropriate business rules.
• Even though these classes don't define any domain logic (they're just data containers), they are still the right place to put domain logic (e.g., the AddBid() method on Item). We just haven't got to that bit yet.
If you want the system to create a corresponding database schema automatically, you can arrange it with a few lines of code:
Remember, though, that you'll have to perform any future schema updates manually, because CreateDatabase() can't update an existing database. Alternatively, you can just create the schema manually in the first place. Either way, once you've created a corresponding database schema, you can create, update, and delete entities using LINQ syntax and methods on System.Data.Linq.DataContext. Here's an example of constructing and saving a new entity:
dc.GetTable<Member>().InsertOnSubmit(new Member
{
LoginName = "Steve",
ReputationPoints = 0
});
dc.SubmitChanges();
And here's an example of retrieving a list of entities in a particular order:
You'll learn more about the internal workings of LINQ queries, and the new C# language features that support them, later in this chapter. For now, instead of scattering data access code all over the place, let's implement some repositories.
Implementing the Auction Repositories
Now that the LINQ to SQL mappings are set up, it's dead easy to provide a full implementation of the repositories outlined earlier:
Notice that these repositories take a connection string as a constructor parameter, and then create their own DataContext from it. This context-per-repository pattern means that repository instances won't interfere with one another, accidentally saving each other's changes or rolling them back. Taking a connection string as a constructor parameter works really well with an IoC container, because you can set up constructor parameters in a configuration file, as you'll see later in the chapter.
Now you can interact with your data store purely through the repository, like so: