Pragmatism and the Nontraditional Approach


By Ingemar Lundberg

When Jimmy first asked me if I was willing to write this "guest segment" I immediately said "Yes, of course." I must admit I was flattered. Not surprisingly, I became doubtful just a short while later. What do I have to say? Can I be precise enough to fit it in a very limited space? And above all, how can my .NET architecture fit into the Domain Model concept?

I will attempt to give you a general description of the architecture I've built for my former employer, which, without shame, I will call my architecture from now on. It is however not the architecture I'm using nowadays. I will also address some of the issues in Jimmy's exercise that I find interesting with regards to the strengths and weaknesses in my architecture. I can't possibly give you the whole picture in this limited space, but because the intention of this section is to show you that there is more than one way to skin a cat, I hope this brief description is sufficient enough to fulfill its purpose. When I talk about how things are done, please remember that the context is this particular architecture and not a general dogma.

Background and History

I need to bore you with some background and history in an attempt to explain why things in my architecture are the way they are. I want to point out two or three things that have been the main influence on the state of things. First, you need to know that before .NET I had designed a MTS/COM+ architecture. With the arrival of .NET, the (home-brewed) concepts from the old architecture weren't more than a few years old, and thus were barely "cemented" in people's (developers') minds and too expensive to lose.[2] The second thing you need to know is that my architecture isn't necessarily "the best" I could think of. I hope it is a good balance between state of the art design/development techniques, the skills of the personnel using it, and the quality/cost ratio requested.

[2] If you work as a "technology leader" in an IT department, I'm sure you've noticed that when you are ready to move on to the next thing, people around you haven't really grasped what you've been talking about lately.

Overview of the Architecture

What then are the concepts of my architecture? Those of you familiar with MTS/ COM+ have been thoroughly taught the concepts of stateless OO programming (a contradiction in terms?). The process object [Ewald TxCOM+] is a stateless object representing a single, entire Business Object (BO) in one instance. It contains the full set of operations for one or a set of business objects of a particular business object type. The statelessness grants you this possibility.

The basis in the architecture is a framework of collaborating classes. It is usable by itself, but to be really effective it has a companion tool, BODef (for a snapshot of a part of it, look ahead to Figure A-11), where you define your business objects and generate boilerplate code. The code generator is more than a wizard because it lets you generate time after time without losing manual code additions. It uses the Generation Gap pattern [Vlissides Pattern Hatching] to accomplish this. In Figure A-5, you can see one (partial) definition of the BOs of the problem at hand.

Figure A-5. BOs for the example


The "standard" way of dividing application classes is shown in Figure A-6. All packages/assemblies, except those marked with <<framework>>, are application specific. The BoSupport assembly is supposed to be available not only in the middle tier but also in Remoting clients. The rich client required in Jimmy's problem challenge is going to communicate with the middle tier via Remoting.[3]

[3] Jimmy's note: As I mentioned in the introduction to this appendix, an application server was in the requirement from the beginning but was removed later on.

Figure A-6. Overall architecture


The Service assembly should contain at least one façade object that implements some basic interfaces to support basic CRUD, IBvDBOReq being the most important; see Figure A-7.

Figure A-7. Architecture for the facade


This façade isn't generated; instead, you inherit all base functionality (from FacadeBase), including the implementation of the mentioned interfaces.

When it comes to persistent storage, the presence of a relational database system (RDBMS) is as sure as the presence of the file system itself at the site. In fact, the RDBMS was an unspoken requirement, and if I wanted proper backup handling of my applications data, I'd better use one. This is an important statement. This is why I don't bother to pretend that we didn't use an RDBMS.

Retrieval of Data

It is possible to return data from the middle tier to the client in any format: DataTable, DataSet, strongly typed DataSet, XML or serialized object graphs, to mention a few (or maybe most). The simplest and most common return format (in my case) is the DataTable. To return a bunch of "business object data," you simply return each as a row.

Business Object Placeholder

A row in the DataTable can easily be converted to a business object placeholder, BoPlaceholder. The BoPlaceholder class, located in the BoSupport assembly, is usable in its own right, but it's also the base class of specific business object placeholders.

The specific placeholders, one of them being POrderPh (purchase order) in Figure A-8, are generated by BODef. The Impl class is generated each time you choose generate. The leaf class is only generated once, which is pretty much the essence of the Generation Gap pattern.

Figure A-8. Usage of BoPlaceholder


You might think of these placeholder objects as information carriers or data transfer objects, but they might be used in more complex behavior scenarios. And, yes, the BoPlaceholder derivates are in the BoSupport assembly, which is supposed to be present at both the middle tier and in the client tier (the rich/fat client).

You can return a DataTable from the middle tier to the client, leaving it to transform from DataRow instances to BoPlaceholder instances. You can also return a set (ArrayList, for instance) of BoPlaceholders to be used directly in the client. BoPlaceholder is serializable. However, remember that you can find loads of samples out there of how to databind a DataTable, but samples of data bound BoPlaceholders, although possible, are rare.

Process Objects

The final execution and enforcement of the business rules is a job for the core business objects, also known as the process objects. These objects are stateless and very short-lived. You can see the POrder process object in Figure A-9 and see that they follow the generation gap pattern. These objects are implemented in the Core assembly. Because I consider authorization to be a core part of business rules, it's no surprise that roles are specified on (public) methods of these objects.

Figure A-9. Locating custom rules in POrder, the TakeOrder() method in this example


Despite the fact that business rule enforcement is the responsibility of the core objects, one of them being POrder in Figure A-9, it is still very possible to implement the business rules in the BoSupport assembly and thus make them available in the rich client. However, when I do that I always make sure I double-check the rules in the process objects before persisting the changes. You see, it is very possible that a client has overridden a rule, say the upper order limit, perhaps via reflection, before submitting it to the façade object in the middle tier.

The process objects are often handed BoPlaceholders and are "hidden" behind a façade object. The process object and the placeholder, such as POrder and POrderPh, are conceptually parts of the same (logical) business object.

Metadata

As you might have noticed in some of the previous figures, there's plenty of meta-data available in the framework classes. A more complete picture of the meta-data, although not explained in depth, is found in Figure A-10. The metadata can give you some dynamics, both on data storage handling and UI handling.

Figure A-10. A complete picture of the metadata


SQL statements in the middle tier are dynamically constructed from meta-data, and you get a fairly good and very well-proven implementation. Should you discover bottlenecks, you have the freedom to implement the SQL handling your own way, such as by using stored procedures.

Jimmy's Order Application Exercise

Let's turn our interest to Jimmy's problem, the order-handling application. I imagine that the user of the order placement functionality (application) is someone speaking with (serving) a customer over the phone. Let's call this person an order taker. The fat client sort of implies this, if you see my point. One could easily imagine the desire for an e-commerce application, if not now then in the future. I don't think this has a big impact on the design should we go that way, but still, my interpretation of the scenario will affect, for instance, what roles we need in the system. I have the roles User and Admin. User can do most stuff and Admin can do just about anything. (With an e-commerce app you might need, say, a PublicUser role.)

I've cut out most real-world attributes to keep it compact. Most of that stuff, such as the customer's invoice address and order delivery address, isn't particularly difficult to handle. In the early phases of real application development, I often concentrate on the relationship between objects (and on their behavior and collaboration, of course) rather than on specific attributes anyway.

Simple Registry Handling

This problem demands the handling of customer data. This is what I would call a bread and butter registry handling functionality. I just have to show you my solution to this because it reveals some of my ideas and the potential for including metadata for the business objects.

Figure A-11 is a snapshot from my business object definition tool, BODef, showing you the Customer object. You can see that besides the field name and its type, there's more information attached to a field.

Figure A-11. The Customer class in BODef


In Figure A-12, which shows a form in the order application, the labels are taken from the metadata. With this grid you can add, edit and delete customers. The code to achieve this is rather compact (see the following code listing). The magic is all in the generic TableHandler, a class that takes advantage of the meta-data of the Customer BO as well as of the generic façade interface, IBvDBOReq, and the BoPlaceholder and DataRow relationship of the architecture.

Figure A-12. A default implementation of the Customer


public CustomerFormList() {   InitializeComponent();   _tabHndlr = new TableHandlerGuid(SvcFactory.Facade,     CustomerPh.FieldInfoBrowser.BOName);   _tabHndlr.TableChanged += new     TableHandler.TableHandlerEventHandler     (TableHandlerEventHandler);   _tabHndlr.HookUpGrid(grd);   _tabHndlr.FillGrid(); } TableHandler _tabHndlr; string[] confirmString = new string[] {   "Do you want to add Customer?",   "Do you want to save changes to Customer?",   "Do you want to delete Customer?" }; void TableHandlerEventHandler(object sender,   TableHandler.TableHandlerChangedEventArgs args) {   args.AcceptChanges = MessageBox.Show(     confirmString[(int)args.TableHandlerChangeType],     "Confirm", MessageBoxButtons.YesNo) == DialogResult.Yes; }


There's no point in describing each little detail. You might want to take away ideas of what you can do with metadata. And not shown here, it's possible to have metadata describing the relationships between business objects, too. (Association objects are first-class citizens in my architecture.)

Order Placement Business Rules

You can sort of read between the lines in Jimmy's problem description and see that this is where he wants us guest writers to dig in. Sure, I'm game.

Let me first tell you what I choose not to comment on in the problem description, which isn't the same as not having it implemented. The search problem is ignored. In my priority list, it was disqualified due to lack of space. The same goes for the per customer order listing. The creditability of a new customer went the same way. I have classified the unique number of the order as a minor challenge in my fake scenario. I simply assign it at the time when an order is submittedthe order taker gets it returned at that point and doesn't need it before then.

Good support for the order taking/fill-in process is a given. I pictured a "builder"[4] that's used to manipulate the order form (think piece of paper), the order form being the metaphor I'll reason around. I also imagined this builder as a serializable object that is, upon completion, submitted from the client to the middle tier.

[4] You can, for instance, think of the StringBuilder class that is used to build a string.

The multi-user aspect adds a few tough issues to solve. For instance, when the order taker talks to the presumptive customer and the price of an item is changed, should he get to know that immediately? It doesn't help that the price is checked with the item repository because if you retrieve the price at 10:55 a.m. and list it on the screen, it might very well be changed at 10:56 a.m. The same problem exists for your stock; is the item available or do you need to order it from your supplier?

One way to solve this problem is to have the middle tier notify all clients (order takers) as soon as there is a price or stock change so that the client can take measures for this. However, this would complicate things considerably. The client is no longer only talking to the middle tier, the middle tier is also, on its own initiative so to speak, talking to all the (active) clients. And besides that, the timing problem isn't eliminated, only minimized.

I want to keep it simple but still support the needs. I need to look at the business process. The order taker talks with a customer on the phone. They discuss what the customer needs, and price and availability is communicated. Eventually the customer decides to buy, and the order taker submits the order to the middle tier. In response, the middle tier returns a report. The report states the final prices and availability. The latter will have an impact on delivery date. All discrepancies with the previously (orally) given facts are marked in the report (if any). The order taker discusses them with the customer and if he is unhappy with that, the order can be cancelled. Cancelling the order is a transaction of its own, an undo/delete transaction.

I understand that if you give a price to a customer you may have to stick to itto some extent at least.[5] This can be corrected quite easily. One cheap way is to leave the price setting to a few people who only change prices after business hours. Another way is to have the system itself postpone effectuation of price changes to the next non-dealing moment. If the client caches price information, she must somehow have the knowledge to refresh (or drop) the cache on suit-able/strategic occasions.

[5] If you, an order taker, misread two zeros in the price, are you then liable?

The problem with availability (stock) is, however, in contrast to prices, a dynamic one. Therefore, the need to have a report given as a response to an order submitted stands. It might as well report any price discrepancies found even if a policy to avoid that is implemented, just to be sure. The report will also have the final say on whether the order is within the global maximum limit or not and if the customer's credit is exceeded or not.

When an order is persisted, we stick to the price given. We don't want to tick our customer off with an invoice with higher prices than promised. This forces us to store the promised prices (OrderPrice in Figure A-5) with the order instead of just referring to the item (that has a price). An alternative is to have price history on the items, which is too much of a hassle. And, yes, we do not "reopen" submitted orders for complementing items in case of, say, the customer realizing he wants three instead of two of something shortly after talking to us. We might place an order with no cost for delivery and/or invoicing to take care of this situation.[6] Changing an order line to fewer items is another story that we can handle easily (it might affect high-volume discounts in the real world).

[6] This is one of the many problems in a toy sample like this that are not stated well.

POrderBuilder

The support for taking and handling purchase orders is within the class POrderBuilder. It's a class meant to be used both in client and middle tier, and it's transported via serialization over the wire. POrderBuilder interacts with the warehouse on several occasions, such as when an order line is created when AddItemToOrder() is called. This interaction is captured in an abstraction called ItemStore consisting of one single method, GetItem().

The discrepancy report is created by POrderBuilder when calling the method CheckDiscrepancies(). The typical scenario is that the client uses a local version of ItemStore see more in the section ItemStore and different implementations in client, server and test, which gives prices and stock during the conversation with the customer. When the order is completed, the order taker submits it to the middle tier. At some point the middle tier calls CheckDiscrepancies() on the passed POrderBuilder to produce the report. At this point the ItemStore is an object living in the middle tier close to the database.

To have CheckDiscrepancies() as a method on POrderBuilder is appealing when it comes to unit testing. Take a look at the next code listing. A POrderBuilder, bldr, is created at [SetUp], where it is also populated with a few order lines. The itmStore is a stub ItemStore also created and associated with bldr during [SetUp]. The test simulates, in the first row, how the situation in the ItemStore has changed regarding stock when it is time to produce that discrepancy report. See the following code:

[Test] public void Stock() {   itmStore.Items[0].Stock = 1;   POrderDiscrepancyReport r = bldr.CheckDiscrepancies();   Assert.AreEqual(0, r.PriceCount);   Assert.AreEqual(1, r.StockCount);   Assert.AreEqual(1, r.GetStockAt(0).NumberOfMissingItems); }


Please picture how the discrepancy report is created early in the order submit handling before the order is committed. However, there is a problem concerning transaction handling here, a problem that is often overlooked or oversimplified. If you're not careful, the check can come out alright, in other words without any discrepancies, but when you commit the order, for instance the last item in stock has been "assigned" to another order and customer. I'm saying that the discrepancy check should happen in the same transaction as the order commit and with locks that prevent others from "stealing" items from the warehouse that you're about to sell.

ItemStoreDifferent Implementations in Client, Server, and Test

It might be interesting to take a closer look at ItemStore. I talked about a local client version as well as a middle tier, transactional, version in the text earlier. What's that all about? Well, let me show you.

In Figure A-13 you can see how POrderBuilder holds a reference to an ItemStore. The reference is implemented as a public property that is assigned by the application code. In the client I use an implementation of ItemStore that is, and I'm sorry if I overstate this, very different from the transactional implementation that I use in the middle tier during order submit. ItemStore isn't much of an interface to implement when it comes to the number of methods; however, it is very central to the POrderBuilder. The ItemStore is, for instance, used by all methods showed in Figure A-13.

Figure A-13. The relationship between POrderBuilder and ItemStore


I've implemented the client version of ItemStore as a simple "read all items from store and cache." This strategy is fine as long as the number of items isn't too great. If it comes to that, it's no big deal to implement a version that queries the middle tier "smarter." In the client, when the order taker has the privilege to "work with an order," a POrderBuilder is instantiated and assigned an instance of the client side ItemStore implementation. At any time, a CheckDiscrepancies() against the client ItemStore is possible.

If the customer accepts, the next step is submission of the order to the middle tier. The POrderBuilder instance is serialized over the wire, but not the ItemStore instance. The reference to ItemStore is marked as [NonSerialized]. In the façade object, a connection/transaction is created and the job to take the order is handed over to the process object (the business object). This is shown in here:

public POrderDiscrepancyReport TakeOrder(POrderBuilder order) {   POrder bo = new POrder();   CnTxWrap cn =       CnTxWrap.BeginTransaction(bo.CreateConnectionForMe() );   try   {     POrderDiscrepancyReport res = bo.TakeOrder(cn, order);     cn.Commit();     return res;   }   catch   {     cn.Rollback();     throw;   } }


While we're at it, let's take a look at the process object's TakeOrder() method in the following listing:

[PrincipalPermission(SecurityAction.Demand, Role = Roles.User)] [PrincipalPermission(SecurityAction.Demand, Role =   @"InternalUnitTesting")] public POrderDiscrepancyReport TakeOrder(CnTxWrap cn,   POrderBuilder order) {   ItemStore itmStore = new TxItemStore(cn);   order.ItemStore = itmStore;   POrderDiscrepancyReport res = order.CheckDiscrepancies();   POrderPh ordr =     ((POrderBuilder.IOrderInternal)order.POrder).POrder;   new POrder().InsertNoRBS(cn, ordr);   OrderLine olproc = new OrderLine();   for(int i=0; i<order.OrderLineCount; i++)   {     OrderLinePh ol =       ((POrderBuilder.IOrderLineInternal)order[i]).OrderLine;     olproc.InsertNoRBS(cn, ol);   }  return res; }


You can see how ItemStore is actually an instance of TxItemStore that is passed the parameter cn containing/wrapping a database connection and an ongoing transaction. This implementation of ItemStore reads the store with an UPDLOCK hint (or whatever method you prefer), causing the store to be locked for update by others at this point. Granted, this is something of a bottleneck but correctness is high.

CheckDiscrepancies() will throw an exception in case there are severe business rule violations, such as if the global maximum limit is exceeded. If the only discrepancies are price differences or stock shortage, these will be noted in the report, but the order will be fully placed.

Do you remember what I said about double-checking business rules that "run" on both the client and the server? The essence is never to trust the client. Well, the checking is done in CheckDiscrepancies(). For instance, any tampering with the prices on the client side would come out as a discrepancy. In this particular situation, the client is running in your controlled environment, but you should nevertheless be cautious. Code executing on the server is much safer than code on the client. Remember that the server can't know (not generally) if the caller is your program sending information on the wire or if it's another program.

Some Final Points

Let's be honest. This isn't what most people would call pure OO. For instance, I'm not afraid of using somewhat "weaker" object references than the language itself provides. Have a look at my item reference in the form of a Guid in Figure A-13. But if you're a bit daring, ask yourself what's so obvious about using "pointer" references anyway. Compare it to proxy references, which give you an option of late/lazy instantiation.

What I want to say is don't get hung up on what's right and what's wrong. Generally, there is no such thing (I'm sure Jimmy agrees). Get the job done. Supply your business with the software it needs. Know what your design goals are (for instance, having the business rules in one place), and realize them.




Applying Domain-Driven Design and Patterns(c) With Examples in C# and  .NET
Applying Domain-Driven Design and Patterns: With Examples in C# and .NET
ISBN: 0321268202
EAN: 2147483647
Year: 2006
Pages: 179
Authors: Jimmy Nilsson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net