Saturday, November 18, 2006

(ZT)The Client Side of ASP.NET Pages

http://msdn.microsoft.com/msdnmag/issues/06/12/Cutting%20Edge/default.aspx

 


Analysis of the ASPX Code
Analysis of the HTML Client Code
The View State Field
The PostBack Mechanism
Analysis of Class Code


There's a trend in the software industry towards moving much of the burden of code writing to the infrastructure of the underlying platform. A variety of development platforms ask developers to provide a high-level description of the information they need in a relatively loose syntax, instead of hard-coding every single byte of it according to a strict set of syntax rules. It is now common for developers to use an XML dialect to describe the desired result and have a compiler or runtime engine parse and process the contents into traditional and executable code.

For example, Windows® Presentation Foundation, one of the pillars of the .NET Framework 3.0, uses XAML as the XML-based presentation language to describe the user interface of the form. The Microsoft AJAX Library (part of the system formerly code-named ASP.NET "Atlas") applies the same principle to rich Web pages with its XML-Script metalanguage (although, technically, XML-Script is not part of the core release, but rather it's being shared as an unofficial sample technology). A declarative layout language, XML-Script wires up HTML elements and script together and forms virtual client-side controls. In the end, XML-Script injects logic and functionality in client pages.

There are a few advantages to using a declarative language to author Web pages and forms. In this way, server-side components can more easily generate pages and forms than if they had to emit actual Visual Basic®, C#, or JavaScript code. Furthermore, declarative markup is inherently easier to devise and design for authoring tools such as Visual Studio®. From an architectural standpoint, by using a declarative approach you indicate what page elements will do, but not how they will do it. In this way, you create an additional abstraction layer.

The first concrete programming environment to take advantage of such a model was ASP.NET, starting with version 1.0. As most Web developers should know by now, an ASP.NET page is typically written in one or two files: an .aspx markup file and, optionally, a code-behind file. The code-behind contains a class file written in any supported programming language, though typically Visual Basic or C#. The .aspx markup file contains HTML tags, ASP.NET control tags, and literals that form the structure of the page (it can also contain code). This text is parsed at run time and transformed into a page class. Such a page class, combined with the code-behind class and some system-generated code, comprise the executable code that processes any posted data, generates the response, and sends it back to the client.

While the overall model is known to the vast majority of ASP.NET developers, a number of black holes exist that only a small group of developers understand with much depth. MSDN®, books, and online articles explain single aspects of the page machinery, but an overall and unified coverage of the page internals is still lacking. If you take a look at the HTML source code of an ASP.NET page you see a number of hidden fields and automatically injected blocks of JavaScript code that you may hardly make sense of. However, these fields and blocks contribute to make the Web page work. In this column I'll analyze the client-side source code that ASP.NET pages generate. I'll cover hidden fields such as the well-known view state, but also little known ones such as control state, event validation, event target, and argument and system-provided script code.

Much of the implementation details I cover here are specific to the current version of ASP.NET. These details could change in the future (they have changed in the past), and you shouldn't build any production code that depends on any undocumented details.

Analysis of the ASPX Code

Figure 1 shows a minimal but working ASP.NET page. Despite its extreme simplicity, this is a good sample as it includes typical elements of a real-world ASP.NET page-input fields, clickable postback elements, and read-only elements.

The .aspx page contains three server controls: a textbox to capture data, a Submit button to start a post operation, and a label to display read-only data. On top of the .aspx file, the Page directive defines some global attributes for the individual page. Let's take a look at the most commonly used attributes of the Page directive, such as those that you saw in Figure 1.

<%@ Page Language="C#" 
AutoEventWireup="true"
CodeFile="Test.aspx.cs"
Inherits="Test"
%>

Most of the Page directive attributes have limited effect on the page markup, the HTML code that the browser receives with the HTTP response. Rather, most Page attributes affect the code of the dynamically generated page that the system builds on top of the .aspx markup and code-behind class. The Language attribute designates the language used to author the code-behind in Visual Studio. The system will use the same language to generate the dynamic page class to serve the browser request for the .aspx resource. The CodeFile attribute indicates the source file where the code-behind class is stored. The Inherits attribute indicates the name of the code-behind class in the code file that should be used as the parent of the dynamically generated page class. Finally, the AutoEventWireup attribute indicates whether a default naming convention should be used to map handling code to Page events. When AutoEventWireup is set to true, you can add a Page_Load method to the code file to handle the page Load event, and it will automatically be registered with the Page's Load event. The implicit naming convention dictates that the event handler will take the form of Page_XXX, where XXX can be the name of any public events defined on the Page class. If AutoEventWireup is set to false, you must explicitly bind the Page class event with its handler. You can do that in a made-to-measure class constructor:

public partial class Test : System.Web.UI.Page
{
public Test()
{
this.Load += new EventHandler(Page_Load);
}
...
}

When the Web server receives an HTTP request for a given .aspx resource, it forwards the request to the ASP.NET worker process. The process hosts the CLR, inside of which a runtime environment is created to process ASP.NET requests. The ultimate goal of the ASP.NET HTTP runtime environment is serving the request-that is, obtaining the markup (HTML, WML, XHTML, and whatever else the app is supposed to return) which will be embedded in the HTTP response. In charge of returning the markup for the request is a special system component known as the HTTP handler.

The HTTP handler is an instance of a class that implements the IHttpHandler interface. The ASP.NET framework comes with a few predefined HTTP handlers to serve particular situations or to act as a base class for other and more specialized requests. The System.Web.UI.Page class is one of the most complex and sophisticated built-in HTTP handlers in ASP.NET.

Each ASP.NET request is mapped to an HTTP handler. Suppose that a client browser places a request for a page named test.aspx. The request is passed to ASP.NET and processed by the HTTP runtime. The runtime determines the HTTP handler class to serve the request through a page handler factory. If is the correct handler is not yet available in the AppDomain, it is created dynamically and stored in the ASP.NET temporary folder on the Web server machine. For a page named test.aspx, the HTTP handler is created as a class named ASP.text_aspx.

The dynamic creation of the HTTP handler class for a given request is a process that takes place only once per page, the first time that page is requested in the application lifetime (although when batch compilation is used, the handler can be generated on the first request for any page in the application). The dynamically created assembly is invalidated and replaced if the application is restarted or if the page source is modified on the Web server. Figure 2 shows the hierarchy of page classes from the base Page class down to the dynamically generated class to serve the user request.

Figure 2 Hierarchy of Page Classes
Figure 2 Hierarchy of Page Classes (Click the image for a smaller view)

Figure 2 Hierarchy of Page Classes
Figure 2 Hierarchy of Page Classes (Click the image for a larger view))

The ASP.NET runtime creates the Visual Basic or C# source code of the dynamic page class by parsing the source code of the corresponding .aspx file. Each tag with runat="server" is mapped to a server control instance. Any other text is mapped to a literal control and emitted verbatim. The Register directive, if any, helps to resolve tags pointing to non-standard controls. The markup returned to the client browser is composed by accumulating the markup that each server control in the page emits. Note that each page generally emits markup, usually HTML markup. However, this is not a requirement, and an ASP.NET page can output any data it wants.

Back to top

Analysis of the HTML Client Code

Figure 3 shows the HTML output for the sample page in Figure 1. In the HTML there's no clue that a Page directive existed in the server-side .aspx page. Instead, the !DOCTYPE directive is copied verbatim. The first runat="server" block in Figure 1 is the <form> tag. This means that any text in between Page and <form> is emitted verbatim. In the source code of the dynamically created page class on the server, this text is converted into a single instance of the LiteralControl class. The <form> tag is emitted like this:

<form name="form1" method="post" action="Test.aspx" id="form1">

The <form runat="server" …> tag is rendered through an instance of the HtmlForm class. The control class has no property to let you set the action attribute on the output markup. The action attribute is hardcoded to the URL of the current page. This behavior is at the foundation of the ASP.NET platform. Note that the ID attribute is partnered with an identical name attribute.

The <asp:textbox> tag is rendered in HTML through an <input type="text"> element. In this case, a name attribute is added to match the original ID attribute. Note that if you omit the ID attribute you may receive a warning from Visual Studio 2005, but ASP.NET will still compile the page successfully. If the ID attribute is missing, a random string is generated and bound to the name attribute. The <asp:Button> tag is rendered through an <input type="submit"> button. An <asp:Label> tag will render the HTML <span> tag to the client browser.

In most cases (though not in all), each tag decorated with the runat="server" attribute generates a corresponding block of HTML markup. The ID string guarantees a persistent match between the two blocks-one on the client side and one on the server side. As you can see in Figure 3, a couple of hidden fields complete the HTML markup: __VIEWSTATE and __EVENTVALIDATION.

Back to top

The View State Field

The contents of the __VIEWSTATE field represent the state of the page when it was last processed on the server. Although sent to the client, the view state doesn't contain any information that should be consumed by the client. The information stored in view state is pertinent only to the server page and some of its child controls and is exclusively read, consumed, and modified by the server.

Implemented in this way, the view state doesn't consume any critical server resources and is fast to retrieve and use. On the other hand, just because the view state is packed with the page, it inevitably increases the size of the HTTP request and response by a few kilobytes. Note that a realistic page padded with a grid of data can easily reach a view state size of 20KB. This extra stuff is uploaded and downloaded each and every time. The view state is one of the most important features of ASP.NET because it enables stateful programming over a stateless protocol such as HTTP. Used without strict criteria, though, the view state can easily become a burden for pages.

By overriding a couple of methods on the code file class, you can leave the contents of the view state field on the server, stored in a database, in the Cache or in the Session object. However, note that leaving the view state information on the server is not the obvious workaround it first appears. It's not by chance, in fact, that the ASP.NET team opted for a page-based view state. A server-based view state is fine as long as user navigates from one page to the next following the links in the application. Remember that ASP.NET applications work by posting repeatedly over the same page. But what if the user clicks the Back button? To be safe, you should maintain view state on a per-request basis rather than on a per-page basis. And the chain of tracked requests should be as long as the requests the user can reach through the Back and Forward buttons. View state stored on the client may not be perfect, but neither is view state stored on the server. The one that's preferable for your application depends on the expectations you have for it.

In ASP.NET 2.0, the __VIEWSTATE hidden field contains two types of information-view state and control state. Developers can disable view state altogether and operate their applications in a pure stateless manner. This is not an issue as long as you use built-in controls and controls that you wrote yourself, or at least controls for which you have access to the source code. What if you use a custom control that assumes an enabled view state? Some controls-typically, rich third-party and custom controls-need to persist private information across postbacks. This information is not public and not designed to be exposed to the application level-for example, the collapsed/expanded status of a dropdown panel. This information can only be persisted to the view state. If the view state is disabled, the control may inadvertently fail.

To alleviate this issue, ASP.NET 2.0 introduces the notion of the control state. Each server control can pack any critical properties to a collection and store it to the page's control state. The control state is saved to the __VIEWSTATE field but, unlike the traditional view state, can't be disabled and is always available. Developers manage the control state through a pair of new overridable methods on the Page class: LoadControlState and SaveControlState. Speaking of the view state in ASP.NET 2.0, though, it is also worth noticing that a new and more effective serialization algorithm is employed to streamline the state of individual controls to a hidden field. As a result, the overall size of the __VIEWSTATE hidden field in most cases is as small as half the size of the corresponding field in ASP.NET 1.x.

As mentioned, the view state is stored in a hidden field to associate it unambiguously with a particular page request. When any of the HTML elements in a given page instance post back, the dynamically generated page class starts working on the server and uses the data stored in the view state to recreate the last known good state for the controls in the page. What if the view state is tampered with on the client? Is that ever possible? By default, the view state is encoded using the Base64 schema and hashed, and the resulting hash value is also stored with the view state. The hash value is calculated from the contents of the view state plus a server key. Whenever the page posts back, the code in the page class separates the contents and hash value of the view state. Next, it recalculates the hash value based on the retrieved view state contents and server key. If the two hash values don't match, a security exception is thrown (see Figure 4).

Figure 4 Page View Can't Be Altered on the Client
Figure 4 Page View Can't Be Altered on the Client (Click the image for a smaller view)

Figure 4 Page View Can't Be Altered on the Client
Figure 4 Page View Can't Be Altered on the Client (Click the image for a larger view))

What if a malicious user attempts to post a fake request with a modified view state? The malicious user would need to know the server key in order to generate a hash value on the modified view state contents that can be matched on the server. The server key, though, is made of server-only information and is not included in the view state field. The tweakviewstate.aspx page in the companion code contains script code to modify the view state and practice with the exception shown in Figure 4.

Although the view state can hardly be used to plan an attack, it doesn't guarantee data confidentiality unless encryption is used. The contents of the view state, in fact, can be decoded and examined on the client, but not successfully modified to serve an altered page state to the server environment.

The __EVENTVALIDATION hidden field is a security measure new to ASP.NET 2.0. The feature prevents unauthorized requests sent by potentially malicious users from the client. To ensure that each and every postback and callback event originates from the expected user interface elements, the page adds an extra layer of validation on events. The page basically matches the contents of the request with the information in the __EVENTVALIDATION field to verify that no extra input field has been added on the client and that value is selected on a list that was already known on the server. The page generates the event validation field during rendering-that is at the last possible moment when the information is available. Like the view state, the event validation field contains a hash value to prevent client-side tampering.

Controls use the RegisterEventForValidation method on the ClientScriptManager object to store their own information for safe postbacks. At a very minimum, each control registers its own unique ID. List controls also store all the values in the list. Server controls that support event validation typically call the ValidateEvent method in their implementation of the IPostBackDataHandler interface. If the validation fails, a security exception would be thrown.

You can enable and disable event validation on a per-page basis; each control class enables event validation through the SupportsEventValidation attribute. Currently, there's no way to enable or disable event validation on a particular control instance.

Event validation is a defense barrier aimed at limiting input to a known set of values. It simply raises the security bar higher and doesn't stop script injection attacks by itself.

Event validation may pose issues if used in the context of AJAX-enabled applications. In such applications, some client work can create new input elements on the fly, thus making the next postback fail because of unknown elements. The best workaround is to render any user interface on the server whenever possible, and hide it on the client using the cascading style sheets display attribute. In this way, any user interface you're going to use is registered with the event validation field. If you write custom controls, you should decorate it with the SupportsEventValidation attribute to enable this feature.

Back to top

The PostBack Mechanism

The ASP.NET page in Figure 1 posts back as the user clicks the button. This is because the <asp:Button> tag renders as an HTML submit <input> element. When a submit input field is clicked, the browser fires the onsubmit HTML client event and then prepares the new request to the server based on the contents of the submitted form. The HTTP request being sent includes an additional piece of information that evaluates to the ID of the button.

The page class scans the body of the HTTP request to see if any of the posted fields matches the ID of a button control in the ASP.NET page. If the match is found, that button control is called to run any code associated with its Click event. More precisely, the page class checks to see if the matching button control implements the IPostBackEventHandler interface. If so, it invokes the RaisePostbackEvent method on the interface. For a button control, the method raises the server-side Click event.

So far, so good. But what if the page contains a LinkButton control instead? Figure 5 shows the markup for an ASP.NET page that is identical to the page in Figure 1 except that a LinkButton is used instead of the Submit button. As you can see, the markup includes two more hidden fields, __EVENTTARGET and __EVENTARGUMENT, and a bit of JavaScript code. The href target of the link button is bound to the __doPostback script function meaning that the function will be invoked whenever a client click on the element is detected. The __doPostback function is emitted in the page by the rendering code of the LinkButton control. It populates the __EVENTTARGET and __EVENTARGUMENT fields with proper information and then triggers the postback via script. In this case, the body of the HTTP postback request simply contains the input fields in the page and no posted data references the Submit button.

How does ASP.NET recognize the control responsible for handling the postback? When no controls referenced in the request body implement the IPostBackEventHandler interface, the page class looks for the __EVENTTARGET hidden field, if any. The contents of the field is assumed to be the ID of the control that caused the postback. If this control implements the IPostBackEventHandler interface, the RaisePostbackEvent method is invoked. For a LinkButton control, this results in the invocation of the Click server event.

Back to top

Analysis of Class Code

The .aspx markup defines the layout of an ASP.NET page and determines size, style and position of constituent controls. It contains no logic, however, except perhaps for some client script code and any Visual Basic or C# inline code you may have. Initialization code, event handlers, and any helper routines typically go in a separate companion file, known as the code-behind file:

public partial class Test : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
...
}
protected void Button1_Click(object sender, EventArgs e)
{
...
}
}

The class in the code file inherits, directly or indirectly, from System.Web.UI.Page. The code file and markup represent required but distinct pieces of information. To fully represent the ASP.NET page, they must be combined to form a page class that incorporates the logic of the code file and the layout data of the markup file. The code file class is already a page class, but it lacks two key pieces of information: the list of child server controls to populate the user interface and the declaration of class members that identify the various server controls.

In ASP.NET 1.x, each time the page author drops a control onto the Web Form, Visual Studio .NET 2003 automatically adds a new line to the code file to create a class member that handles the just dropped server control. This does a pretty good job of keeping everything in sync, but often developers run into compile errors due to the lack of a class member or existence of useless class members.

In ASP.NET 2.0 the issue is fixed in an elegant way. Enter partial classes, a source-level, assembly-limited, non-object-oriented way to extend the behavior of a class. In the .NET Framework 2.0, a class definition can span over two or more files. Each file contains a fragment of the final class definition and the compiler takes care of merging the various partial definitions to form a single, unified class. All fragments must have the same signature and the final class definition must be syntactically correct.

Next, a second partial class is generated dynamically to list all control members. The two partial classes are merged at compile-time. When the .aspx markup file is parsed to create the temporary ASP.test_aspx class, this class inherits from the combined code file in its final version. If the ASP.NET page is not bound to a code file but contains its code inline, then the dynamic page class inherits from System.Web.UI.Page and includes any inline code in its body.

There's a lot more to learn about the dynamic page compilation machinery, but this provides fodder for a future column.

Monday, November 13, 2006

(ZT)C# Interview Basic Question

http://dflying.dflying.net/1/archive/104_c_basic_interview_questions.html

 

  1. What is the implicit name of the parameter passed into a property’s ’set’ method?
    ‘value’. And it’s data type depends on whatever variable we’re changing.
  2. How do you inherit from a class in C#? Place a colon and then the name of the base class.
  3. Does C# support multiple inheritances? No, use interfaces instead.
  4. When you inherit a protected class-level variable, who is it available to? Only classes that ultimately inherit from the class with a protected member can see that member.
  5. Are private class-level variables inherited? Yes, but they are not accessible, so looking at it you can honestly say that they are not inherited. But they are.
  6. Describe the accessibility modifier protected internal. It’s available to derived classes and classes within the same Assembly (and naturally from the base class it’s declared in).
  7. C# provides a default constructor for me. I write a constructor that takes a string as a parameter, but want to keep the no parameter one. How many constructors should I write? Two. Once you write at least one constructor, C# cancels the freebie constructor, and now you have to write one yourself, even if there’s no implementation in it.
  8. What’s the top .NET class that everything is derived from? System.Object.
  9. How’s method overriding different from overloading? When overriding, you change the method behavior for a derived class. Overloading simply involves having a method with the same name within the class.
  10. What does the keyword virtual mean in the method definition? The method can be over-ridden.
  11. Can you declare the override method static while the original method is non-static? No, you can’t, the signature of the virtual method must remain the same, only the keyword virtual is changed to keyword override.
  12. Can you override private virtual methods? No, moreover, you cannot access private methods in inherited classes, have to be protected in the base class to allow any sort of access.
  13. Can you prevent your class from being inherited and becoming a base class for some other classes? Yes, that’s what keyword sealed in the class definition is for. The developer trying to derive from your class will get a message: cannot inherit from Sealed class WhateverBaseClassName. It’s the same concept as final class in Java.
  14. Can you allow class to be inherited, but prevent the method from being over-ridden? Yes, just leave the class public and make the method sealed.
  15. What’s an abstract class? A class that cannot be instantiated. A concept in C++ known as pure virtual method. A class that must be inherited and have the methods over-ridden. Although an abstract class does not require implementations (its methods can be abstract) it can also offer implementations of methods (either virtual or not) which can be called in implementing classes.
  16. When do you absolutely have to declare a class as abstract (as opposed to free-willed educated choice or decision based on UML diagram)? When at least one of the methods in the class is abstract. When the class itself is inherited from an abstract class, but not all base abstract methods have been over-ridden.
  17. What’s an interface class? It’s an abstract class with public abstract methods all of which must be implemented in the inherited classes.
  18. Why can’t you specify the accessibility modifier for methods inside the interface? They all must be public. Therefore, to prevent you from getting the false impression that you have any freedom of choice, you are not allowed to specify any accessibility, it’s public by default.
  19. Can you inherit multiple interfaces? Yes, why not.
  20. And if they have conflicting method names? It’s up to you to implement the method inside your own class, so implementation is left entirely up to you. This might cause a problem on a higher-level scale if similarly named methods from different interfaces expect different data, but as far as compiler cares you’re okay.
  21. What’s the difference between an interface and abstract class? In the interface all methods must be abstract. In the abstract class some methods can be concrete. In the interface no accessibility modifiers are allowed, which is ok in abstract classes.
  22. How can you overload a method? Different parameter data types, different number of parameters, different order of parameters.
  23. What is the difference between const and readonly?
    The const keyword is used for compile time constants while the readonly keyword is used for runtime constants.
  24. What’s the difference between System.String and System.StringBuilder classes? System.String is immutable while System.StringBuilder was designed with the purpose of having a mutable string where a variety of operations can be performed.

 

C#面试基础问题

English Version: http://dflying.dflying.net/1/archive/104_c_basic_interview_questions.html

如果你的简历上面写“熟悉/了解C#”,那么你就应该能够回答下面的这些基础问题。我将给出自己的简要答案以供参考。欢迎讨论。
如果是“精通”,那么请参考:http://www.cnblogs.com/dflying/archive/2006/04/01/364458.html

  1. 传入某个属性的set方法的隐含参数的名称是什么?
    value,它的类型和属性所声名的类型相同。
  2. 如何在C#中实现继承?
    在类名后加上一个冒号,再加上基类的名称。
  3. C#支持多重继承么?
    不支持。可以用接口来实现。
  4. 被protected修饰的属性/方法在何处可以访问?
    在继承或间接继承与这个类的子类中可以访问。
  5. 私有成员会被继承么?
    会,但是不能被访问。所以看上去他们似乎是不能被继承的,但实际上确实被继承了。
  6. 请描述一下修饰符protected internal
    被protected internal修饰的属性/方法只能在它的在同一个程序集(Assembly)中的子类被访问。
  7. C#提供一个默认的无参数构造函数,当我实现了另外一个有一个参数的构造函数时候,还想保留这个无参数的构造函数。这样我应该写几个构造函数?
    两个,一旦你实现了一个构造函数,C#就不会再提供默认的构造函数了,所以需要手动实现那个无参数构造函数。
  8. C#中所有对象共同的基类是什么?
    System.Object.
  9. 重载和覆写有什么区别?
    重载提供了对一个方法签名的不同参数调用的实现。覆写提供了子类中改变父类方法行为的实现。
  10. 在方法定义中,virtual有什么含意?
    被virtual修饰的方法可以被子类覆写。
  11. 能够将非静态的方法覆写成静态方法么?
    不能,覆写方法的签名必须与被覆写方法的签名保持一致,除了将virtual改为override。
  12. 可以覆写私有的虚方法么?
    不可以,甚至子类中无法访问父类中的私有方法。
  13. 能够阻止某一个类被其他类继承么?
    可以,使用关键字sealed。
  14. 能够实现允许某个类被继承,但不允许其中的某个方法被覆写么?
    可以,标记这个类为public,并标记这个方法为sealed。
  15. 什么是抽象类(abstract class)?
    一种不可以被实例化的类。抽象类中一般含有抽象方法,当然也可有具体实现。继承类只有实现过所有抽象类的抽象方法后才能被实例化。
  16. 何时必须声明一个类为抽象类?
    当这个类中包含抽象方法时,或是该类并没有完全实现父类的抽象方法时。
  17. 接口(interface)是什么?
    只含有共有抽象方法(public abstract method)的类。这些方法必须在子类中被实现。
  18. 为什么不能指定接口中方法的修饰符?
    接口中的方法用来定义对象之间通信的契约,指定接口中的方法为私有或保护没有意义。他们默认为公有方法。
  19. 可以继承多个接口么?
    当然。
  20. 那么如果这些接口中有重复的方法名称呢?
    这种情况中你可以决定如何实现。当然需要特别得小心。但是在编译环节是没有问题的。
  21. 接口和抽象类的区别是什么?
    接口中所有方法必须是抽象的,并且不能指定方法的访问修饰符。抽象类中可以有方法的实现,也可以指定方法的访问修饰符。
  22. 如何区别重载方法?
    不同的参数类型,不同的参数个数,不同的参数顺序。
  23. const和readonly有什么区别?
    const关键字用来声明编译时常量,readonly用来声明运行时常量。
  24. System.String 和System.StringBuilder有什么区别?
    System.String是不可变的字符串。System.StringBuilder存放了一个可变的字符串,并提供一些对这个字符串修改的方法。

posted on 2006-04-01 20:36 Dflying Chen 阅读(4165) 评论(30) 编辑 收藏 引用 网摘 所属分类: .NETC#


评论:

# re: C#面试基础问题 2006-04-02 14:35 | Phoenix Huang

90%是面向对象设计的问题。  回复

# re: C#面试基础问题 2006-04-02 14:48 | Dflying Chen

@Phoenix Huang
是的,当然这些也考察了C#的语言基础。  回复

# re: C#面试基础问题 2006-04-02 17:15 | 林子

不错!
不过如果答案如果能再OO一些那就现好了!  回复

# re: C#面试基础问题 2006-04-02 18:29 | Dflying Chen

@林子
欢迎您提出修改的建议 :)  回复

# re: C#面试基础问题 2006-04-02 22:41 | 装配脑袋

有几道题的答案错误,列出来供你面试时参考:
3 C#支持多重继承么?
答案是类之间不支持,接口之间支持。类对接口叫做实现,不叫继承。
6 请描述一下修饰符protected internal。
正确答案是,在同一个Assembly中,他的访问级别和public一样,而跨Assembly访问时,他的访问级别同protected一样。即protected的范围 + internal的范围。
11 能够将非静态的方法覆写成静态方法么?
答案并没有大错,只是静态方法根本无override概念,属于引起困惑的问题
13 能够阻止某一个类被其他类继承么?
答案没错,但是如果应试者提到将所有构造函数的访问级别都设为Private或子类无法访问到的其他级别(Assembly外的internal),也有同样的效果。
14 能够实现允许某个类被继承,但不允许其中的某个方法被覆写么?
同样是可能引起困惑的题。因为默认(不带virtual)的方法就是不能被override的。带sealed的方法必定是被override过的。
20 接口和抽象类的区别是什么?
还有一个容易被忽视的区别,C#不允许接口带有静态成员,尽管这不是.NET的限制。
23 const和readonly有什么区别?
还有一些区别,当应试者提到时你不能说是错的:作字段时const隐含静态,const可以是局部的,readonly可以在构造函数中初始化等。  回复

# re: C#面试基础问题 2006-04-03 09:32 | Dflying Chen

@装配脑袋
3 C#支持多重继承么?
--答案是类之间不支持,接口之间支持。类对接口叫做实现,不叫继承。
inherit an interface没有什么不妥,为什么中文就不可以?
6 请描述一下修饰符protected internal。
--正确答案是,在同一个Assembly中,他的访问级别和public一样,而跨Assembly访问时,他的访问级别同protected一样。即protected的范围 + internal的范围。
同一个Assembly中依然是protected,您可以查阅一下相关资料。
11 能够将非静态的方法覆写成静态方法么?
--答案并没有大错,只是静态方法根本无override概念,属于引起困惑的问题
我只是问“能覆写成”么?如果她知道静态方法根本无override概念,显然可以打出这个问题。
13 能够阻止某一个类被其他类继承么?
--答案没错,但是如果应试者提到将所有构造函数的访问级别都设为Private或子类无法访问到的其他级别(Assembly外的internal),也有同样的效果。
您说的对,但如果这样思路想下来,我还可以说用C++写父类不能让C#继承。不要太钻牛角尖了。
14 能够实现允许某个类被继承,但不允许其中的某个方法被覆写么?
--同样是可能引起困惑的题。因为默认(不带virtual)的方法就是不能被override的。带sealed的方法必定是被override过的。
这个答案有理。
20 接口和抽象类的区别是什么?
--还有一个容易被忽视的区别,C#不允许接口带有静态成员,尽管这不是.NET的限制。
有道理,但接口不可以有成员吧,不管静态还是非静态的
23 const和readonly有什么区别?
--还有一些区别,当应试者提到时你不能说是错的:作字段时const隐含静态,const可以是局部的,readonly可以在构造函数中初始化等。
我不是很明白你的意思。但我觉得回答出“const关键字用来声明编译时常量,readonly用来声明运行时常量。”已经足够完美了。  回复

# re: C#面试基础问题 2006-04-03 09:44 | 装配脑袋

第六题你错了,不要坚持错误好吗?去试一下就什么都知道了。只有C++/CLI才支持你描述的那种访问级别,internal AND protected.
第十三题,我并非钻牛角尖,我恰好用到了这种手法。我需要一个抽象基类作为我的泛型类型字典,为了模拟type-traits我要求我的继承体系内类的数量有限而且对外隐藏。所以我就需要一个具有internal构造函数的抽象类,只有我自己的代码可以继承他,外部的不可以。你也许不能理解为什么这样做,但是它工作得很好。  回复

# re: C#面试基础问题 2006-04-03 09:50 | 装配脑袋

第二十题,成员函数也叫成员。这很重要,因为operator overloading的过程是静态的。
第二十三题,const可以用于局部常量,而readonly,实际是类的initonly字段,显然不能是局部的。  回复

# re: C#面试基础问题 2006-04-03 10:02 | 装配脑袋

如果我是参加面试的人,你会对我有何感想?
我真的不是钻牛角尖,我每一个质疑,都是我以我的真实程序为依据的,你忽视的地方并不是我恰好关注的地方,而是我一开始就关注的。  回复

# re: C#面试基础问题 2006-04-03 10:20 | Dflying Chen

@装配脑袋
第六题是我错了,感谢指正.
第十三题我明白你的意思,只是我感觉答出来sealed就够了。
第二十题,我不知道成员函数也叫成员,一直用方法(method)来表示。成员函数是member function么?  回复

# re: C#面试基础问题 2006-04-03 10:21 | Dflying Chen

@装配脑袋
你忽视的地方并不是我恰好关注的地方,而是我一开始就关注的。
这句话什么意思?  回复

# re: C#面试基础问题 2006-04-03 10:24 | 装配脑袋

就是说,我并不是为了挑你的错而想这么深的,而是一开始就知道
PS. method的定义不就是member function吗。。。
回复

# re: C#面试基础问题 2006-04-03 10:27 | Dflying Chen

@装配脑袋
感谢指点,受益匪浅:)
member function好久没有听说了,我基础也不是很好,见笑了。  回复

# re: C#面试基础问题 2006-04-05 16:31 | 小新0574

我都能答出来,哈哈,熟悉就是这种级别么?出些asp.net的题做做?  回复

# re: C#面试基础问题 2006-04-14 13:34 | Inkerman

quite useful, indeed.
Do you have English version of this blog?
I find it's very hard to read in Chinese as I learned my programming language only in English.
Thanks
Inkerman
回复

# re: C#面试基础问题 2006-04-14 13:40 | Dflying Chen

@Inkerman
sure, please refer to http://dflying.dflying.net/
hope may help :-)  回复

# re: C#面试基础问题 2006-05-05 14:58 | Li-Birch

呵呵,不错不错~~
我是在校学生,快毕业了
因为不是计算机专业,但又很喜欢,一直都在自学,也做过项目
不过还是不系统,特别是些基础性的东西
希望能多点类似的资料~
谢谢谢谢~~  回复

# re: C#面试基础问题 2006-05-05 18:18 | Dflying Chen

@Li-Birch
谈这种东西要冒风险的:)  回复

# re: C#面试基础问题 2006-05-07 09:33 | johnsir

@Dflying Chen
为什么要冒风险呀 ?  回复

# re: C#面试基础问题 2006-05-07 09:53 | Dflying Chen

@johnsir
这种问题往往带来很多争吵阿,还会被人骂……  回复

# re: C#面试基础问题 2006-05-09 10:30 | totti

能找到高手,但也存在碰到孔乙己的危险.  回复

# re: C#面试基础问题 2006-05-09 10:32 | totti

这种OO面试题,一定要结合一个Demo进行机试,不然很有可能碰到孔乙己。  回复

# re: C#面试基础问题 2006-05-09 10:33 | Dflying Chen

@totti
呵呵,好多人“架构”都是一套一套的……吓人哦  回复

# re: C#面试基础问题 2006-07-21 20:02 | 我支持 装配脑袋

好的程序员,必须在每一个基础上有最清晰的理解.一知半解很可怕的。  回复

# re: C#面试基础问题 2006-07-28 10:07 | a_a

后面有几道题答不上来,看来我基础还差得很远呀!
不过我也发现了一个问题,就是第24题,以我目前所学习的程度来看,应该是不对的。
24. System.String 和System.StringBuilder有什么区别?
System.String是不可变的字符串。System.StringBuilder存放了一个可变的字符串,并提供一些对这个字符串修改的方法。
我想StringBuilder中封装的应该也是一个普通的String,而不是“可变的字符串”,我特地用Reflector看了一下,结果也是这样。StringBuilder应该是封装了一系列方便对字符串进行操作的方法,并且对效率进行了优化。
不知道我说的对不对,请指正啦。  回复

# re: C#面试基础问题 2006-08-02 18:19 |

C#支持多重继承么?
答案是类之间不支持,接口之间支持。类对接口叫做实现,不叫继承。
这个说的好的,本质上是不支持多重继承的  回复

# re: C#面试基础问题 2006-08-03 21:46 | crazydd

请问: abtract class 可以有构造函数嘛?还是必须有?
abtract class 和 接口的区别,我觉得是不是还有一个? abtract class可以有数据成员,而接口不可以。我是菜鸟,请指教。  回复

# re: C#面试基础问题 2006-09-08 11:28 | 月亮lover

感谢楼主和装配脑袋,受益匪浅  回复

# re: C#面试基础问题 2006-09-08 11:55 | Dflying Chen

@月亮lover
我也要感谢装配脑袋,好多地方都让我豁然开朗!  回复

# re: C#面试基础问题 2006-10-20 00:31 | MK2

@a_a
string 虽然是引用类型,但每次操作都是做值复制,要重新申请和分配内存,再将新的string 的引用传出`````
而StringBuilder 是预先申请了空间,默认大小是16,只要当前字符串实质大小不超过可用空间大小,就不需要申请内存和复制工作。若要对字符串做大量操作,一般推荐用StringBuilder,因为申请分配内存和复制工作是很耗资源的。
在C#中,string类最奇怪,又是类,但传递的又不是引用,只是做值复制,这个特性又是非引用类型的特点``````  回复

Web Service Optimization

 

Server Side :

Hardware improvement : Multiple processor, Big Memory, SCSI Driver

 

Client Side:

(1). Call methods asynchronously.

(2). XML compression :

See the followng instruction :

Compress the XML-traffic between the client and the server using the SOAPExtensions mechanism and any compression algorithm. You can use a ZIP-compression for you data. There are a lot of open-source compression libraries for C#. For example, you can use a free library called SharpZipLib to compress your web methods. Read this article and download the "CompressionExtension" project. Adding a compression to your webservice is very easy: include the downloaded project into your solution, reference it in your webservice, and mark your web methods with [CompressionExtension] in the proxy-class code. Also, add the [CompressionExtension] flag in the .asmx component. That's it.

(3).Preload all necessary assemblies at the very beginning.

(4). Use pre-authorization under only proper user accessing web service other than "anonymous"

(5). Edit IIS customer error message to minimize network traffic.

DateTime Format (ZT)

有时候我们要对时间进行转换,达到不同的显示效果
 默认格式为:2005-6-6 14:33:34
 如果要换成成200506,06-2005,2005-6-6或更多的该怎么办呢
 我们要用到:DateTime.ToString的方法(String, IFormatProvider)
 using System;
 using System.Globalization;
 String format="D";
 DateTime date=DataTime,Now;
 Response.Write(date.ToString(format, DateTimeFormatInfo.InvariantInfo));
 结果输出
 Thursday, June 16, 2005
参数format格式详细用法
 格式字符 关联属性/说明
 d ShortDatePattern
 D LongDatePattern
 f 完整日期和时间(长日期和短时间)
 F FullDateTimePattern(长日期和长时间)
 g 常规(短日期和短时间)
 G 常规(短日期和长时间)
 m、M MonthDayPattern
 r、R RFC1123Pattern
 s 使用当地时间的 SortableDateTimePattern(基于 ISO 8601)
 t ShortTimePattern
 T LongTimePattern
 u UniversalSortableDateTimePattern 用于显示通用时间的格式
 U 使用通用时间的完整日期和时间(长日期和长时间)
 y、Y YearMonthPattern
 下表列出了可被合并以构造自定义模式的模式。这些模式是区分大小写的;例如,识别“MM”,但不识别“mm”。如果自定义模式包含空白字符或用单引号括起来的字符,则输出字符串页也将包含这些字符。未定义为格式模式的一部分或未定义为格式字符的字符按其原义复制。
 格式模式 说明
 d 月中的某一天。一位数的日期没有前导零。
 dd 月中的某一天。一位数的日期有一个前导零。
 ddd 周中某天的缩写名称,在 AbbreviatedDayNames 中定义。
 dddd 周中某天的完整名称,在 DayNames 中定义。
 M 月份数字。一位数的月份没有前导零。
 MM 月份数字。一位数的月份有一个前导零。
 MMM 月份的缩写名称,在 AbbreviatedMonthNames 中定义。
 MMMM 月份的完整名称,在 MonthNames 中定义。
 y 不包含纪元的年份。如果不包含纪元的年份小于 10,则显示不具有前导零的年份。
 yy 不包含纪元的年份。如果不包含纪元的年份小于 10,则显示具有前导零的年份。
 yyyy 包括纪元的四位数的年份。
 gg 时期或纪元。如果要设置格式的日期不具有关联的时期或纪元字符串,则忽略该模式。
 h 12 小时制的小时。一位数的小时数没有前导零。
 hh 12 小时制的小时。一位数的小时数有前导零。
 H 24 小时制的小时。一位数的小时数没有前导零。
 HH 24 小时制的小时。一位数的小时数有前导零。
 m 分钟。一位数的分钟数没有前导零。
 mm 分钟。一位数的分钟数有一个前导零。
 s 秒。一位数的秒数没有前导零。
 ss 秒。一位数的秒数有一个前导零。
 f 秒的小数精度为一位。其余数字被截断。
 ff 秒的小数精度为两位。其余数字被截断。
 fff 秒的小数精度为三位。其余数字被截断。
 ffff 秒的小数精度为四位。其余数字被截断。
 fffff 秒的小数精度为五位。其余数字被截断。
 ffffff 秒的小数精度为六位。其余数字被截断。
 fffffff 秒的小数精度为七位。其余数字被截断。
 t 在 AMDesignator 或 PMDesignator 中定义的 AM/PM 指示项的第一个字符(如果存在)。
 tt 在 AMDesignator 或 PMDesignator 中定义的 AM/PM 指示项(如果存在)。
 z 时区偏移量(“+”或“-”后面仅跟小时)。一位数的小时数没有前导零。例如,太平洋标准时间是“-8”。
 zz 时区偏移量(“+”或“-”后面仅跟小时)。一位数的小时数有前导零。例如,太平洋标准时间是“-08”。
 zzz 完整时区偏移量(“+”或“-”后面跟有小时和分钟)。一位数的小时数和分钟数有前导零。例如,太平洋标准时间是“-08:00”。
 : 在 TimeSeparator 中定义的默认时间分隔符。
 / 在 DateSeparator 中定义的默认日期分隔符。
 % c 其中 c 是格式模式(如果单独使用)。如果格式模式与原义字符或其他格式模式合并,则可以省略“%”字符。
 \ c 其中 c 是任意字符。照原义显示字符。若要显示反斜杠字符,请使用“\\”。
 只有上面第二个表中列出的格式模式才能用于创建自定义模式;在第一个表中列出的标准格式字符不能用于创建自定义模式。自定义模式的长度至少为两个字符;例如,
 DateTime.ToString( "d") 返回 DateTime 值;“d”是标准短日期模式。
 DateTime.ToString( "%d") 返回月中的某天;“%d”是自定义模式。
 DateTime.ToString( "d ") 返回后面跟有一个空白字符的月中的某天;“d”是自定义模式。
 比较方便的是,上面的参数可以随意组合,并且不会出错,多试试,肯定会找到你要的时间格式
 如要得到2005年06月 这样格式的时间
 可以这样写:
date.ToString("yyyy年MM月", DateTimeFormatInfo.InvariantInfo)
 如此类推

Saturday, November 11, 2006

(ZT). Net环境下消息队列(MSMQ)对象的应用

http://www.cnblogs.com/rickie/archive/2004/11/17/64712.aspx

 

关于消息对象(MSMQ)的一些基本概念可以从《消息队列(Message Queue)简介及其使用》查阅,这里归纳在.Net 环境下应用消息队列(MSMQ)开发的一些基本对象和方法。

队列类型及其相应的路径格式:

Public: [MachineName]\[QueueName]

Private: [MachineName]\Private$\[QueueName]

Journal: [MachineName]\[QueueName]\Journal$

Machine journal: [MachineName]\Journal$

Machine dead-letter: [MachineName]\DeadLetter$

Machine transactional dead-letter: [MachineName]\XactDeadLetter$

The first portion of the path indicates a computer or domain name or uses a period (.) to indicate the current computer.

1. 创建消息队列

可以手动的方式通过Windows提供的工具创建,或者通过程序的方式创建:

if(MessageQueue.Exists(".\\Private$\\MSMQDemo"))

queue = new MessageQueue(".\\Private$\\MSMQDemo");

else

queue = MessageQueue.Create(".\\Private$\\MSMQDemo");

2. 发送消息

缺省情况下,消息序列化XML格式,也可设置为MessageQueue对象的Formatter属性为BinaryMessageFormatter,以二进制格式序列化。

设置消息序列化格式:

if(rdoXMLFormatter.Checked)

queue.Formatter = new XmlMessageFormatter();

else

queue.Formatter = new BinaryMessageFormatter();

发送简单的文本消息:

string strMessage = "Hello, I am Rickie.";

queue.Send(strMessage, "Simple text message");

消息队列可以传送简单的文本消息,也可以传送对象消息,但需要满足如下条件:

(1)class必须有一个无参数的公共构造函数,.Net使用这个构造函数在接收端重建对象。

(2)class必须标示为serializable(序列化)。

(3)所有的class属性必须可读写,因为.Net在重建对象时不能够恢复只读属性的属性值,因此只读属性不能够序列化。

发送对象消息(CustomerInfo class需要满足上述条件):

CustomerInfo theCustomer = new CustomerInfo("0001", "Rickie Lee", "Rickieleemail@yahoo.com");

queue.Send(theCustomer, "Object message");

3. /显示消息

当消息接受后,消息将从队列中删除。可以通过使用MessageQueue.Peek方法来检索消息队列中的第一个消息的复制,保留消息在队列中。不过,这样只能获取的相同的消息。更好的办法是通过foreach来读消息队列中的消息,但不删除队列中的消息。

foreach(System.Messaging.Message message in queue)

{

txtResults.Text += message.Label + Environment.NewLine;

}

4. 接收消息

一般而言,可以通过Receive方法来读取队列中的消息,对于非事务性的队列,优先读取高优先级的消息。如果队列中有多个相同优先级的消息,则以先进先去的方式进行读取消息。对于事务性的队列,则完全以先进先去的方式进行读取消息,忽略消息的优先级。

System.Messaging.Message receivedMessage;

receivedMessage = queue.Receive(TimeSpan.FromSeconds(5));

上面采用同步调用,并且一直等到队列中有可用消息或超时过期。

Demo界面(不在提供DEMO程序):

其他相关事项:

  • 关于消息的加密、路由等等特性,需要有配置Active Directory的消息队列服务器。
  • 为了避免存放消息队列的计算机重新启动而丢失消息,可以通过设置消息对象的Recoverable属性为true,在消息传递过程中将消息保存到磁盘上来保证消息的传递,默认为false。
  • 消息发送方和消息接收方需采用相同的序列化格式,如XML或Binary。
  • 建议每一个消息队列存放相同类型的消息对象,这样可以省掉获取消息对象后,进行类型判别的麻烦。

5.消息队列在分布式系统中的应用

消息队列MSMQ和数据库不一样,消息队列缺乏足够的错误检查能力,并且MSMQ由于需要束缚在windows平台,这些是MSMQ的不足之处。另外,在Production环境中,需要编写大量的代码来进行错误检测和响应。还有大量的死信队列、响应队列和日记队列可能部分在企业不同的计算机上,使得跟踪这些问题或进行诊断变得比较困难。

但是,MSMQ作为组件内部连接比较有用。例如,你可以创建一个XML Web Services使用MSMQ来转发对另一个Server端组件的请求,这种设计巧妙回避了其他异步调用的方法,并且确保可扩展性和性能。

增广贤文

少时学语苦难圆,只道工夫半未全。到老始知非力取,三分人事七分天

 

昔时贤文,诲汝谆谆。集韵增广,多见多闻。观今宜鉴古,无古不成今。
知己知彼,将心比心。酒逢知己饮,诗向会人吟。相识满天下,知心能几人。
相逢好似初相识,到老终无怨恨心。近水知鱼性,近山识鸟音。
易涨易退山溪水,易反易复小人心。运去金成铁,时来铁成金。
读书须用意,一字值千金。逢人且说三分话,未可全抛一片心。
有意栽花花不发,无心插柳柳成荫。画龙画虎难画骨,知人知面不知心。
钱财如粪土,仁义值千金。流水下滩非有意,白云出岫本无心。
路遥知马力,事久见人心。马行无力皆因瘦,人不风流只为贫。
饶人不是痴汉,痴汉不会饶人。是亲不是亲,非亲却是亲。
美不美,乡中水;亲不亲,故乡人。相逢不饮空归去,洞中桃花也笑人。
为人莫作亏心事,半夜敲门心不惊。当时若不登高望,谁知东流海样深。
两人一条心,有钱堪买金;一人一条心,无钱难买针。
莺花犹怕春光老,岂可教人枉度春。红粉佳人休使老,风流浪子莫教贫。
黄金无假,阿魏无真。客来主不顾,应恐是痴人。
贫居闹市无人问,富在深山有远亲。谁人背后无人说,那个人前不说人。
有钱道真语,无钱语不真,不信但看筵中酒,杯杯先劝有钱人。
闹里有钱,静处安身。来如风雨,去似微尘。长江后浪催前浪,世人新人赶旧人。
近水楼台先得月,向阳花木早逢春。古人不见今时月,今月曾以照古人。
先到为君,后到为臣。莫道君行早,更有早行人。莫道直中直,须防仁不仁?
山中有直树,世上无直人。自恨枝无叶,莫怨太阳倾。大家都是命,半点不由人。
一年之计在于春,一日之计在于寅,一家之计在于和,一生之计在于勤。
责人之心责己,恕己之心恕人。守口如瓶,防意如城。宁可人负我,切莫我负人。
再三须重事,第一莫欺心。虎身犹可近,人毒不堪亲。来说是非者,便是是非人。
远水难救近火,远亲不如近邻。有茶有酒多兄弟,急难何曾见一人!
人情似纸张张薄,世事如棋局局新。山中也有千年树,世上难逢百岁人。
力弱休负重,言轻莫劝人。无钱休入众,遭难莫寻亲。
平生莫做皱眉事,世上应无切齿人。士者国之宝,儒为席上珍。
若要断酒法,醒眼看醉人。求人须求大丈夫,济人须济急时无。
渴时一滴如甘露,醉后添杯不如无。久住令人贱,频来亲也疏。
酒中不语真君子,财上分明大丈夫。积金千两,不如多买经书。
养子不教如养驴,养女不如养猪。有田不耕仓廪虚,有书不读子孙愚。
仓廪虚兮岁月乏,子孙愚兮礼义疏。同君一席话,胜读十年书。
人不通古今,马牛如襟裾。茫茫四海人无数,哪个男儿是丈夫!
美酒酿成缘好客,黄金散尽为收书。救人一命,胜造七级浮屠。
城门失火,殃及池鱼。庭前生瑞草,好事不如无。欲求生富贵,须下死功夫。
百年成之不足,一旦败之有余。人心似铁,官法如炉。善化不足,恶化有余。
水太清则无鱼,人太察则无谋。知者减半,愚者全无。在家由父,出嫁从夫。
痴人畏妇,贤女敬夫。是非终日有,不听自然无。宁可正而不足,不右邪而有余。
宁可信其有,不可信其无。竹篱茅舍风光好,僧院道房终不如。
命里有时终须有,命里无时莫强求。道院迎仙客,书堂隐相儒。
庭栽栖凤竹,池养化龙鱼。结交须胜己,似我不如无。但看三五日,相见不如初。
人情似水分高下,世事如云任卷舒。会说说都市,不会说说屋里。
磨刀恨不利,刀利伤人指。求财恨不多,财多害人子。
知足常足,终身不辱;知止常止,终身不耻。有福伤财,无福伤己。
差之毫厘,失之千里。若登高必自卑,若涉远必自迩。三思而行,再思可矣。
使口不如自走,求人不如求己。小时是兄弟,长大各乡里。
嫉财莫嫉食,怨生莫怨死。人见白头嗔,我见白头喜,多少少年亡,不到白头死。
墙有缝,壁有耳。好事不出门,恶事传千里。贼是小人,知过君子。
君子固穷,小人穷斯滥矣。贫穷自在,富贵多忧。不以我为德,反以我为仇。
宁可直中取,不向曲中求。人无远虑,必有近忧。
知我者,谓我心忧,不知我者,谓我何求?晴天不肯去,直待雨淋头。
成事莫说,覆水难收。是非只为多开口,烦脑皆皆因强出头。
忍一时之气,免得百日之忧。近来学得乌龟法,得缩头时且缩头。
惧法朝朝乐,欺公日日忧。人生一世,草木一春。
黑发不知勤学早,转眼便是白头翁。月过十五光明少,人到中年万事休。
儿孙自有儿孙福,莫为儿孙做马牛。人生不满百,常怀千岁忧。
今朝有酒今朝醉,明日愁来明日忧。路逢险处难回避,事到头来不自由。
药能医假病,酒不解真愁。人贫不语,水平不流。
一家养女百家求,一马不行百马忧。有花方酌酒,无月不登楼。
三杯通大道,一醉解千愁。深山毕竟藏猛虎,大海终须纳细流。
惜花须检点,爱月不梳头。大抵选他肌骨好,不擦红粉也风流。
受恩深处宜先退,得意浓时便可休。莫待是非来入耳,从前恩爱反成愁。
留得五湖明月在,不愁无处下金钓。休别有鱼处,莫恋浅滩头。
去时终须去,再三留不住。忍一句,息一怒;饶一着,退一步。
三十不豪,四十不富,五十将近寻死路。生不认魂,死不认尸。
一寸光阴一寸金,寸金难买寸光阴。父母恩深终有别,夫妻义重也分离。
人生似鸟同林宿,大限来时各自飞。人善被人欺,马善被人骑。
人无横财不富,马无夜草不肥。人恶人怕天不怕,人善人欺天不欺。
善恶到头终有报,只争来早与来迟。黄河尚有澄清日,岂可人无得运时!
得宠思辱,居安思危。念念有如临敌日,心心常似过桥时。
英雄行险道,富贵似花枝。人情莫道春光好,只怕秋来有冷时。
送君千时里,终须一别。但将冷眼观螃蟹,看你横行到几时。
见事莫说,问事不知。闲事休管,无事早归。假缎染就真红色,也被旁人说是非。
善事可作,恶事莫为。许人一物,千金不移。龙生龙子,虎生虎儿。
龙游浅水遭虾戏,虎落平阳被犬欺。一举首登龙虎榜,十年身到凤凰池。
十载寒窗无人问,一举成名天下知。酒债寻常行处有,人生七十古来稀。
养儿防老,积谷防饥。当家才知盐米贵,养子方知父母恩。
常将有日思无日,莫把无时当有时。时来风送滕王阁,运去雷轰荐福碑。
入门休问荣枯事,观看容颜便得知。官清书吏瘦,神灵庙祝肥。
息却雷霆之怒,罢却虎狼之威。饶人算之本,输人算之机。好言难得,恶语易施。
一言既出,驷马难追。道吾好者是吾贼,道吾恶者是吾师。
路逢侠客须呈剑,不是才人莫献诗。
三人行必有我师,择其善者而从之,其不善者而改之。
欲昌和顺须为善,要振家声在读书。少壮不努力,老大徒伤悲。
人有善愿,天必佑之。莫饮卯时酒,昏昏醉到酉;莫骂酉时妻,一夜受孤凄。
种麻得麻,种豆得豆。天网恢恢,疏而不漏。见官莫向前,做客莫在后。
宁添一斗,莫添一口。螳螂捕蝉,岂知黄雀在后。
不求金玉重重贵,但愿儿孙个个贤。一日夫妻,百世姻缘。
百世修来同船渡,千世修来共枕眠。杀人一万,自损三千。伤人一语,利如刀割。
枯木逢春犹再发,人无两度再少年。未晚先投宿,鸡鸣早看天。
将相顶头堪走马,公侯肚内好撑船。富人思来年,穷人思眼前。
世上若要人情好,赊去物件不取钱。死生有命,富贵在天。
击石原有火,不击乃无烟。为学始知道,不学亦枉然。莫笑他人老,终须还到老。
和得邻里好,犹如拾片宝。但能依本分,终须无烦恼。
大家做事寻常,小家做事慌张。大家礼义教子弟,小家凶恶训儿郎。
君子爱财,取之有道;贞妇爱色,纳之以礼。
善有善报,恶有恶报,不是不报,日子未到。万恶淫为首,百行孝当先。
人而无信,不知其可也。一人道虚,千人传实。凡事要好,须问三老。
若争小可,便失大道。家中不和邻里欺,邻里不和说是非。年年防饥,夜夜防盗。
好学者如禾如稻,不好学者如蒿如草。遇饮酒时须饮酒,得高歌处且高歌。
因风吹火,用力不多。不因渔父引,怎得见波涛。
无求到处人情好,不饮任他酒价高。知事少时烦恼少,识人多处是非多。
世间好语书说尽,天下名山僧占多。入山不怕伤人虎,只怕人情两面刀。
强中更有强中手,恶人终受恶人磨。会使不在家豪富,风流不在着衣多。
光阴似箭,日月如梭。天时不如地利,地利不如人和。黄金未为贵,安乐值钱多。
万般皆下品,唯有读书高。为善最乐,为恶难逃。羊有跪乳之恩,鸦有反哺之义。
孝顺还生孝顺子,忤逆还生忤逆儿,不信但看檐前水,点点滴在旧窝池。
隐恶扬善,执其两端。妻贤夫祸少,子孝父心宽。
人生知足何时足,到老偷闲且是闲。但有绿杨堪系马,处处有路透长安。
既堕釜甑,反顾何益?反复之水,收之实难。见者易,学者难。
莫将容易得,便作等闲看。用心计较般般错,退步思量事事宽。
道路各别,养家一般。从俭入奢易,从奢返俭难。
知音说与知音听,不是知音莫与弹。点石化为金,人心犹未足。信了肚,卖了屋。
谁人不爱子孙贤,谁人不爱千种粟,奈五行不是这般题目。
莫把真心空计较,儿孙自有儿孙福。天下无不是的父母,世上最难得者兄弟。
与人不和,劝人养鹅;与人不睦,劝人架屋。但行好事,莫问前程。
不交僧道,便是好人。河狭水激,人急计生。明知山有虎,莫向虎山行。
路不铲不平,事不为不成,人不劝不善,钟不敲不鸣。无钱方断酒,临老始看经。
点塔七层,不如暗处一灯。堂上二老是活佛,何用灵山朝世尊。
万事劝人休瞒昧,举头三尺有神明。但存方寸土,留与子孙耕。
灭却心头火,剔起佛前灯。惺惺常不足,蒙蒙作公卿。众星朗朗,不如孤月独明。
兄弟相害,不如友生。合理可作,小利莫争。牡丹花好空入目,枣花虽小结实成。
欺老莫欺少,欺少心不明。随分耕锄收地利,他时饱暖谢苍天。
得忍且忍,得耐且耐,不忍不耐,小事成大。相论逞英豪,家计渐渐消。
贤妇令夫贵,恶妇令夫败。一人有庆,兆民咸赖。人老心不老,人穷志不穷。
人无千日好,花无百日红。杀人可恕,情理难容。
乍富不知新受用,乍贫难改旧家风。座上客常满,怀中酒不空。
屋漏更遭连夜雨,行船又遇打头风。笋因落箨方为竹,鱼为奔波始化龙。
曾记少年骑竹马,看看又是白头翁。礼义生于富足,盗贼出于赌赙。
天上众星皆拱北,世间无水不朝东。君子安贫,达人知命。
良药苦口利于病,忠言逆耳利于行。顺天者存,逆天者亡。人为财死,鸟为食亡。
夫妻相和好,琴瑟与笙簧。有儿穷不久,无子富不长。善必寿考,恶必早亡。
爽口食多偏作病,快心事过恐生殃。富贵定要依本分,贫穷不必再思量。
画水无风空作浪,绣花虽好不闻香。
贪他一斗米,失却半年粮;争他一脚豚,反失一肘羊。
龙归晚洞云犹湿,鹿过春山草木香。平生只会说人短,何不回头把己量。
见善如不及,见恶如探汤。人穷志短,马瘦毛长。自家心里急,他人不知忙。
贫无达士将金赠,病有高人说药方。触来莫与竞,事过心清凉。
秋至满山多秀色,春来无处不花香。凡人不可貌相,海水不可斗量。
清清之水为土所防,济济之士酒所伤。蒿草之下还有兰香,崴茅茨之屋或有侯王。
无限朱门生饿殍,几多白屋出公卿。醉后乾坤大,壶中日月长。
万事皆已定,浮生空自忙。千里送毫毛,礼轻仁义重。世事明如镜,前程暗似漆。
架上碗儿轮流转,媳妇自有做婆时。人生一世,如驹过隙。
良田万倾,日食三升;大厦千间,夜眠八尺。千经万典,孝弟为先。
一字入公门,九牛拔不出。八字衙门向南开,有理无钱莫进来。
富从升合(升、合[音gě]都是较小的容积单位──笔者)起,贫因不算来。
万事不由人计较,一生都是命安排。家无读书子,官从何处来。
人间私语,天闻如雷;暗室亏心,神目如电。
一毫之恶,劝人莫作;一毫之善,与人方便。
欺人是祸,饶人是富;天眼昭昭,报应甚速。圣贤言语,神钦鬼服。
人各有心,心各有见。口说不如身逢,耳闻不如眼见。养兵千日,用兵一时。
国清才子贵,家富小儿娇。利剑割体疮犹合,恶语伤人恨不消。
有人堪出众,无衣懒出门。公道世间唯白发,贵人头上不曾饶。
为官须作相,及第必争先。苗从地发,树由枝分。
父子亲而家不退,兄弟和而家不分。官有公法,民有私约。
闲时不烧香,急时抱佛脚。幸生太平无事日,恐防年老不多时。
国乱思良将,家贫思贤妻。池塘积水须防旱,田土深耕足养家。
根深不怕风摇动,树正何愁月影斜。学在一人之下,用在万人之上。
一字为师,终身如父。忘恩负义,禽兽之徒。劝君莫将油炒菜,留与儿孙夜读书。
书中自有千钟粟,书中自有颜如玉。莫怨天来莫怨人,五行八字命生成。
莫怨自己穷,穷要穷得干净;莫羡他人富,富要富得清高。
别人骑马我骑驴,仔细思量我不如,等我回头看,还有挑脚汉。
路外有饥人,家中有剩饭,积德与儿孙,要广行方便。作善鬼神钦,作恶遭天遣。
积钱积谷不如积德,买田买地不如买书。一日春工十日粮,十日春工半年粮。
疏懒人没吃,勤俭粮满仓。人亲财不亲,财利要分清。
十分伶俐使七分,常留三分与儿孙,若要十分都使尽,远在儿孙近在身。
君子乐得做君子,小人枉自做小人。
好学者则庶民之子为公卿,不好学者则公卿之子为庶民。惜钱莫教子,护短莫从师。
记得旧文章,便是新举子。人在家是中坐,祸从天上落。但求心无愧,不怕有后灾。
只有和气去迎人,那有相打得太平?忠厚自有忠厚报,豪强一定受官刑。
人到公门正好修,留些阴德在后头。为人何必争高下,一旦无命万事休。
山高不算高,人心比天高,白水变酒卖,还嫌猪无糟。贫寒休要怨,富贵不须骄。
善恶随人作,祸福自已招。奉劝君子,各宜守己,只此呈示,万无一失

(ZT)《解剖PetShop》系列

http://www.brucezhang.com/?page_id=69

PetShop是一个范例,微软用它来展示.Net企业系统开发的能力。业界有许多.Net与J2EE之争,许多数据是从微软的PetShop和Sun的PetStore而来。这种争论不可避免带有浓厚的商业色彩,对于我们开发人员而言,没有必要过多关注。然而PetShop随着版本的不断更新,至现在基于.Net 2.0的PetShop4.0为止,整个设计逐渐变得成熟而优雅,却又很多可以借鉴之处。PetShop是一个小型的项目,系统架构与代码都比较简单,却也凸现了许多颇有价值的设计与开发理念。本系列试图对PetShop作一个全方位的解剖,依据的代码是PetShop4.0,可以从链接http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/bdasamppet4.asp中获得。

1、PetShop的系统架构设计
2、PetShop数据访问层之数据库访问设计
3、PetShop数据访问层之消息处理
4、PetShop之ASP.NET缓存
5、PetShop之业务逻辑层设计
6、PetShop之表示层设计

 

 

一、PetShop的系统架构设计

在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。微软推荐的分层式结构一般分为三层,从下至上分别为:数据访问层、业务逻辑层(又或成为领域层)、表示层,如图所示:

ps01.gif
图一:三层的分层式结构

数据访问层:有时候也称为是持久层,其功能主要是负责数据库的访问。简单的说法就是实现对数据表的Select,Insert,Update,Delete的操作。如果要加入ORM的元素,那么就会包括对象和数据表之间的mapping,以及对象实体的持久化。在PetShop的数据访问层中,并没有使用ORM,从而导致了代码量的增加,可以看作是整个设计实现中的一大败笔。

业务逻辑层:是整个系统的核心,它与这个系统的业务(领域)有关。以PetShop为例,业务逻辑层的相关设计,均和网上宠物店特有的逻辑相关,例如查询宠物,下订单,添加宠物到购物车等等。如果涉及到数据库的访问,则调用数据访问层。

表示层:是系统的UI部分,负责使用者与整个系统的交互。在这一层中,理想的状态是不应包括系统的业务逻辑。表示层中的逻辑代码,仅与界面元素有关。在PetShop中,是利用ASP.Net来设计的,因此包含了许多Web控件和相关逻辑。

分层式结构究竟其优势何在?Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:
1、开发人员可以只关注整个结构中的其中某一层;
2、可以很容易的用新的实现来替换原有层次的实现;
3、可以降低层与层之间的依赖;
4、有利于标准化;
5、利于各层逻辑的复用。

概括来说,分层式设计可以达至如下目的:分散关注、松散耦合、逻辑复用、标准定义。

一个好的分层式结构,可以使得开发人员的分工更加明确。一旦定义好各层次之间的接口,负责不同逻辑设计的开发人员就可以分散关注,齐头并进。例如UI人员只需考虑用户界面的体验与操作,领域的设计人员可以仅关注业务逻辑的设计,而数据库设计人员也不必为繁琐的用户交互而头疼了。每个开发人员的任务得到了确认,开发进度就可以迅速的提高。

松散耦合的好处是显而易见的。如果一个系统没有分层,那么各自的逻辑都紧紧纠缠在一起,彼此间相互依赖,谁都是不可替换的。一旦发生改变,则牵一发而动全身,对项目的影响极为严重。降低层与层间的依赖性,既可以良好地保证未来的可扩展,在复用性上也是优势明显。每个功能模块一旦定义好统一的接口,就可以被各个模块所调用,而不用为相同的功能进行重复地开发。

进行好的分层式结构设计,标准也是必不可少的。只有在一定程度的标准化基础上,这个系统才是可扩展的,可替换的。而层与层之间的通信也必然保证了接口的标准化。

“金无足赤,人无完人”,分层式结构也不可避免具有一些缺陷:
1、降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。
2、有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。

前面提到,PetShop的表示层是用ASP.Net设计的,也就是说,它应是一个BS系统。在.Net中,标准的BS分层式结构如下图所示:

ps02.gif
图二:.Net中标准的BS分层式结构

随着PetShop版本的更新,其分层式结构也在不断的完善,例如PetShop2.0,就没有采用标准的三层式结构,如图三:

ps03.gif
图三:PetShop 2.0的体系架构

从图中我们可以看到,并没有明显的数据访问层设计。这样的设计虽然提高了数据访问的性能,但也同时导致了业务逻辑层与数据访问的职责混乱。一旦要求支持的数据库发生变化,或者需要修改数据访问的逻辑,由于没有清晰的分层,会导致项目作大的修改。而随着硬件系统性能的提高,以及充分利用缓存、异步处理等机制,分层式结构所带来的性能影响几乎可以忽略不计。

PetShop3.0纠正了此前层次不明的问题,将数据访问逻辑作为单独的一层独立出来:

ps04.gif
图四:PetShop 3.0的体系架构

PetShop4.0基本上延续了3.0的结构,但在性能上作了一定的改进,引入了缓存和异步处理机制,同时又充分利用了ASP.Net 2.0的新功能MemberShip,因此PetShop4.0的系统架构图如下所示:

ps05.gif
图五:PetShop 4.0的体系架构

比较3.0和4.0的系统架构图,其核心的内容并没有发生变化。在数据访问层(DAL)中,仍然采用DAL Interface抽象出数据访问逻辑,并以DAL Factory作为数据访问层对象的工厂模块。对于DAL Interface而言,分别有支持MS-SQL的SQL Server DAL和支持Oracle的Oracle DAL具体实现。而Model模块则包含了数据实体对象。其详细的模块结构图如下所示:

ps06.gif
图六:数据访问层的模块结构图

可以看到,在数据访问层中,完全采用了“面向接口编程”思想。抽象出来的IDAL模块,脱离了与具体数据库的依赖,从而使得整个数据访问层利于数据库迁移。DALFactory模块专门管理DAL对象的创建,便于业务逻辑层访问。SQLServerDAL和OracleDAL模块均实现IDAL模块的接口,其中包含的逻辑就是对数据库的Select,Insert,Update和Delete操作。因为数据库类型的不同,对数据库的操作也有所不同,代码也会因此有所区别。

此外,抽象出来的IDAL模块,除了解除了向下的依赖之外,对于其上的业务逻辑层,同样仅存在弱依赖关系,如下图所示:

ps07.gif
图七:业务逻辑层的模块结构图

图七中BLL是业务逻辑层的核心模块,它包含了整个系统的核心业务。在业务逻辑层中,不能直接访问数据库,而必须通过数据访问层。注意图中对数据访问业务的调用,是通过接口模块IDAL来完成的。既然与具体的数据访问逻辑无关,则层与层之间的关系就是松散耦合的。如果此时需要修改数据访问层的具体实现,只要不涉及到IDAL的接口定义,那么业务逻辑层就不会受到任何影响。毕竟,具体实现的SQLServerDAL和OracalDAL根本就与业务逻辑层没有半点关系。

因为在PetShop 4.0中引入了异步处理机制。插入订单的策略可以分为同步和异步,两者的插入策略明显不同,但对于调用者而言,插入订单的接口是完全一样的,所以PetShop 4.0中设计了IBLLStrategy模块。虽然在IBLLStrategy模块中,仅仅是简单的IOrderStategy,但同时也给出了一个范例和信息,那就是在业务逻辑的处理中,如果存在业务操作的多样化,或者是今后可能的变化,均应利用抽象的原理。或者使用接口,或者使用抽象类,从而脱离对具体业务的依赖。不过在PetShop中,由于业务逻辑相对简单,这种思想体现得不够明显。也正因为此,PetShop将核心的业务逻辑都放到了一个模块BLL中,并没有将具体的实现和抽象严格的按照模块分开。所以表示层和业务逻辑层之间的调用关系,其耦合度相对较高:

ps08.gif
图八:表示层的模块结构图

在图五中,各个层次中还引入了辅助的模块,如数据访问层的Messaging模块,是为异步插入订单的功能提供,采用了MSMQ(Microsoft Messaging Queue)技术。而表示层的CacheDependency则提供缓存功能。这些特殊的模块,我会在此后的文章中详细介绍。

 

 

《解剖PetShop》系列之二

二、PetShop数据访问层之数据库访问设计
在系列一中,我从整体上分析了PetShop的架构设计,并提及了分层的概念。从本部分开始,我将依次对各层进行代码级的分析,以求获得更加细致而深入的理解。在PetShop 4.0中,由于引入了ASP.Net 2.0的一些新特色,所以数据层的内容也更加的广泛和复杂,包括:数据库访问、Messaging、MemberShip、Profile四部分。在系列二中,我将介绍有关数据库访问的设计。

在PetShop中,系统需要处理的数据库对象分为两类:一是数据实体,对应数据库中相应的数据表。它们没有行为,仅用于表现对象的数据。这些实体类都被放到Model程序集中,例如数据表Order对应的实体类OrderInfo,其类图如下: 

ps09.gif

这些对象并不具有持久化的功能,简单地说,它们是作为数据的载体,便于业务逻辑针对相应数据表进行读/写操作。虽然这些类的属性分别映射了数据表的列,而每一个对象实例也恰恰对应于数据表的每一行,但这些实体类却并不具备对应的数据库访问能力。

由于数据访问层和业务逻辑层都将对这些数据实体进行操作,因此程序集Model会被这两层的模块所引用。

第二类数据库对象则是数据的业务逻辑对象。这里所指的业务逻辑,并非业务逻辑层意义上的领域(domain)业务逻辑(从这个意义上,我更倾向于将业务逻辑层称为“领域逻辑层”),一般意义上说,这些业务逻辑即为基本的数据库操作,包括Select,Insert,Update和Delete。由于这些业务逻辑对象,仅具有行为而与数据无关,因此它们均被抽象为一个单独的接口模块IDAL,例如数据表Order对应的接口IOrder: 

ps10.gif

将数据实体与相关的数据库操作分离出来,符合面向对象的精神。首先,它体现了“职责分离”的原则。将数据实体与其行为分开,使得两者之间依赖减弱,当数据行为发生改变时,并不影响Model模块中的数据实体对象,避免了因一个类职责过多、过大,从而导致该类的引用者发生“灾难性”的影响。其次,它体现了“抽象”的精神,或者说是“面向接口编程”的最佳体现。抽象的接口模块IDAL,与具体的数据库访问实现完全隔离。这种与实现无关的设计,保证了系统的可扩展性,同时也保证了数据库的可移植性。在PetShop中,可以支持SQL Server和Oracle,那么它们具体的实现就分别放在两个不同的模块SQLServerDAL、OracleDAL中。

以Order为例,在SQLServerDAL、OracleDAL两个模块中,有不同的实现,但它们同时又都实现了IOrder接口,如图: 

ps11.gif

从数据库的实现来看,PetShop体现出了没有ORM框架的臃肿与丑陋。由于要对数据表进行Insert和Select操作,以SQL Server为例,就使用了SqlCommand,SqlParameter,SqlDataReader等对象,以完成这些操作。尤其复杂的是Parameter的传递,在PetShop中,使用了大量的字符串常量来保存参数的名称。此外,PetShop还专门为SQL Server和Oracle提供了抽象的Helper类,包装了一些常用的操作,如ExecuteNonQuery、ExecuteReader等方法。

在没有ORM的情况下,使用Helper类是一个比较好的策略,利用它来完成数据库基本操作的封装,可以减少很多和数据库操作有关的代码,这体现了对象复用的原则。PetShop将这些Helper类统一放到DBUtility模块中,不同数据库的Helper类暴露的方法基本相同,只除了一些特殊的要求,例如Oracle中处理bool类型的方式就和SQL Server不同,从而专门提供了OraBit和OraBool方法。此外,Helper类中的方法均为static方法,以利于调用。OracleHelper的类图如下: 

ps12.gif

对于数据访问层来说,最头疼的是SQL语句的处理。在早期的CS结构中,由于未采用三层式架构设计,数据访问层和业务逻辑层是紧密糅合在一起的,因此,SQL语句遍布与系统的每一个角落。这给程序的维护带来极大的困难。此外,由于Oracle使用的是PL-SQL,而SQL Server和Sybase等使用的是T-SQL,两者虽然都遵循了标准SQL的语法,但在很多细节上仍有区别,如果将SQL语句大量的使用到程序中,无疑为可能的数据库移植也带来了困难。

最好的方法是采用存储过程。这种方法使得程序更加整洁,此外,由于存储过程可以以数据库脚本的形式存在,也便于移植和修改。但这种方式仍然有缺陷。一是存储过程的测试相对困难。虽然有相应的调试工具,但比起对代码的调试而言,仍然比较复杂且不方便。二是对系统的更新带来障碍。如果数据库访问是由程序完成,在.Net平台下,我们仅需要在修改程序后,将重新编译的程序集xcopy到部署的服务器上即可。如果使用了存储过程,出于安全的考虑,必须有专门的DBA重新运行存储过程的脚本,部署的方式受到了限制。

我曾经在一个项目中,利用一个专门的表来存放SQL语句。如要使用相关的SQL语句,就利用关键字搜索获得对应语句。这种做法近似于存储过程的调用,但却避免了部署上的问题。然而这种方式却在性能上无法得到保证。它仅适合于SQL语句较少的场景。不过,利用良好的设计,我们可以为各种业务提供不同的表来存放SQL语句。同样的道理,这些SQL语句也可以存放到XML文件中,更有利于系统的扩展或修改。不过前提是,我们需要为它提供专门的SQL语句管理工具。

SQL语句的使用无法避免,如何更好的应用SQL语句也无定论,但有一个原则值得我们遵守,就是“应该尽量让SQL语句尽存在于数据访问层的具体实现中”。

当然,如果应用ORM,那么一切就变得不同了。因为ORM框架已经为数据访问提供了基本的Select,Insert,Update和Delete操作了。例如在NHibernate中,我们可以直接调用ISession对象的Save方法,来Insert(或者说是Create)一个数据实体对象:
public void Insert(OrderInfo order)
{
    ISession s = Sessions.GetSession();
    ITransaction trans = null;
    try
    {
    trans = s.BeginTransaction();
      s.Save( order);
      trans.Commit();
    }
    finally
    {
      s.Close();
    }
}

没有SQL语句,也没有那些烦人的Parameters,甚至不需要专门去考虑事务。此外,这样的设计,也是与数据库无关的,NHibernate可以通过Dialect(方言)的机制支持不同的数据库。唯一要做的是,我们需要为OrderInfo定义hbm文件。

当然,ORM框架并非是万能的,面对纷繁复杂的业务逻辑,它并不能完全消灭SQL语句,以及替代复杂的数据库访问逻辑,但它却很好的体现了“80/20(或90/10)法则”(也被称为“帕累托法则”),也就是说:花比较少(10%-20%)的力气就可以解决大部分(80%-90%)的问题,而要解决剩下的少部分问题则需要多得多的努力。至少,那些在数据访问层中占据了绝大部分的CRUD操作,通过利用ORM框架,我们就仅需要付出极少数时间和精力来解决它们了。这无疑缩短了整个项目开发的周期。

还是回到对PetShop的讨论上来。现在我们已经有了数据实体,数据对象的抽象接口和实现,可以说有关数据库访问的主体就已经完成了。留待我们的还有两个问题需要解决:
1、数据对象创建的管理
2、利于数据库的移植

在PetShop中,要创建的数据对象包括Order,Product,Category,Inventory,Item。在前面的设计中,这些对象已经被抽象为对应的接口,而其实现则根据数据库的不同而有所不同。也就是说,创建的对象有多种类别,而每种类别又有不同的实现,这是典型的抽象工厂模式的应用场景。而上面所述的两个问题,也都可以通过抽象工厂模式来解决。标准的抽象工厂模式类图如下: 

ps13.gif

例如,创建SQL Server的Order对象如下:
PetShopFactory factory = new SQLServerFactory();
IOrder = factory.CreateOrder();

要考虑到数据库的可移植性,则factory必须作为一个全局变量,并在主程序运行时被实例化。但这样的设计虽然已经达到了“封装变化”的目的,但在创建PetShopFactory对象时,仍不可避免的出现了具体的类SQLServerFactory,也即是说,程序在这个层面上产生了与SQLServerFactory的强依赖。一旦整个系统要求支持Oracle,那么还需要修改这行代码为:
PetShopFactory factory = new OracleFactory();

修改代码的这种行为显然是不可接受的。解决的办法是“依赖注入”。“依赖注入”的功能通常是用专门的IoC容器提供的,在Java平台下,这样的容器包括Spring,PicoContainer等。而在.Net平台下,最常见的则是Spring.Net。不过,在PetShop系统中,并不需要专门的容器来实现“依赖注入”,简单的做法还是利用配置文件和反射功能来实现。也就是说,我们可以在web.config文件中,配置好具体的Factory对象的完整的类名。然而,当我们利用配置文件和反射功能时,具体工厂的创建就显得有些“画蛇添足”了,我们完全可以在配置文件中,直接指向具体的数据库对象实现类,例如PetShop.SQLServerDAL.IOrder。那么,抽象工厂模式中的相关工厂就可以简化为一个工厂类了,所以我将这种模式称之为“具有简单工厂特质的抽象工厂模式”,其类图如下: 

ps14.gif

DataAccess类完全取代了前面创建的工厂类体系,它是一个sealed类,其中创建各种数据对象的方法,均为静态方法。之所以能用这个类达到抽象工厂的目的,是因为配置文件和反射的运用,如下的代码片断所示:
public sealed class DataAccess
{
 // Look up the DAL implementation we should be using
    private static readonly string path = ConfigurationManager.AppSettings[”WebDAL”];
    private static readonly string orderPath = ConfigurationManager.AppSettings[”OrdersDAL”];

 public static PetShop.IDAL.IOrder CreateOrder()
 {
         string className = orderPath + “.Order”;
         return (PetShop.IDAL.IOrder)Assembly.Load(orderPath).CreateInstance(className);
    }
}

在PetShop中,这种依赖配置文件和反射创建对象的方式极其常见,包括IBLLStategy、CacheDependencyFactory等等。这些实现逻辑散布于整个PetShop系统中,在我看来,是可以在此基础上进行重构的。也就是说,我们可以为整个系统提供类似于“Service Locator”的实现:
public static class ServiceLocator
{
 private static readonly string dalPath = ConfigurationManager.AppSettings[”WebDAL”];
    private static readonly string orderPath = ConfigurationManager.AppSettings[”OrdersDAL”];
 //……
 private static readonly string orderStategyPath = ConfigurationManager.AppSettings[”OrderStrategyAssembly”];

 public static object LocateDALObject(string className)
 {
  string fullPath = dalPath + “.” + className;
  return Assembly.Load(dalPath).CreateInstance(fullPath);
 }
public static object LocateDALOrderObject(string className)
 {
  string fullPath = orderPath + “.” + className;
  return Assembly.Load(orderPath).CreateInstance(fullPath);
 }
public static object LocateOrderStrategyObject(string className)
 {
  string fullPath = orderStategyPath + “.” + className;
  return Assembly.Load(orderStategyPath).CreateInstance(fullPath);
 }
 //……
}

那么和所谓“依赖注入”相关的代码都可以利用ServiceLocator来完成。例如类DataAccess就可以简化为:
public sealed class DataAccess
{
 public static PetShop.IDAL.IOrder CreateOrder()
 {
         return (PetShop.IDAL.IOrder)ServiceLocator. LocateDALOrderObject(”Order”);
    }
}

通过ServiceLocator,将所有与配置文件相关的namespace值统一管理起来,这有利于各种动态创建对象的管理和未来的维护。

 

 

《解剖PetShop》系列之三

三、PetShop数据访问层之消息处理

    在进行系统设计时,除了对安全、事务等问题给与足够的重视外,性能也是一个不可避免的问题所在,尤其是一个B/S结构的软件系统,必须充分地考虑访问量、数据流量、服务器负荷的问题。解决性能的瓶颈,除了对硬件系统进行升级外,软件设计的合理性尤为重要。
    在前面我曾提到,分层式结构设计可能会在一定程度上影响数据访问的性能,然而与它给设计人员带来的好处相比,几乎可以忽略。要提供整个系统的性能,还可以从数据库的优化着手,例如连接池的使用、建立索引、优化查询策略等等,例如在PetShop中就利用了数据库的Cache,对于数据量较大的订单数据,则利用分库的方式为其单独建立了Order和Inventory数据库。而在软件设计上,比较有用的方式是利用多线程与异步处理方式。
    在PetShop4.0中,使用了Microsoft Messaging Queue(MSMQ)技术来完成异步处理,利用消息队列临时存放要插入的数据,使得数据访问因为不需要访问数据库从而提供了访问性能,至于队列中的数据,则等待系统空闲的时候再进行处理,将其最终插入到数据库中。
    PetShop4.0中的消息处理,主要分为如下几部分:消息接口IMessaging、消息工厂MessagingFactory、MSMQ实现MSMQMessaging以及数据后台处理应用程序OrderProcessor。
从模块化分上,PetShop自始自终地履行了“面向接口设计”的原则,将消息处理的接口与实现分开,并通过工厂模式封装消息实现对象的创建,以达到松散耦合的目的。
    由于在PetShop中仅对订单的处理使用了异步处理方式,因此在消息接口IMessaging中,仅定义了一个IOrder接口,其类图如下:

ps01.gif

    在对消息接口的实现中,考虑到未来的扩展中会有其他的数据对象会使用MSMQ,因此定义了一个Queue的基类,实现消息Receive和Send的基本操作:
public virtual object Receive()
{
      try
     {
          using (Message message = queue.Receive(timeout, transactionType))
             return message;
      }
      catch (MessageQueueException mqex)
     {
          if (mqex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout)
             throw new TimeoutException();
                throw;
      }
}
public virtual void Send(object msg)
{
      queue.Send(msg, transactionType);
}
    其中queue对象是System.Messaging.MessageQueue类型,作为存放数据的队列。MSMQ队列是一个可持久的队列,因此不必担心用户不间断地下订单会导致订单数据的丢失。在PetShopQueue设置了timeout值,OrderProcessor会根据timeout值定期扫描队列中的订单数据。
    MSMQMessaging模块中,Order对象实现了IMessaging模块中定义的接口IOrder,同时它还继承了基类PetShopQueue,其定义如下:
    public class Order:PetShopQueue, PetShop.IMessaging.IOrder
    方法的实现代码如下:
    public new OrderInfo Receive()
    {
        // This method involves in distributed transaction and need Automatic Transaction type
        base.transactionType = MessageQueueTransactionType.Automatic;
        return (OrderInfo)((Message)base.Receive()).Body;
    }     public OrderInfo Receive(int timeout)
    {
        base.timeout = TimeSpan.FromSeconds(Convert.ToDouble(timeout));
        return Receive();
    }

    public void Send(OrderInfo orderMessage)
    {
        // This method does not involve in distributed transaction and optimizes performance using Single type
        base.transactionType = MessageQueueTransactionType.Single;
        base.Send(orderMessage);
    }
    所以,最后的类图应该如下: 

ps02.gif

    注意在Order类的Receive()方法中,是用new关键字而不是override关键字来重写其父类PetShopQueue的Receive()虚方法。因此,如果是实例化如下的对象,将会调用PetShopQueue的Receive()方法,而不是子类Order的Receive()方法:
    PetShopQueue queue = new Order();
    queue.Receive();
    从设计上来看,由于PetShop采用“面向接口设计”的原则,如果我们要创建Order对象,应该采用如下的方式:
    IOrder order = new Order();
    order.Receive();
    考虑到IOrder的实现有可能的变化,PetShop仍然利用了工厂模式,将IOrder对象的创建用专门的工厂模块进行了封装: 

ps03.gif

    在类QueueAccess中,通过CreateOrder()方法利用反射技术创建正确的IOrder类型对象:
    public static PetShop.IMessaging.IOrder CreateOrder()
    {
        string className = path + “.Order”;
        return PetShop.IMessaging.IOrder)Assembly.Load(path).CreateInstance(className);
    }
    path的值通过配置文件获取:
    private static readonly string path = ConfigurationManager.AppSettings[”OrderMessaging”];
    而配置文件中,OrderMessaging的值设置如下:
    <add key=”OrderMessaging” value=”PetShop.MSMQMessaging”/>
    之所以利用工厂模式来负责对象的创建,是便于在业务层中对其调用,例如在BLL模块中OrderAsynchronous类:
public class OrderAsynchronous : IOrderStrategy
{       
    private static readonly PetShop.IMessaging.IOrder asynchOrder = PetShop.MessagingFactory.QueueAccess.CreateOrder();
    public void Insert(PetShop.Model.OrderInfo order)
{
        asynchOrder.Send(order);
    }
}
    一旦IOrder接口的实现发生变化,这种实现方式就可以使得客户仅需要修改配置文件,而不需要修改代码,如此就可以避免程序集的重新编译和部署,使得系统能够灵活应对需求的改变。例如定义一个实现IOrder接口的SpecialOrder,则可以新增一个模块,如PetShop.SpecialMSMQMessaging,而类名则仍然为Order,那么此时我们仅需要修改配置文件中OrderMessaging的值即可:
    <add key=”OrderMessaging” value=”PetShop.SpecialMSMQMessaging”/>
    OrderProcessor是一个控制台应用程序,不过可以根据需求将其设计为Windows Service。它的目的就是接收消息队列中的订单数据,然后将其插入到Order和Inventory数据库中。它利用了多线程技术,以达到提高系统性能的目的。
    在OrderProcessor应用程序中,主函数Main用于控制线程,而核心的执行任务则由方法ProcessOrders()实现:
    private static void ProcessOrders()
    {
        // the transaction timeout should be long enough to handle all of orders in the batch
        TimeSpan tsTimeout = TimeSpan.FromSeconds(Convert.ToDouble(transactionTimeout * batchSize));

        Order order = new Order();
        while (true)
        {
            // queue timeout variables
            TimeSpan datetimeStarting = new TimeSpan(DateTime.Now.Ticks);
            double elapsedTime = 0;

            int processedItems = 0;

            ArrayList queueOrders = new ArrayList();

            using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required, tsTimeout))
            {
                // Receive the orders from the queue
                for (int j = 0; j < batchSize; j++)
                {
                    try
                    {
                        //only receive more queued orders if there is enough time
                        if ((elapsedTime + queueTimeout + transactionTimeout) < tsTimeout.TotalSeconds)
                        {
                            queueOrders.Add(order.ReceiveFromQueue(queueTimeout));
                        }
                        else
                        {
                            j = batchSize;   // exit loop
                        }

                        //update elapsed time
                        elapsedTime = new TimeSpan(DateTime.Now.Ticks).TotalSeconds - datetimeStarting.TotalSeconds;
                    }
                    catch (TimeoutException)
                    {
                        //exit loop because no more messages are waiting
                        j = batchSize;
                    }
                }
                //process the queued orders
                for (int k = 0; k < queueOrders.Count; k++)
                {
                    order.Insert((OrderInfo)queueOrders[k]);
                    processedItems++;
                    totalOrdersProcessed++;
                }

                //batch complete or MSMQ receive timed out
                ts.Complete();
            }

            Console.WriteLine("(Thread Id " + Thread.CurrentThread.ManagedThreadId + ") batch finished, " + processedItems + " items, in " + elapsedTime.ToString() + " seconds.");
        }
    }
    首先,它会通过PetShop.BLL.Order类的公共方法ReceiveFromQueue()来获取消息队列中的订单数据,并将其放入到一个ArrayList对象中,然而再调用PetShop.BLL.Order类的Insert方法将其插入到Order和Inventory数据库中。
    在PetShop.BLL.Order类中,并不是直接执行插入订单的操作,而是调用了IOrderStrategy接口的Insert()方法:
public void Insert(OrderInfo order)
{
    // Call credit card procesor
    ProcessCreditCard(order);

    // Insert the order (a)synchrounously based on configuration
    orderInsertStrategy.Insert(order);
}
    在这里,运用了一个策略模式,类图如下所示: 

ps05.gif

    在PetShop.BLL.Order类中,仍然利用配置文件来动态创建IOrderStategy对象:
private static readonly PetShop.IBLLStrategy.IOrderStrategy orderInsertStrategy = LoadInsertStrategy();
private static PetShop.IBLLStrategy.IOrderStrategy LoadInsertStrategy()
{
    // Look up which strategy to use from config file
    string path = ConfigurationManager.AppSettings[”OrderStrategyAssembly”];
    string className = ConfigurationManager.AppSettings[”OrderStrategyClass”];

    // Using the evidence given in the config file load the appropriate assembly and class
    return (PetShop.IBLLStrategy.IOrderStrategy)Assembly.Load(path).CreateInstance(className);
}
    由于OrderProcessor是一个单独的应用程序,因此它使用的配置文件与PetShop不同,是存放在应用程序的App.config文件中,在该文件中,对IOrderStategy的配置为:
    <add key=”OrderStrategyAssembly” value=”PetShop.BLL” />
    <add key=”OrderStrategyClass” value=”PetShop.BLL.OrderSynchronous” />   
    因此,以异步方式插入订单的流程如下图所示: 

ps06.gif

    Microsoft Messaging Queue(MSMQ)技术除用于异步处理以外,它主要还是一种分布式处理技术。分布式处理中,一个重要的技术要素就是有关消息的处理,而在System.Messaging命名空间中,已经提供了Message类,可以用于承载消息的传递,前提上消息的发送方与接收方在数据定义上应有统一的接口规范。
    MSMQ在分布式处理的运用,在我参与的项目中已经有了实现。在为一个汽车制造商开发一个大型系统时,分销商Dealer作为.Net客户端,需要将数据传递到管理中心,并且该数据将被Oracle的EBS(E-Business System)使用。由于分销商管理系统(DMS)采用的是C/S结构,数据库为SQL Server,而汽车制造商管理中心的EBS数据库为Oracle。这里就涉及到两个系统之间数据的传递。
    实现架构如下:

ps07.gif

     首先Dealer的数据通过MSMQ传递到MSMQ Server,此时可以将数据插入到SQL Server数据库中,同时利用FTP将数据传送到专门的文件服务器上。然后利用IBM的EAI技术(企业应用集成,Enterprise Application Itegration)定期将文件服务器中的文件,利用接口规范写入到EAI数据库服务器中,并最终写道EBS的Oracle数据库中。
    上述架构是一个典型的分布式处理结构,而技术实现的核心就是MSMQ和EAI。由于我们已经定义了统一的接口规范,在通过消息队列形成文件后,此时的数据就已经与平台无关了,使得在.Net平台下的分销商管理系统能够与Oracle的EBS集成起来,完成数据的处理。

 

《解剖PetShop》系列之四

四 PetShop之ASP.NET缓存

如果对微型计算机硬件系统有足够的了解,那么我们对于Cache这个名词一定是耳熟能详的。在CPU以及主板的芯片中,都引入了这种名为高速缓冲存储器(Cache)的技术。因为Cache的存取速度比内存快,因而引入Cache能够有效的解决CPU与内存之间的速度不匹配问题。硬件系统可以利用Cache存储CPU访问概率高的那些数据,当CPU需要访问这些数据时,可以直接从Cache中读取,而不必访问存取速度相对较慢的内存,从而提高了CPU的工作效率。软件设计借鉴了硬件设计中引入缓存的机制以改善整个系统的性能,尤其是对于一个数据库驱动的Web应用程序而言,缓存的利用是不可或缺的,毕竟,数据库查询可能是整个Web站点中调用最频繁但同时又是执行最缓慢的操作之一,我们不能被它老迈的双腿拖缓我们前进的征程。缓存机制正是解决这一缺陷的加速器。

4.1  ASP.NET缓存概述

作为.Net框架下开发Web应用程序的主打产品,ASP.NET充分考虑了缓存机制。通过某种方法,将系统需要的数据对象、Web页面存储在内存中,使得Web站点在需要获取这些数据时,不需要经过繁琐的数据库连接、查询和复杂的逻辑运算,就可以“触手可及”,如“探囊取物”般容易而快速,从而提高整个Web系统的性能。

ASP.NET提供了两种基本的缓存机制来提供缓存功能。一种是应用程序缓存,它允许开发者将程序生成的数据或报表业务对象放入缓存中。另外一种缓存机制是页输出缓存,利用它,可以直接获取存放在缓存中的页面,而不需要经过繁杂的对该页面的再次处理。

应用程序缓存其实现原理说来平淡无奇,仅仅是通过ASP.NET管理内存中的缓存空间。放入缓存中的应用程序数据对象,以键/值对的方式存储,这便于用户在访问缓存中的数据项时,可以根据key值判断该项是否存在缓存中。

放入在缓存中的数据对象其生命周期是受到限制的,即使在整个应用程序的生命周期里,也不能保证该数据对象一直有效。ASP.NET可以对应用程序缓存进行管理,例如当数据项无效、过期或内存不足时移除它们。此外,调用者还可以通过CacheItemRemovedCallback委托,定义回调方法使得数据项被移除时能够通知用户。

在.Net Framework中,应用程序缓存通过System.Web.Caching.Cache类实现。它是一个密封类,不能被继承。对于每一个应用程序域,都要创建一个Cache类的实例,其生命周期与应用程序域的生命周期保持一致。我们可以利用Add或Insert方法,将数据项添加到应用程序缓存中,如下所示:
Cache[”First”] = “First Item”;
Cache.Insert(”Second”, “Second Item”);

我们还可以为应用程序缓存添加依赖项,使得依赖项发生更改时,该数据项能够从缓存中移除:
string[] dependencies = {”Second”};
Cache.Insert(”Third”, “Third Item”,
new System.Web.Caching.CacheDependency(null, dependencies));

与之对应的是缓存中数据项的移除。前面提到ASP.NET可以自动管理缓存中项的移除,但我们也可以通过代码编写的方式显式的移除相关的数据项:
Cache.Remove(”First”);

相对于应用程序缓存而言,页输出缓存的应用更为广泛。它可以通过内存将处理后的ASP.NET页面存储起来,当客户端再一次访问该页面时,可以省去页面处理的过程,从而提高页面访问的性能,以及Web服务器的吞吐量。例如,在一个电子商务网站里,用户需要经常查询商品信息,这个过程会涉及到数据库访问以及搜索条件的匹配,在数据量较大的情况下,如此的搜索过程是较为耗时的。此时,利用页输出缓存就可以将第一次搜索得到的查询结果页存储在缓存中。当用户第二次查询时,就可以省去数据查询的过程,减少页面的响应时间。

页输出缓存分为整页缓存和部分页缓存。我们可以通过@OutputCache指令完成对Web页面的输出缓存。它主要包含两个参数:Duration和VaryByParam。Duration参数用于设置页面或控件进行缓存的时间,其单位为秒。如下的设置表示缓存在60秒内有效:
< %@ OutputCache Duration=“60“ VaryByParam=“none“ %>

只要没有超过Duration设置的期限值,当用户访问相同的页面或控件时,就可以直接在缓存中获取。
使用VaryByParam参数可以根据设置的参数值建立不同的缓存。例如在一个输出天气预报结果的页面中,如果需要为一个ID为txtCity的TextBox控件建立缓存,其值将显示某城市的气温,那么我们可以进行如下的设置:
< %@ OutputCache Duration=”60” VaryByParam=”txtCity” %>

如此一来,ASP.NET会对txtCity控件的值进行判断,只有输入的值与缓存值相同,才从缓存中取出相应的值。这就有效地避免了因为值的不同而导致输出错误的数据。

利用缓存的机制对性能的提升非常明显。通过ACT(Application Center Test)的测试,可以发现设置缓存后执行的性能比未设置缓存时的性能足足提高三倍多。

引入缓存看来是提高性能的“完美”解决方案,然而“金无足赤,人无完人”,缓存机制也有缺点,那就是数据过期的问题。一旦应用程序数据或者页面结果值发生的改变,那么在缓存有效期范围内,你所获得的结果将是过期的、不准确的数据。我们可以想一想股票系统利用缓存所带来的灾难,当你利用错误过期的数据去分析股市的风云变幻时,你会发现获得的结果真可以说是“失之毫厘,谬以千里”,看似大好的局面就会像美丽的泡沫一样,用针一戳,转眼就消失得无影无踪。

那么我们是否应该为了追求高性能,而不顾所谓“数据过期”所带来的隐患呢?显然,在类似于股票系统这种数据更新频繁的特定场景下,数据过期的糟糕表现甚至比低效的性能更让人难以接受。故而,我们需要在性能与数据正确性间作出权衡。所幸的是,.Net Framework 2.0引入了一种新的缓存机制,它为我们的“鱼与熊掌兼得”带来了技术上的可行性。

.Net 2.0引入的自定义缓存依赖项,特别是基于MS-SQL Server的SqlCacheDependency特性,使得我们可以避免“数据过期”的问题,它能够根据数据库中相应数据的变化,通知缓存,并移除那些过期的数据。事实上,在PetShop 4.0中,就充分地利用了SqlCacheDependency特性。

4.2 SqlCacheDependency特性

SqlCacheDependency特性实际上是通过System.Web.Caching.SqlCacheDependency类来体现的。通过该类,可以在所有支持的SQL Server版本(7.0,2000,2005)上监视特定的SQL Server数据库表,并创建依赖于该表以及表中数据行的缓存项。当数据表或表中特定行的数据发生更改时,具有依赖项的数据项就会失效,并自动从Cache中删除该项,从而保证了缓存中不再保留过期的数据。
由于版本的原因,SQL Server 2005完全支持SqlCacheDependency特性,但对于SQL Server 7.0和SQL Server 2000而言,就没有如此幸运了。毕竟这些产品出现在.Net Framework 2.0之前,因此它并没有实现自动监视数据表数据变化,通知ASP.NET的功能。解决的办法就是利用轮询机制,通过ASP.NET进程内的一个线程以指定的时间间隔轮询SQL Server数据库,以跟踪数据的变化情况。

要使得7.0或者2000版本的SQL Server支持SqlCacheDependency特性,需要对数据库服务器执行相关的配置步骤。有两种方法配置SQL Server:使用aspnet_regsql命令行工具,或者使用SqlCacheDependencyAdmin类。

4.2.1  利用aspnet_regsql工具

aspnet_regsql工具位于Windows\Microsoft.NET\Framework\[版本]文件夹中。如果直接双击该工具的执行文件,会弹出一个向导对话框,提示我们完成相应的操作:

4-1.gif
图4-1 aspnet_regsql工具

如图4-1所示中的提示信息,说明该向导主要用于配置SQL Server数据库,如membership,profiles等信息,如果要配置SqlCacheDependency,则需要以命令行的方式执行。以PetShop 4.0为例,数据库名为MSPetShop4,则命令为:
aspnet_regsql -S localhost -E -d MSPetShop4 -ed

以下是该工具的命令参数说明:
-?  显示该工具的帮助功能;
-S  后接的参数为数据库服务器的名称或者IP地址;
-U  后接的参数为数据库的登陆用户名;
-P  后接的参数为数据库的登陆密码;
-E  当使用windows集成验证时,使用该功能;
-d  后接参数为对哪一个数据库采用SqlCacheDependency功能;
-t  后接参数为对哪一个表采用SqlCacheDependency功能;
-ed  允许对数据库使用SqlCacheDependency功能;
-dd  禁止对数据库采用SqlCacheDependency功能;
-et  允许对数据表采用SqlCacheDependency功能;
-dt  禁止对数据表采用SqlCacheDependency功能;
-lt  列出当前数据库中有哪些表已经采用sqlcachedependency功能。

以上面的命令为例,说明将对名为MSPetShop4的数据库采用SqlCacheDependency功能,且SQL Server采用了windows集成验证方式。我们还可以对相关的数据表执行aspnet_regsql命令,如:
aspnet_regsql -S localhost -E -d MSPetShop4 -t Item -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Product -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Category -et

当执行上述的四条命令后,aspnet_regsql工具会在MSPetShop4数据库中建立一个名为AspNet_SqlCacheTablesForChangeNotification的新数据库表。该数据表包含三个字段。字段tableName记录要追踪的数据表的名称,例如在PetShop 4.0中,要记录的数据表就包括Category、Item和Product。notificationCreated字段记录开始追踪的时间。changeId作为一个类型为int的字段,用于记录数据表数据发生变化的次数。如图4-2所示:

4-2.gif
图4-2 AspNet_SqlCacheTablesForChangeNotification数据表

除此之外,执行该命令还会为MSPetShop4数据库添加一组存储过程,为ASP.NET提供查询追踪的数据表的情况,同时还将为使用了SqlCacheDependency的表添加触发器,分别对应Insert、Update、Delete等与数据更改相关的操作。例如Product数据表的触发器:
CREATE TRIGGER dbo.[Product_AspNet_SqlCacheNotification_Trigger] ON [Product]
    FOR INSERT, UPDATE, DELETE AS BEGIN
    SET NOCOUNT ON
    EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N’Product’
END

其中,AspNet_SqlCacheUpdateChangeIdStoredProcedure即是工具添加的一组存储过程中的一个。当对Product数据表执行Insert、Update或Delete等操作时,就会激活触发器,然后执行AspNet_SqlCacheUpdateChangeIdStoredProcedure存储过程。其执行的过程就是修改AspNet_SqlCacheTablesForChangeNotification数据表的changeId字段值:
CREATE PROCEDURE dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure
             @tableName NVARCHAR(450)
         AS
         BEGIN
             UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK) SET changeId = changeId + 1
             WHERE tableName = @tableName
         END  
GO

4.2.2  利用SqlCacheDependencyAdmin类

我们也可以利用编程的方式来来管理数据库对SqlCacheDependency特性的使用。该类包含了五个重要的方法:

DisableNotifications
为特定数据库禁用 SqlCacheDependency对象更改通知

DisableTableForNotifications
为数据库中的特定表禁用SqlCacheDependency对象更改通知

EnableNotifications
为特定数据库启用SqlCacheDependency对象更改通知

EnableTableForNotifications
为数据库中的特定表启用SqlCacheDependency对象更改通知

GetTablesEnabledForNotifications
返回启用了SqlCacheDependency对象更改通知的所有表的列表

表4-1 SqlCacheDependencyAdmin类的主要方法

假设我们定义了如下的数据库连接字符串:
const string connectionStr = “Server=localhost;Database=MSPetShop4″;

那么为数据库MSPetShop4启用SqlCacheDependency对象更改通知的实现为:
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
   {
       SqlCacheDependencyAdmin.EnableNotifications(connectionStr);
   }
}

为数据表Product启用SqlCacheDependency对象更改通知的实现则为:
SqlCacheDependencyAdmin.EnableTableForNotifications(connectionStr, “Product”);

如果要调用表4-1中所示的相关方法,需要注意的是访问SQL Server数据库的帐户必须具有创建表和存储过程的权限。如果要调用EnableTableForNotifications方法,还需要具有在该表上创建SQL Server触发器的权限。

虽然说编程方式赋予了程序员更大的灵活性,但aspnet_regsql工具却提供了更简单的方法实现对SqlCacheDependency的配置与管理。PetShop 4.0采用的正是aspnet_regsql工具的办法,它编写了一个文件名为InstallDatabases.cmd的批处理文件,其中包含了对aspnet_regsql工具的执行,并通过安装程序去调用该文件,实现对SQL Server的配置。

4.3 在PetShop 4.0中ASP.NET缓存的实现

PetShop作为一个B2C的宠物网上商店,需要充分考虑访客的用户体验,如果因为数据量大而导致Web服务器的响应不及时,页面和查询数据迟迟得不到结果,会因此而破坏客户访问网站的心情,在耗尽耐心的等待后,可能会失去这一部分客户。无疑,这是非常糟糕的结果。因而在对其进行体系架构设计时,整个系统的性能就显得殊为重要。然而,我们不能因噎废食,因为专注于性能而忽略数据的正确性。在PetShop 3.0版本以及之前的版本,因为ASP.NET缓存的局限性,这一问题并没有得到很好的解决。PetShop 4.0则引入了SqlCacheDependency特性,使得系统对缓存的处理较之以前大为改观。

4.3.1  CacheDependency接口

PetShop 4.0引入了SqlCacheDependency特性,对Category、Product和Item数据表对应的缓存实施了SQL Cache Invalidation技术。当对应的数据表数据发生更改后,该技术能够将相关项从缓存中移除。实现这一技术的核心是SqlCacheDependency类,它继承了CacheDependency类。然而为了保证整个架构的可扩展性,我们也允许设计者建立自定义的CacheDependency类,用以扩展缓存依赖。这就有必要为CacheDependency建立抽象接口,并在web.config文件中进行配置。

在PetShop 4.0的命名空间PetShop.ICacheDependency中,定义了名为IPetShopCacheDependency接口,它仅包含了一个接口方法:
public interface IPetShopCacheDependency
{      
    AggregateCacheDependency GetDependency();
}

AggregateCacheDependency是.Net Framework 2.0新增的一个类,它负责监视依赖项对象的集合。当这个集合中的任意一个依赖项对象发生改变时,该依赖项对象对应的缓存对象都将被自动移除。
AggregateCacheDependency类起到了组合CacheDependency对象的作用,它可以将多个CacheDependency对象甚至于不同类型的CacheDependency对象与缓存项建立关联。由于PetShop需要为Category、Product和Item数据表建立依赖项,因而IPetShopCacheDependency的接口方法GetDependency()其目的就是返回建立了这些依赖项的AggregateCacheDependency对象。

4.3.2  CacheDependency实现

CacheDependency的实现正是为Category、Product和Item数据表建立了对应的SqlCacheDependency类型的依赖项,如代码所示:
public abstract class TableDependency : IPetShopCacheDependency
{
    // This is the separator that’s used in web.config
    protected char[] configurationSeparator = new char[] { ‘,’ };

    protected AggregateCacheDependency dependency = new AggregateCacheDependency();
    protected TableDependency(string configKey)
    {
        string dbName = ConfigurationManager.AppSettings[”CacheDatabaseName”];
        string tableConfig = ConfigurationManager.AppSettings[configKey];
        string[] tables = tableConfig.Split(configurationSeparator);

        foreach (string tableName in tables)
            dependency.Add(new SqlCacheDependency(dbName, tableName));
    }
    public AggregateCacheDependency GetDependency()
   {
        return dependency;
    }
}

需要建立依赖项的数据库与数据表都配置在web.config文件中,其设置如下:

根据各个数据表间的依赖关系,因而不同的数据表需要建立的依赖项也是不相同的,从配置文件中的value值可以看出。然而不管建立依赖项的多寡,其创建的行为逻辑都是相似的,因而在设计时,抽象了一个共同的类TableDependency,并通过建立带参数的构造函数,完成对依赖项的建立。由于接口方法GetDependency()的实现中,返回的对象dependency是在受保护的构造函数创建的,因此这里的实现方式也可以看作是Template Method模式的灵活运用。例如TableDependency的子类Product,就是利用父类的构造函数建立了Product、Category数据表的SqlCacheDependency依赖:
public class Product : TableDependency
{
    public Product() : base(”ProductTableDependency”) { }
}

如果需要自定义CacheDependency,那么创建依赖项的方式又有不同。然而不管是创建SqlCacheDependency对象,还是自定义的CacheDependency对象,都是将这些依赖项添加到AggregateCacheDependency类中,因而我们也可以为自定义CacheDependency建立专门的类,只要实现IPetShopCacheDependency接口即可。

4.3.3  CacheDependency工厂

继承了抽象类TableDependency的Product、Category和Item类均需要在调用时创建各自的对象。由于它们的父类TableDependency实现了接口IPetShopCacheDependency,因而它们也间接实现了IPetShopCacheDependency接口,这为实现工厂模式提供了前提。

在PetShop 4.0中,依然利用了配置文件和反射技术来实现工厂模式。命名空间PetShop.CacheDependencyFactory中,类DependencyAccess即为创建IPetShopCacheDependency对象的工厂类:
public static class DependencyAccess
{       
    public static IPetShopCacheDependency CreateCategoryDependency()
    {
        return LoadInstance(”Category”);
    }
    public static IPetShopCacheDependency CreateProductDependency()
    {
        return LoadInstance(”Product”);
    }
    public static IPetShopCacheDependency CreateItemDependency()
    {
        return LoadInstance(”Item”);
    }
    private static IPetShopCacheDependency LoadInstance(string className)
    {
        string path = ConfigurationManager.AppSettings[”CacheDependencyAssembly”];
        string fullyQualifiedClass = path + “.” + className;
        return (IPetShopCacheDependency)Assembly.Load(path).CreateInstance(fullyQualifiedClass);
    }
}
整个工厂模式的实现如图4-3所示:

4-3.gif
 图4-3 CacheDependency工厂

虽然DependencyAccess类创建了实现了IPetShopCacheDependency接口的类Category、Product、Item,然而我们之所以引入IPetShopCacheDependency接口,其目的就在于获得创建了依赖项的AggregateCacheDependency类型的对象。我们可以调用对象的接口方法GetDependency(),如下所示:
AggregateCacheDependency dependency = DependencyAccess.CreateCategoryDependency().GetDependency();

为了方便调用者,似乎我们可以对DependencyAccess类进行改进,将原有的CreateCategoryDependency()方法,修改为创建AggregateCacheDependency类型对象的方法。

然而这样的做法扰乱了作为工厂类的DependencyAccess的本身职责,且创建IPetShopCacheDependency接口对象的行为仍然有可能被调用者调用,所以保留原有的DependencyAccess类仍然是有必要的。

在PetShop 4.0的设计中,是通过引入Facade模式以方便调用者更加简单地获得AggregateCacheDependency类型对象。

4.3.4  引入Facade模式

利用Facade模式可以将一些复杂的逻辑进行包装,以方便调用者对这些复杂逻辑的调用。就好像提供一个统一的门面一般,将内部的子系统封装起来,统一为一个高层次的接口。一个典型的Facade模式示意图如下所示:

4-4.gif
图4-4 Facade模式

Facade模式的目的并非要引入一个新的功能,而是在现有功能的基础上提供一个更高层次的抽象,使得调用者可以直接调用,而不用关心内部的实现方式。以CacheDependency工厂为例,我们需要为调用者提供获得AggregateCacheDependency对象的简便方法,因而创建了DependencyFacade类:
public static class DependencyFacade
{
    private static readonly string path = ConfigurationManager.AppSettings[”CacheDependencyAssembly”];
    public static AggregateCacheDependency GetCategoryDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateCategoryDependency().GetDependency();
        else
            return null;
    }
    public static AggregateCacheDependency GetProductDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateProductDependency().GetDependency();
        else
            return null;
        }
    public static AggregateCacheDependency GetItemDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateItemDependency().GetDependency();
        else
            return null;
    }
}

DependencyFacade类封装了获取AggregateCacheDependency类型对象的逻辑,如此一来,调用者可以调用相关方法获得创建相关依赖项的AggregateCacheDependency类型对象:
AggregateCacheDependency dependency = DependencyFacade.GetCategoryDependency();

比起直接调用DependencyAccess类的GetDependency()方法而言,除了方法更简单之外,同时它还对CacheDependencyAssembly配置节进行了判断,如果其值为空,则返回null对象。

在PetShop.Web的App_Code文件夹下,静态类WebUtility的GetCategoryName()和GetProductName()方法调用了DependencyFacade类。例如GetCategoryName()方法:
public static string GetCategoryName(string categoryId)
{
     Category category = new Category();
     if (!enableCaching)
            return category.GetCategory(categoryId).Name;

     string cacheKey = string.Format(CATEGORY_NAME_KEY, categoryId);

     // 检查缓存中是否存在该数据项;
     string data = (string)HttpRuntime.Cache[cacheKey];
     if (data == null)
     {
           // 通过web.config的配置获取duration值;
           int cacheDuration = int.Parse(ConfigurationManager.AppSettings[”CategoryCacheDuration”]);
           // 如果缓存中不存在该数据项,则通过业务逻辑层访问数据库获取;
           data = category.GetCategory(categoryId).Name;
           // 通过Facade类创建AggregateCacheDependency对象;
           AggregateCacheDependency cd = DependencyFacade.GetCategoryDependency();
           // 将数据项以及AggregateCacheDependency 对象存储到缓存中;
           HttpRuntime.Cache.Add(cacheKey, data, cd, DateTime.Now.AddHours(cacheDuration), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
      }
      return data;
}

GetCategoryName()方法首先会检查缓存中是否已经存在CategoryName数据项,如果已经存在,就通过缓存直接获取数据;否则将通过业务逻辑层调用数据访问层访问数据库获得CategoryName,在获得了CategoryName后,会将新获取的数据连同DependencyFacade类创建的AggregateCacheDependency对象添加到缓存中。

WebUtility静态类被表示层的许多页面所调用,例如Product页面:
public partial class Products : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Page.Title = WebUtility.GetCategoryName(Request.QueryString[”categoryId”]);
    }
}

显示页面title的逻辑是放在Page_Load事件方法中,因而每次打开该页面都要执行获取CategoryName的方法。如果没有采用缓存机制,当Category数据较多时,页面的显示就会非常缓慢。

4.3.5  引入Proxy模式

业务逻辑层BLL中与Product、Category、Item有关的业务方法,其实现逻辑是调用数据访问层(DAL)对象访问数据库,以获取相关数据。为了改善系统性能,我们就需要为这些实现方法增加缓存机制的逻辑。当我们操作增加了缓存机制的业务对象时,对于调用者而言,应与BLL业务对象的调用保持一致。也即是说,我们需要引入一个新的对象去控制原来的BLL业务对象,这个新的对象就是Proxy模式中的代理对象。

以PetShop.BLL.Product业务对象为例,PetShop为其建立了代理对象ProductDataProxy,并在GetProductByCategory()等方法中,引入了缓存机制,例如:
public static class ProductDataProxy
{

    private static readonly int productTimeout = int.Parse(ConfigurationManager.AppSettings[”ProductCacheDuration”]);
    private static readonly bool enableCaching = bool.Parse(ConfigurationManager.AppSettings[”EnableCaching”]);
    public static IList
GetProductsByCategory(string category)
    {
        Product product = new Product();

        if (!enableCaching)
            return product.GetProductsByCategory(category);

        string key = “product_by_category_” + category;
        IList data = (IList )HttpRuntime.Cache[key];

        // Check if the data exists in the data cache
        if (data == null)
        {
            data = product.GetProductsByCategory(category);

            // Create a AggregateCacheDependency object from the factory
            AggregateCacheDependency cd = DependencyFacade.GetProductDependency();

            // Store the output in the data cache, and Add the necessary AggregateCacheDependency object
            HttpRuntime.Cache.Add(key, data, cd, DateTime.Now.AddHours(productTimeout), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
        }
        return data;
    }
}

与业务逻辑层Product对象的GetProductsByCategory()方法相比,增加了缓存机制。当缓存内不存在相关数据项时,则直接调用业务逻辑层Product的GetProductsByCategory()方法来获取数据,并将其与对应的AggregateCacheDependency对象一起存储在缓存中。

引入Proxy模式,实现了在缓存级别上对业务对象的封装,增强了对业务对象的控制。由于暴露在对象外的方法是一致的,因而对于调用方而言,调用代理对象与真实对象并没有实质的区别。

从职责分离与分层设计的角度分析,我更希望这些Proxy对象是被定义在业务逻辑层中,而不像在PetShop的设计那样,被划分到表示层UI中。此外,如果需要考虑程序的可扩展性与可替换性,我们还可以为真实对象与代理对象建立统一的接口或抽象类。然而,单以PetShop的表示层调用来看,采用静态类与静态方法的方式,或许更为合理。我们需要谨记,“过度设计”是软件设计的警戒线。

如果需要对UI层采用缓存机制,将应用程序数据存放到缓存中,就可以调用这些代理对象。以ProductsControl用户控件为例,调用方式如下:
productsList.DataSource = ProductDataProxy.GetProductsByCategory(categoryKey);

productsList对象属于自定义的CustomList类型,这是一个派生自System.Web.UI.WebControls.DataList控件的类,它的DataSource属性可以接受IList集合对象。
不过在PetShop 4.0的设计中,对于类似于ProductsControl类型的控件而言,采用的缓存机制是页输出缓存。我们可以从ProductsControl.ascx页面的Source代码中发现端倪:
< %@ OutputCache Duration="100000" VaryByParam="page;categoryId" %>

与ASP.NET 1.x的页输出缓存不同的是,在ASP.NET 2.0中,为ASP.NET用户控件新引入了CachePolicy属性,该属性的类型为ControlCachePolicy类,它以编程方式实现了对ASP.NET用户控件的输出缓存设置。我们可以通过设置ControlCachePolicy类的Dependency属性,来设置与该用户控件相关的依赖项,例如在ProductsControl用户控件中,进行如下的设置:
protected void Page_Load(object sender, EventArgs e)
{
    this.CachePolicy.Dependency = DependencyFacade.GetProductDependency();
}

采用页输出缓存,并且利用ControlCachePolicy设置输出缓存,能够将业务数据与整个页面放入到缓存中。这种方式比起应用程序缓存而言,在性能上有很大的提高。同时,它又通过引入的SqlCacheDependency特性有效地避免了“数据过期”的缺点,因而在PetShop 4.0中被广泛采用。相反,之前为Product、Category、Item业务对象建立的代理对象则被“投闲散置”,仅仅作为一种设计方法的展示而“幸存”与整个系统的源代码中。

 

 

《解剖PetShop》系列之五

五 PetShop之业务逻辑层设计

业务逻辑层(Business Logic Layer)无疑是系统架构中体现核心价值的部分。它的关注点主要集中在业务规则的制定、业务流程的实现等与业务需求有关的系统设计,也即是说它是与系统所应对的领域(Domain)逻辑有关,很多时候,我们也将业务逻辑层称为领域层。例如Martin Fowler在《Patterns of Enterprise Application Architecture》一书中,将整个架构分为三个主要的层:表示层、领域层和数据源层。作为领域驱动设计的先驱Eric Evans,对业务逻辑层作了更细致地划分,细分为应用层与领域层,通过分层进一步将领域逻辑与领域逻辑的解决方案分离。

业务逻辑层在体系架构中的位置很关键,它处于数据访问层与表示层中间,起到了数据交换中承上启下的作用。由于层是一种弱耦合结构,层与层之间的依赖是向下的,底层对于上层而言是“无知”的,改变上层的设计对于其调用的底层而言没有任何影响。如果在分层设计时,遵循了面向接口设计的思想,那么这种向下的依赖也应该是一种弱依赖关系。因而在不改变接口定义的前提下,理想的分层式架构,应该是一个支持可抽取、可替换的“抽屉”式架构。正因为如此,业务逻辑层的设计对于一个支持可扩展的架构尤为关键,因为它扮演了两个不同的角色。对于数据访问层而言,它是调用者;对于表示层而言,它却是被调用者。依赖与被依赖的关系都纠结在业务逻辑层上,如何实现依赖关系的解耦,则是除了实现业务逻辑之外留给设计师的任务。

5.1  与领域专家合作

设计业务逻辑层最大的障碍不在于技术,而在于对领域业务的分析与理解。很难想象一个不熟悉该领域业务规则和流程的架构设计师能够设计出合乎客户需求的系统架构。几乎可以下定结论的是,业务逻辑层的设计过程必须有领域专家的参与。在我曾经参与开发的项目中,所涉及的领域就涵盖了电力、半导体、汽车等诸多行业,如果缺乏这些领域的专家,软件架构的设计尤其是业务逻辑层的设计就无从谈起。这个结论唯一的例外是,架构设计师同时又是该领域的专家。然而,正所谓“千军易得,一将难求”,我们很难寻觅到这样卓越出众的人才。

领域专家在团队中扮演的角色通常称为Business Consultor(业务咨询师),负责提供与领域业务有关的咨询,与架构师一起参与架构与数据库的设计,撰写需求文档和设计用例(或者用户故事User Story)。如果在测试阶段,还应该包括撰写测试用例。理想的状态是,领域专家应该参与到整个项目的开发过程中,而不仅仅是需求阶段。

领域专家可以是专门聘请的对该领域具有较深造诣的咨询师,也可以是作为需求提供方的客户。在极限编程(Extreme Programming)中,就将客户作为领域专家引入到整个开发团队中。它强调了现场客户原则。现场客户需要参与到计划游戏、开发迭代、编码测试等项目开发的各个阶段。由于领域专家与设计师以及开发人员组成了一个团队,贯穿开发过程的始终,就可以避免需求理解错误的情况出现。即使项目的开发与实际需求不符,也可以在项目早期及时修正,从而避免了项目不必要的延期,加强了对项目过程和成本的控制。正如Steve McConnell在构建活动的前期准备中提及的一个原则:发现错误的时间要尽可能接近引入该错误的时间。需求的缺陷在系统中潜伏的时间越长,代价就越昂贵。如果在项目开发中能够与领域专家充分的合作,就可以最大效果地规避这样一种恶性的链式反应。

传统的软件开发模型同样重视与领域专家的合作,但这种合作主要集中在需求分析阶段。例如瀑布模型,就非常强调早期计划与需求调研。然而这种未雨绸缪的早期计划方式,对架构师与需求调研人员的技能要求非常高,它强调需求文档的精确性,一旦分析出现偏差,或者需求发生变更,当项目开发进入设计阶段后,由于缺乏与领域专家沟通与合作的机制,开发人员估量不到这些错误与误差,因而难以及时作出修正。一旦这些问题像毒瘤一般在系统中蔓延开来,逐渐暴露在开发人员面前时,已经成了一座难以逾越的高山。我们需要消耗更多的人力物力,才能够修正这些错误,从而导致开发成本成数量级的增加,甚至于导致项目延期。当然还有一个好的选择,就是放弃整个项目。这样的例子不胜枚举,事实上,项目开发的“滑铁卢”,究其原因,大部分都是因为业务逻辑分析上出现了问题。

迭代式模型较之瀑布模型有很大地改进,因为它允许变更、优化系统需求,整个迭代过程实际上就是与领域专家的合作过程,通过向客户演示迭代所产生的系统功能,从而及时获取反馈,并逐一解决迭代演示中出现的问题,保证系统向着合乎客户需求的方向演化。因而,迭代式模型往往能够解决早期计划不足的问题,它允许在发现缺陷的时候,在需求变更的时候重新设计、重新编码并重新测试。

无论采用何种开发模型,与领域专家的合作都将成为项目成败与否的关键。这基于一个软件开发的普遍真理,那就是世界上没有不变的需求。一句经典名言是:“没有不变的需求,世上的软件都改动过3次以上,唯一一个只改动过两次的软件的拥有者已经死了,死在去修改需求的路上。”一语道尽了软件开发的残酷与艰辛!

那么应该如何加强与领域专家的合作呢?James Carey和Brent Carlson根据他们在参与的IBM SanFrancisco项目中获得的经验,提出了Innocent Questions模式,其意义即“改进领域专家和技术专家的沟通质量”。在一个项目团队中,如果我们没有一位既能担任首席架构师,同时又是领域专家的人选,那么加强领域专家与技术专家的合作就显得尤为重要了。毕竟,作为一个领域专家而言,可能并不熟悉软件设计方法学,也不具备面向对象开发和架构设计的能力,同样,大部分技术专家很有可能对该项目所涉及的业务领域仅停留在一知半解的地步。如果领域专家与技术专家不能有效沟通,则整个项目的前途就岌岌可危了。

Innocent Questions模式提出的解决方案包括:
(1)选用可以与人和谐相处的人员组建开发团队;
(2)清楚地定义角色和职权;
(3)明确定义需要的交互点;
(4)保持团队紧密;
(5)雇佣优秀的人。

事实上,这已经从技术的角度上升到对团队的管理层次了。就好比篮球运动一样,即使你的球队集合了五名世界上最顶尖最有天赋的球员,如果各自为战,要想取得比赛的胜利依旧是非常困难的。团队精神与权责分明才是取得胜利的保障,软件开发同样如此。

与领域专家合作的基础是保证开发团队中永远保留至少一名领域专家。他可以是系统的客户,第三方公司的咨询师,最理想是自己公司雇佣的专家。如果项目中缺乏这样的一个人,那么我的建议是去雇佣他,如果你不想看到项目遭遇“西伯利亚寒流”的话。

确定领域专家的角色任务与职责。必须要让团队中的每一个人明确领域专家在整个团队中究竟扮演什么样的角色,他的职责是什么。一个合格的领域专家必须对业务领域有足够深入的理解,他应该是一个能够俯瞰整个系统需求、总揽全局的人物。在项目开发过程中,将由他负责业务规则和流程的制定,负责与客户的沟通,需求的调研与讨论,并于设计师一起参与系统架构的设计。编档是领域专家必须参与的工作,无论是需求文档还是设计文档,以及用例的编写,领域专家或者提出意见,或者作为撰写的作者,至少他也应该是评审委员会的重要成员。

规范业务领域的术语和技术术语。领域专家和技术专家必须在保证不产生二义性的语义环境下进行沟通与交流。如果出现理解上的分歧,我们必须及时解决,通过讨论确立术语标准。很难想象两个语言不通的人能够相互合作愉快,解决的办法是加入一位翻译人员。在领域专家与技术专家之间搭建一座语义上的桥梁,使其能够相互理解、相互认同。还有一个办法是在团队内部开展培训活动。尤其对于开发人员而言,或多或少地了解一些业务领域知识,对于项目的开发有很大的帮助。在我参与过的半导体领域的项目开发,团队就专门邀请了半导体行业的专家就生产过程的业务逻辑进行了全方位的介绍与培训。正所谓“磨刀不误砍柴工”,虽然我们消费了培训的时间,但对于掌握了业务规则与流程的开发人员,却能够提升项目开发进度,总体上节约了开发成本。

加强与客户的沟通。客户同时也可以作为团队的领域专家,极限编程的现场客户原则是最好的示例。但现实并不都如此的完美,在无法要求客户成为开发团队中的固定一员时,聘请或者安排一个专门的领域专家,加强与客户的沟通,就显得尤为重要。项目可以通过领域专家获得客户的及时反馈。而通过领域专家去了解变更了的需求,会在最大程度上减少需求误差的可能。

5.2  业务逻辑层的模式应用

Martin Fowler在《企业应用架构模式》一书中对领域层(即业务逻辑层)的架构模式作了整体概括,他将业务逻辑设计分为三种主要的模式:Transaction Script、Domain Model和Table Module。

Transaction Script模式将业务逻辑看作是一个个过程,是比较典型的面向过程开发模式。应用Transaction Script模式可以不需要数据访问层,而是利用SQL语句直接访问数据库。为了有效地管理SQL语句,可以将与数据库访问有关的行为放到一个专门的Gateway类中。应用Transaction Script模式不需要太多面向对象知识,简单直接的特性是该模式全部价值之所在。因而,在许多业务逻辑相对简单的项目中,应用Transaction Script模式较多。

Domain Model模式是典型的面向对象设计思想的体现。它充分考虑了业务逻辑的复杂多变,引入了Strategy模式等设计模式思想,并通过建立领域对象以及抽象接口,实现模式的可扩展性,并利用面向对象思想与身俱来的特性,如继承、封装与多态,用于处理复杂多变的业务逻辑。唯一制约该模式应用的是对象与关系数据库的映射。我们可以引入ORM工具,或者利用Data Mapper模式来完成关系向对象的映射。

与Domain Model模式相似的是Table Module模式,它同样具有面向对象设计的思想,唯一不同的是它获得的对象并非是单纯的领域对象,而是DataSet对象。如果为关系数据表与对象建立一个简单的映射关系,那么Domain Model模式就是为数据表中的每一条记录建立一个领域对象,而Table Module模式则是将整个数据表看作是一个完整的对象。虽然利用DataSet对象会丢失面向对象的基本特性,但它在为表示层提供数据源支持方面却有着得天独厚的优势。尤其是在.Net平台下,ADO.NET与Web控件都为Table Module模式提供了生长的肥沃土壤。

5.3  PetShop的业务逻辑层设计

PetShop在业务逻辑层设计中引入了Domain Model模式,这与数据访问层对于数据对象的支持是分不开的。由于PetShop并没有对宠物网上商店的业务逻辑进行深入,也省略了许多复杂细节的商务逻辑,因而在Domain Model模式的应用上并不明显。最典型地应该是对Order领域对象的处理方式,通过引入Strategy模式完成对插入订单行为的封装。关于这一点,我已在第27章有了详尽的描述,这里就不再赘述。

本应是系统架构设计中最核心的业务逻辑层,由于简化了业务流程的缘故,使得PetShop在这一层的设计有些乏善可陈。虽然在业务逻辑层中,针对B2C业务定义了相关的领域对象,但这些领域对象仅仅是完成了对数据访问层中数据对象的简单封装而已,其目的仅在于分离层次,以支持对各种数据库的扩展,同时将SQL语句排除在业务逻辑层外,避免了SQL语句的四处蔓延。

最能体现PetShop业务逻辑的除了对订单的管理之外,还包括购物车(Shopping Cart)与Wish List的管理。在PetShop的BLL模块中,定义了Cart类来负责相关的业务逻辑,定义如下:
[Serializable]
public class Cart
{
    private Dictionary cartItems = new Dictionary();
    public decimal Total
    {
        get
        {
            decimal total = 0;
            foreach (CartItemInfo item in cartItems.Values)
                total += item.Price * item.Quantity;
            return total;
        }
    }
    public void SetQuantity(string itemId, int qty)
    {
        cartItems[itemId].Quantity = qty;
    }
    public int Count
    {
        get { return cartItems.Count; }
    }
    public void Add(string itemId)
    {
        CartItemInfo cartItem;
        if (!cartItems.TryGetValue(itemId, out cartItem))
        {
            Item item = new Item();
            ItemInfo data = item.GetItem(itemId);
            if (data != null)
            {
                CartItemInfo newItem = new CartItemInfo(itemId, data.ProductName, 1, (decimal)data.Price, data.Name, data.CategoryId, data.ProductId);
                cartItems.Add(itemId, newItem);
            }
        }
        else
            cartItem.Quantity++;
    }
    //其他方法略;
}

Cart类通过一个Dictionary对象来负责对购物车内容的存储,同时定义了Add、Remove、Clear等方法,来实现对购物车内容的管理。

在前面我提到PetShop业务逻辑层中的领域对象仅仅是完成对数据对象的简单封装,但这种分离层次的方法在架构设计中依然扮演了举足轻重的作用。以Cart类的Add()方法为例,在方法内部引入了PetShop.BLL.Item领域对象,并调用了Item对象的GetItem()方法。如果没有在业务逻辑层封装Item对象,而是直接调用数据访问层的Item数据对象,为保证层次间的弱依赖关系,就需要调用工厂对象的工厂方法来创建PetShop.IDAL.IItem接口类型对象。一旦数据访问层的Item对象被多次调用,就会造成重复代码,既不离于程序的修改与扩展,也导致程序结构生长为臃肿的态势。

此外,领域对象对数据访问层数据对象的封装,也有利于表示层对业务逻辑层的调用。在三层式架构中,表示层应该是对于数据访问层是“无知”的,这样既减少了层与层间的依赖关系,也能有效避免“循环依赖”的后果。

值得商榷的是Cart类的Total属性。其值的获取是通过遍历购物车集合,然后累加价格与商品数量的乘积。这里显然简化了业务逻辑,而没有充分考虑需求的扩展。事实上,这种获取购物车总价格的算法,在大多数情况下仅仅是其中的一种策略而已,我们还应该考虑折扣的情况。例如,当总价格超过100元时,可以给与顾客一定的折扣,这是与网站的促销计划相关的。除了给与折扣的促销计划外,网站也可以考虑赠送礼品的促销策略,因此我们有必要引入Strategy模式,定义接口IOnSaleStrategy:
public interface IOnSaleStrategy
{
     decimal CalculateTotalPrice(Dictionary cartItems);
}

如此一来,我们可以为Cart类定义一个有参数的构造函数:
private IOnSaleStrategy m_onSale;
public Cart(IOnSaleStrategy onSale)
{
     m_onSale = onSale;
}

那么Total属性就可以修改为:
public decimal Total
{
     get {return m_onSale.CalculateTotalPrice(cartItems);}
}

如此一来,就可以使得Cart类能够有效地支持网站推出的促销计划,也符合开-闭原则。同样的,这种设计方式也是Domain Model模式的体现。修改后的设计如图5-1所示: 

5-1.gif

图5-1 引入Strategy模式

作为一个B2C的电子商务架构,它所涉及的业务领域已为大部分设计师与开发人员所熟悉,因而在本例中,与领域专家的合作显得并不那么重要。然而,如果我们要开发一个成功的电子商务网站,与领域专家的合作仍然是必不可少的。以订单的管理而言,如果考虑复杂的商业应用,就需要管理订单的跟踪(Tracking),与网上银行的合作,账户安全性,库存管理,物流管理,以及客户关系管理(CRM)。整个业务过程却涵盖了诸如电子商务、银行、物流、客户关系学等诸多领域,如果没有领域专家的参与,业务逻辑层的设计也许会“败走麦城”。

5.4  与数据访问层的通信

业务逻辑层需要与数据访问层通信,利用数据访问层访问数据库,因此业务逻辑层与数据访问层之间就存在依赖关系。在数据访问层引入接口程序集以及数据工厂的设计前提下,能够做到两者间关系为弱依赖。我们从业务逻辑层的引用程序集中可以看到,BLL模块并没有引用SQLServerDAL和OracleDAL程序集。在业务逻辑层中,有关数据访问层中数据对象的调用,均利用多态原理定义了抽象的接口类型对象,然后利用工厂对象的工厂方法创建具体的数据对象。如PetShop.BLL.PetShop领域对象所示:
namespace PetShop.BLL
{
    public class Product
    {
    //根据工厂对象创建IProduct接口类型实例;
        private static readonly IProduct dal =  PetShop.DALFactory.DataAccess.CreateProduct();       
        //调用IProduct对象的接口方法GetProductByCategory();
  public IList
GetProductsByCategory(string category)
  {
   // 如果为空则新建List对象;
   if(string.IsNullOrEmpty(category))
    return new List ();

   // 通过数据访问层的数据对象访问数据库;
   return dal.GetProductsByCategory(category);
  }
        //其他方法略;
    }
}

在领域对象Product类中,利用数据访问层的工厂类DALFactory.DataAccess创建PetShop.IDAL.IProduct类型的实例,如此就可以解除对具体程序集SQLServerDAL或OracleDAL的依赖。只要PetShop.IDAL的接口方法不变,即使修改了IDAL接口模块的具体实现,都不会影响业务逻辑层的实现。这种松散的弱耦合关系,才能够最大程度地支持架构的可扩展。

领域对象Product实际上还完成了对数据对象Product的封装,它们暴露在外的接口方法是一致地,正是通过封装,使得表示层可以完全脱离数据库以及数据访问层,表示层的调用者仅需要关注业务逻辑层的实现逻辑,以及领域对象暴露的接口和调用方式。事实上,只要设计合理,规范了各个层次的接口方法,三层式架构的设计完全可以分离开由不同的开发人员同时开发,这就可以有效地利用开发资源,缩短项目开发周期。

5.5  面向接口设计

也许是业务逻辑比较简单地缘故,在业务逻辑层的设计中,并没有秉承在数据访问层中面向接口设计的思想。除了完成对插入订单策略的抽象外,整个业务逻辑层仅以BLL模块实现,没有为领域对象定义抽象的接口。因而PetShop的表示层与业务逻辑层就存在强依赖关系,如果业务逻辑层中的需求发生变更,就必然会影响表示层的实现。唯一可堪欣慰的是,由于我们采用分层式架构将用户界面与业务领域逻辑完全分离,一旦用户界面发生更改,例如将B/S架构修改为C/S架构,那么业务逻辑层的实现模块是可以完全重用的。

然而,最理想的方式仍然是面向接口设计。根据第28章对ASP.NET缓存的分析,我们可以将表示层App_Code下的Proxy类与Utility类划分到业务逻辑层中,并修改这些静态类为实例类,并将这些类中与业务领域有关的方法抽象为接口,然后建立如数据访问层一样的抽象工厂。通过“依赖注入”方式,解除与具体领域对象类的依赖,使得表示层仅依赖于业务逻辑层的接口程序集以及工厂模块。

那么,这样的设计是否有“过度设计”的嫌疑呢?我们需要依据业务逻辑的需求情况而定。此外,如果我们需要引入缓存机制,为领域对象创建代理类,那么为领域对象建立接口,就显得尤为必要。我们可以建立一个专门的接口模块IBLL,用以定义领域对象的接口。以Product领域对象为例,我们可以建立IProduct接口:
public interface IProduct
{
   IList GetProductByCategory(string category);
   IList GetProductByCategory(string[] keywords);
   ProductInfo GetProduct(string productId);
}

在BLL模块中可以引入对IBLL程序集的依赖,则领域对象Product的定义如下:
public class Product:IProduct
{
  public IList GetProductByCategory(string category) { //实现略; }
  public IList GetProductByCategory(string[] keywords) { //实现略; }
  public ProductInfo GetProduct(string productId) { //实现略; }
}

然后我们可以为代理对象建立专门的程序集BLLProxy,它不仅引入对IBLL程序集的依赖,同时还将依赖于BLL程序集。此时代理对象ProductDataProxy的定义如下:
using PetShop.IBLL;
using PetShop.BLL;
namespace PetShop.BLLProxy
{
  public class ProductDataProxy:IProduct
  {
     public IList GetProductByCategory(string category)
     {
        Product product = new Product();
        //其他实现略;
     }
     public IList GetProductByCategory(string[] keywords) { //实现略; }
     public ProductInfo GetProduct(string productId) { //实现略; }
  }
}

如此的设计正是典型的Proxy模式,其类结构如图5-2所示: 

5-2.gif

图5-2 Proxy模式

参照数据访问层的设计方法,我们可以为领域对象及代理对象建立抽象工厂,并在web.config中配置相关的配置节,然后利用反射技术创建具体的对象实例。如此一来,表示层就可以仅仅依赖PetShop.IBLL程序集以及工厂模块,如此就可以解除表示层与具体领域对象之间的依赖关系。表示层与修改后的业务逻辑层的关系如图5-3所示:

5-3.gif

图5-3 修改后的业务逻辑层与表示层的关系

图5-4则是PetShop 4.0原有设计的层次关系图:

5-4.gif

图5-4 PetShop 4.0中表示层与业务逻辑层的关系

通过比较图5-3与图5-4,虽然后者不管是模块的个数,还是模块之间的关系,都相对更加简单,然而Web Component组件与业务逻辑层之间却是强耦合的,这样的设计不利于应对业务扩展与需求变更。通过引入接口模块IBLL与工厂模块BLLFactory,解除了与具体模块BLL的依赖关系。这种设计对于业务逻辑相对比较复杂的系统而言,更符合面向对象的设计思想,有利于我们建立可抽取、可替换的“抽屉”式三层架构。

 

 

《解剖PetShop》系列之六

六 PetShop之表示层设计

表示层(Presentation Layer)的设计可以给系统客户最直接的体验和最十足的信心。正如人与人的相交相识一样,初次见面的感觉总是永难忘怀的。一件交付给客户使用的产品,如果在用户界面(User Interface,UI)上缺乏吸引人的特色,界面不友好,操作不够体贴,即使这件产品性能非常优异,架构设计合理,业务逻辑都满足了客户的需求,却仍然难以讨得客户的欢心。俗语云:“佛要金装,人要衣装”,特别是对于Web应用程序而言,Web网页就好比人的衣装,代表着整个系统的身份与脸面,是招徕“顾客”的最大卖点。

“献丑不如藏拙”,作为艺术细胞缺乏的我,并不打算在用户界面的美术设计上大做文章,是以本书略过不提。本章所关注的表示层设计,还是以架构设计的角度,阐述在表示层设计中对模式的应用,ASP.NET控件的设计与运用,同时还包括了对ASP.NET 2.0新特色的介绍。

6.1  MVC模式

表示层设计中最重要的模式是MVC(Model-View-Controller,即模型-视图-控制器)模式。MVC模式最早是由SmallTalk语言研究团提出的,被广泛应用在用户交互应用程序中。Controller根据用户请求(Response)修改Model的属性,此时Event(事件)被触发,所有依赖于Model的View对象会自动更新,并基于Model对象产生一个响应(Response)信息,返回给Controller。Martin Fowler在《企业应用架构模式》一书中,展示了MVC模式应用的全过程,如图6-1所示: 

6-1.gif

图6-1 典型的MVC模式

如果将MVC模式拆解为三个独立的部分:Model、View、Controller,我们可以通过GOF设计模式来实现和管理它们之间的关系。在体系架构设计中,业务逻辑层的领域对象以及数据访问层的数据值对象都属于MVC模式的Model对象。如果要管理Model与View之间的关系,可以利用Observer模式,View作为观察者,一旦Model的属性值发生变化,就会通知View基于Model的值进行更新。而Controller作为控制用户请求/响应的对象,则可以利用Mediator模式,专门负责请求/响应任务之间的调节。而对于View本身,在面向组件设计思想的基础上,我们通常将它设计为组件或者控件,这些组件或者控件根据自身特性的不同,共同组成一种类似于递归组合的对象结构,因而我们可以利用Composite模式来设计View对象。

然而在.NET平台下,我们并不需要自己去实现MVC模式。对于View对象而言,ASP.NET已经提供了常用的Web控件,我们也可以通过继承System.Web.UI.UserControl,自定义用户控件,并利用ASPX页面组合Web控件来实现视图。ASP.NET定义了System.Web.UI.Page类,它相当于MVC模式的Controller对象,可以处理用户的请求。由于利用了codebehind技术,使得用户界面的显示与UI实现逻辑完全分离,也即是说,View对象与Controller对象成为相对独立的两部分,从而有利于代码的重用性。比较ASP而言,这种编程方式更符合开发人员的编程习惯,同时有利于开发人员与UI设计人员的分工与协作。至于Model对象,则为业务逻辑层的领域对象。此外,.NET平台通过ADO.NET提供了DataSet对象,便于与Web控件的数据源绑定。

6.2  Page Controller模式的应用

通观PetShop的表示层设计,充分利用了ASP.NET的技术特点,通过Web页面与用户控件控制和展现视图,并利用codebehind技术将业务逻辑层的领域对象加入到表示层实现逻辑中,一个典型的Page Controller模式呼之欲出。

Page Controller模式是Martin Fowler在《企业应用架构模式》中最重要的表示层模式之一。在.NET平台下,Page Controller模式的实现非常简单,以Products.aspx页面为例。首先在aspx页面中,进行如下的设置:
< %@ Page AutoEventWireup="true" Language="C#" MasterPageFile="~/MasterPage.master" Title="Products" Inherits="PetShop.Web.Products" CodeFile="~/Products.aspx.cs" %>

Aspx页面继承自System.Web.UI.Page类。Page类对象通过继承System.Web.UI.Control类,从而拥有了Web控件的特性,同时它还实现了IHttpHandler接口。作为ASP.NET处理HTTP Web请求的接口,提供了如下的定义:
[AspNetHostingPermission(SecurityAction.InheritanceDemand,
Level=AspNetHostingPermissionLevel.Minimal),
AspNetHostingPermission(SecurityAction.LinkDemand,
Level=AspNetHostingPermissionLevel.Minimal)]
public interface IHttpHandler
{
      void ProcessRequest(HttpContext context);
      bool IsReusable { get; }
}

Page类实现了ProcessRequest()方法,通过它可以设置Page对象的Request和Response属性,从而完成对用户请求/相应的控制。然后Page类通过从Control类继承来的Load事件,将View与Model建立关联,如Products.aspx.cs所示:
public partial class Products : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //get page header and title
        Page.Title = WebUtility.GetCategoryName(Request.QueryString[”categoryId”]);
    }
}

事件机制恰好是observer模式的实现,当ASPX页面的Load事件被激发后,系统通过WebUtility类(在第28章中有对WebUtility类的详细介绍)的GetCategoryName()方法,获得Category值,并将其显示在页面的Title上。Page对象作为Controller,就好似一个调停者,用于协调View与Model之间的关系。

由于ASPX页面中还可以包含Web控件,这些控件对象同样是作为View对象,通过Page类型对象完成对它们的控制。例如在CheckOut.aspx页面中,当用户发出CheckOut的请求后,作为System.Web.UI.WebControls.Winzard控件类型的wzdCheckOut,会在整个向导过程结束时,触发FinishButtonClick事件,并在该事件中调用领域对象Order的Insert()方法,如下所示:
public partial class CheckOut : System.Web.UI.Page…

    protected void wzdCheckOut_FinishButtonClick(object sender, WizardNavigationEventArgs e) {
        if (Profile.ShoppingCart.CartItems.Count > 0) {
            if (Profile.ShoppingCart.Count > 0) {

                // display ordered items
                CartListOrdered.Bind(Profile.ShoppingCart.CartItems);

                // display total and credit card information
                ltlTotalComplete.Text = ltlTotal.Text;
                ltlCreditCardComplete.Text = ltlCreditCard.Text;

                // create order
                OrderInfo order = new OrderInfo(int.MinValue, DateTime.Now, User.Identity.Name, GetCreditCardInfo(), billingForm.Address, shippingForm.Address, Profile.ShoppingCart.Total, Profile.ShoppingCart.GetOrderLineItems(), null);

                // insert
                Order newOrder = new Order();
                newOrder.Insert(order);

                // destroy cart
                Profile.ShoppingCart.Clear();
                Profile.Save();
            }
        }
        else {
            lblMsg.Text = ”
Can not process the order. Your cart is empty.

Continue shopping

“;
            wzdCheckOut.Visible = false;
        }
    }

在上面的一段代码中,非常典型地表达了Model与View之间的关系。它通过获取控件的属性值,作为参数值传递给数据值对象OrderInfo,从而利用页面上产生的订单信息创建订单对象,然后再调用领域对象Order的Inser()方法将OrderInfo对象插入到数据表中。此外,它还对领域对象ShoppingCart的数据项作出判断,如果其值等于0,就在页面中显示UI提示信息。此时,View的内容决定了Model的值,而Model值反过来又决定了View的显示内容。

6.3  ASP.NET控件

ASP.NET控件是View对象最重要的组成部分,它充分利用了面向对象的设计思想,通过封装与继承构建一个个控件对象,使得用户在开发Web页面时,能够重用这些控件,甚至自定义自己的控件。在第8章中,我已经介绍了.NET Framework中控件的设计思想,通过引入一种“复合方式”的Composite模式实现了控件树。在ASP.NET控件中,System.Web.UI.Control就是这棵控件树的根,它定义了所有ASP.NET控件共有的属性、方法和事件,并负责管理和控制控件的整个执行生命周期。

Control基类并没有包含UI的特定功能,如果需要提供与UI相关的方法属性,就需要从System.Web.UI.WebControls.WebControl类派生。该类实际上也是Control类的子类,但它附加了诸如ForeColor、BackColor、Font等属性。

除此之外,还有一个重要的类是System.Web.UI.UserControl,即用户控件类,它同样是Control类的子类。我们可以自定义一些用户控件派生自UserControl,在Visual Studio的Design环境下,我们可以通过拖动控件的方式将多种类型的控件组合成一个自定义用户控件,也可以在codebehind方式下,为自定义用户控件类添加新的属性和方法。

整个ASP.NET控件类的层次结构如图6-2所示: 

6-2.gif

图6-2 ASP.NET控件类的层次结构

ASP.NET控件的执行生命周期如表6-1所示:

阶段

控件需要执行的操作

要重写的方法或事件

初始化
初始化在传入 Web 请求生命周期内所需的设置。
Init 事件(OnInit 方法)

加载视图状态
在此阶段结束时,就会自动填充控件的 ViewState 属性,控件可以重写 LoadViewState 方法的默认实现,以自定义状态还原。
LoadViewState 方法

处理回发数据
处理传入窗体数据,并相应地更新属性。
注意:只有处理回发数据的控件参与此阶段。
LoadPostData 方法(如果已实现 IPostBackDataHandler)

加载
执行所有请求共有的操作,如设置数据库查询。此时,树中的服务器控件已创建并初始化、状态已还原并且窗体控件反映了客户端的数据。
Load 事件(OnLoad 方法)

发送回发更改通知
引发更改事件以响应当前和以前回发之间的状态更改。
注意:只有引发回发更改事件的控件参与此阶段。
RaisePostDataChangedEvent 方法(如果已实现 IPostBackDataHandler)

处理回发事件
处理引起回发的客户端事件,并在服务器上引发相应的事件。
注意:只有处理回发事件的控件参与此阶段。
RaisePostBackEvent 方法(如果已实现 IPostBackEventHandler)

预呈现
在呈现输出之前执行任何更新。可以保存在预呈现阶段对控件状态所做的更改,而在呈现阶段所对的更改则会丢失。
PreRender 事件(OnPreRender 方法)

保存状态
在此阶段后,自动将控件的 ViewState 属性保持到字符串对象中。此字符串对象被发送到客户端并作为隐藏变量发送回来。为了提高效率,控件可以重写 SaveViewState 方法以修改 ViewState 属性。
SaveViewState 方法

呈现
生成呈现给客户端的输出。
Render 方法

处置
执行销毁控件前的所有最终清理操作。在此阶段必须释放对昂贵资源的引用,如数据库链接。
Dispose 方法

卸载
执行销毁控件前的所有最终清理操作。控件作者通常在 Dispose 中执行清除,而不处理此事件。
UnLoad 事件(On UnLoad 方法)

表6-1 ASP.NET控件的执行生命周期

在这里,控件设计利用了Template Method模式,Control基类提供了大部分protected虚方法,留待其子类改写其方法。以PetShop 4.0为例,就定义了两个ASP.NET控件,它们都属于System.Web.UI.WebControls.WebControl的子类。其中,CustomList控件派生自System.Web.UI.WebControls.DataList,CustomGrid控件则派生自System.Web.UI.WebControls.Repeater。

由于这两个控件都改变了其父类控件的呈现方式,故而,我们可以通过重写父类的Render虚方法,完成控件的自定义。例如CustomGrid控件:
public class CustomGrid : Repeater…
//Static constants
    protected const string HTML1 = ”

“;
    protected const string HTML2 = “

“;
    protected const string HTML3 = “
“;
    protected const string HTML4 = “

“;
    private static readonly Regex RX = new Regex(@”^&page=\d+”,
RegexOptions.Compiled);
    private const string LINK_PREV = “< Previous“;
    private const string LINK_MORE = “More >“;
private const string KEY_PAGE = “page”;
    private const string COMMA = “?”;
    private const string AMP = “&”;

override protected void Render(HtmlTextWriter writer) {

        //Check there is some data attached
        if (ItemCount == 0) {
            writer.Write(emptyText);
            return;
        }
        //Mask the query
        string query = Context.Request.Url.Query.Replace(COMMA, AMP);
        query = RX.Replace(query, string.Empty);
        // Write out the first part of the control, the table header
        writer.Write(HTML1);
        // Call the inherited method
        base.Render(writer);
        // Write out a table row closure
        writer.Write(HTML2);
        //Determin whether next and previous buttons are required
        //Previous button?
        if (currentPageIndex > 0)
            writer.Write(string.Format(LINK_PREV, (currentPageIndex - 1) + query));
        //Close the table data tag
        writer.Write(HTML3);

        //Next button?
        if (currentPageIndex < PageCount)
            writer.Write(string.Format(LINK_MORE, (currentPageIndex + 1) + query));

        //Close the table
        writer.Write(HTML4);
    }

由于CustomGrid继承自Repeater控件,因而它同时还继承了Repeater的DataSource属性,这是一个虚属性,它默认的set访问器属性如下:
public virtual object DataSource
{
      get  {… }
      set
      {
            if (((value != null) && !(value is IListSource)) && !(value is IEnumerable))
            {
                  throw new ArgumentException(SR.GetString("Invalid_DataSource_Type", new object[] { this.ID }));
            }
            this.dataSource = value;
            this.OnDataPropertyChanged();
      }
}

对于CustomGrid而言,DataSource属性有着不同的设置行为,因而在定义CustomGrid控件的时候,需要改写DataSource虚属性,如下所示:
private IList dataSource;
private int itemCount;

override public object DataSource {
    set {
    //This try catch block is to avoid issues with the VS.NET designer
        //The designer will try and bind a datasource which does not derive from ILIST
        try {
            dataSource = (IList)value;
            ItemCount = dataSource.Count;
        }
        catch {
            dataSource = null;
            ItemCount = 0;
        }
    }
}

当设置的value对象值不为IList类型时,set访问器就将捕获异常,然后将dataSource字段设置为null。

由于我们改写了DataSource属性,因而改写Repeater类的OnDataBinding()方法也就势在必行。此外,CustomGrid还提供了分页的功能,我们也需要实现分页的相关操作。与DataSource属性不同,Repeater类的OnDataBinding()方法实际上是继承和改写了Control基类的OnDataBinding()虚方法,而我们又在此基础上改写了Repeater类的OnDataBinding()方法:
override protected void OnDataBinding(EventArgs e) {

    //Work out which items we want to render to the page
    int start = CurrentPageIndex * pageSize;
    int size = Math.Min(pageSize, ItemCount - start);

    IList page = new ArrayList();
    //Add the relevant items from the datasource
    for (int i = 0; i < size; i++)
        page.Add(dataSource[start + i]);

    //set the base objects datasource
    base.DataSource = page;
    base.OnDataBinding(e);
}

此外,CustomGrid控件类还增加了许多属于自己的属性和方法,例如PageSize、PageCount属性以及SetPage()方法等。正是因为ASP.NET控件引入了Composite模式与Template Method模式,当我们在自定义控件时,就可以通过继承与改写的方式来完成控件的设计。自定义ASP.NET控件一方面可以根据系统的需求实现特定的功能,也能够最大限度地实现对象的重用,既可以减少编码量,同时也有利于未来对程序的扩展与修改。
在PetShop 4.0中,除了自定义了上述WebControl控件的子控件外,最主要的还是利用了用户控件。在Controls文件夹下,一共定义了11个用户控件,内容涵盖客户地址信息、信用卡信息、购物车信息、期望列表(Wish List)信息以及导航信息、搜索结果信息等。它们相当于是一些组合控件,除了包含了子控件的方法和属性外,也定义了一些必要的UI实现逻辑。以ShoppingCartControl用户控件为例,它会在该控件被呈现(Render)之前,做一些数据准备工作,获取购物车数据,并作为数据源绑定到其下的Repeater控件:
public partial class ShoppingCartControl : System.Web.UI.UserControl...
    protected void Page_PreRender(object sender, EventArgs e) {
        if (!IsPostBack) {
            BindCart();               
        }
    }
    private void BindCart() {

        ICollection cart = Profile.ShoppingCart.CartItems;
        if (cart.Count > 0) {
            repShoppingCart.DataSource = cart;
            repShoppingCart.DataBind();
            PrintTotal();
            plhTotal.Visible = true;
        }
        else {
            repShoppingCart.Visible = false;
            plhTotal.Visible = false;
            lblMsg.Text = “Your cart is empty.”;
        }
    }

在ShoppingCart页面下,我们可以加入该用户控件,如下所示:

由于ShoppingCartControl用户控件已经实现了用于呈现购物车数据的逻辑,那么在ShoppingCart.aspx.cs中,就可以不用负责这些逻辑,在充分完成对象重用的过程中,同时又达到了职责分离的目的。用户控件的设计者与页面设计者可以互不干扰,分头完成自己的设计。特别是对于页面设计者而言,他可以是单一的UI设计人员角色,仅需要关注用户界面是否美观与友好,对于表示层中对领域对象的调用与操作就可以不必理会,整个页面的代码也显得结构清晰、逻辑清楚,无疑也“干净”了不少。

6.4  ASP.NET 2.0新特性

由于PetShop 4.0是基于.NET Framework 2.0平台开发的电子商务系统,因而它在表示层也引入了许多ASP.NET 2.0的新特性,例如MemberShip、Profile、Master Page、登录控件等特性。接下来,我将结合PetShop 4.0的设计分别介绍它们的实现。

6.4.1  Profile特性

Profile提供的功能是针对用户的个性化服务。在ASP.NET 1.x版本时,我们可以利用Session、Cookie等方法来存储用户的状态信息。然而Session对象是具有生存期的,一旦生存期结束,该对象保留的值就会失效。Cookie将用户信息保存在客户端,它具有一定的安全隐患,一些重要的信息不能存储在Cookie中。一旦客户端禁止使用Cookie,则该功能就将失去应用的作用。

Profile的出现解决了如上的烦恼,它可以将用户的个人化信息保存在指定的数据库中。ASP.NET 2.0的Profile功能默认支持Access数据库和SQL Server数据库,如果需要支持其他数据库,可以编写相关的ProfileProvider类。Profile对象是强类型的,我们可以为用户信息建立属性,以PetShop 4.0为例,它建立了ShoppingCart、WishList和AccountInfo属性。

由于Profile功能需要访问数据库,因而在数据访问层(DAL)定义了和Product等数据表相似的模块结构。首先定义了一个IProfileDAL接口模块,包含了接口IPetShopProfileProvider:
public interface IPetShopProfileProvider

 AddressInfo GetAccountInfo(string userName, string appName);  
 void SetAccountInfo(int uniqueID, AddressInfo addressInfo);
 IList GetCartItems(string userName, string appName,
bool isShoppingCart);
 void SetCartItems(int uniqueID, ICollection cartItems,
bool isShoppingCart);
 void UpdateActivityDates(string userName, bool activityOnly, string appName);
 int GetUniqueID(string userName, bool isAuthenticated, bool ignoreAuthenticationType,
 string appName);
 int CreateProfileForUser(string userName, bool isAuthenticated, string appName);
 IList GetInactiveProfiles(int authenticationOption,
DateTime userInactiveSinceDate, string appName);
 bool DeleteProfile(string userName, string appName);   
 IList GetProfileInfo(int authenticationOption,
string usernameToMatch, DateTime userInactiveSinceDate, string appName,
out int totalRecords);
}

因为PetShop 4.0版本分别支持SQL Server和Oracle数据库,因而它分别定义了两个不同的PetShopProfileProvider类,实现IPetShopProfileProvider接口,并放在两个不同的模块SQLProfileDAL和OracleProfileDAL中。具体的实现请参见PetShop 4.0的源代码。
同样的,PetShop 4.0为Profile引入了工厂模式,定义了模块ProfileDALFActory,工厂类DataAccess的定义如下:
public sealed class DataAccess {

    private static readonly string profilePath = ConfigurationManager.AppSettings[”ProfileDAL”];
    public static PetShop.IProfileDAL.IPetShopProfileProvider CreatePetShopProfileProvider() {
 string className = profilePath + “.PetShopProfileProvider”;
 return (PetShop.IProfileDAL.IPetShopProfileProvider)Assembly.Load(profilePath).CreateInstance(className);
    }
}

在业务逻辑层(BLL)中,单独定义了模块Profile,它添加了对BLL、IProfileDAL和ProfileDALFactory模块的程序集。在该模块中,定义了密封类PetShopProfileProvider,它继承自System.Web.Profile.ProfileProvider类,该类作为Profile的Provider基类,用于在自定义配置文件中实现相关的配置文件服务。在PetShopProfileProvider类中,重写了父类ProfileProvider中的一些方法,例如Initialize()、GetPropertyValues()、SetPropertyValues()、DeleteProfiles()等方法。此外,还为ShoppingCart、WishList、AccountInfo属性提供了Get和Set方法。至于Provider的具体实现,则调用工厂类DataAccess创建的具体类型对象,如下所示:
private static readonly IPetShopProfileProvider dal = DataAccess.CreatePetShopProfileProvider();

定义了PetShop.Profile.PetShopProfileProvider类后,才可以在web.config配置文件中配置如下的配置节:

在配置文件中,针对ShoppingCart、WishList和AccountInfo(它们的类型分别为PetShop.BLL.Cart、PetShop.BLL.Cart、PetShop.Model.AddressInfo)属性分别定义了ShoppingCartProvider、WishListProvider、AccountInfoProvider,它们的类型均为PetShop.Profile.PetShopProfileProvider类型。至于Profile的信息究竟是存储在何种类型的数据库中,则由以下的配置节决定:

而键值为ProfileDAL的值,正是Profile的工厂类PetShop.ProfileDALFactory.DataAccess在利用反射技术创建IPetShopProfileProvider类型对象时获取的。

在表示层中,可以利用页面的Profile属性访问用户的个性化属性,例如在ShoppingCart页面的codebehind代码ShoppingCart.aspx.cs中,调用Profile的ShoppingCart属性:
public partial class ShoppingCart : System.Web.UI.Page {

    protected void Page_PreInit(object sender, EventArgs e) {
        if (!IsPostBack) {
            string itemId = Request.QueryString[”addItem”];
            if (!string.IsNullOrEmpty(itemId)) {
                Profile.ShoppingCart.Add(itemId);
                Profile.Save();
                // Redirect to prevent duplictations in the cart if user hits “Refresh”
                Response.Redirect(”~/ShoppingCart.aspx”, true);
            }
        }
    }
}

在上述的代码中,Profile属性的值从何而来?实际上,在我们为web.config配置文件中对Profile进行配置后,启动Web应用程序,ASP.NET会根据该配置文件中的相关配置创建一个ProfileCommon类的实例。该类继承自System.Web.Profile.ProfileBase类。然后调用从父类继承来的GetPropertyValue和SetPropertyValue方法,检索和设置配置文件的属性值。然后,ASP.NET将创建好的ProfileCommon实例设置为页面的Profile属性值。因而,我们可以通过智能感知获取Profile的ShoppingCart属性,同时也可以利用ProfileCommon继承自ProfileBase类的Save()方法,根据属性值更新Profile的数据源。

6.4.2  Membership特性

PetShop 4.0并没有利用Membership的高级功能,而是直接让Membership特性和ASP.NET 2.0新增的登录控件进行绑定。由于.NET Framework 2.0已经定义了针对SQL Server的SqlMembershipProvider,因此对于PetShop 4.0而言,实现Membership比之实现Profile要简单,仅仅需要为Oracle数据库定义MembershipProvider即可。在PetShop.Membership模块中,定义了OracleMembershipProvider类,它继承自System.Web.Security.MembershipProvider抽象类。

OracleMembershipProvider类的实现具有极高的参考价值,如果我们需要定义自己的MembershipProvider类,可以参考该类的实现。
事实上OracleMemberShip类的实现并不复杂,在该类中,主要是针对用户及用户安全而实现相关的行为。由于在父类MembershipProvider中,已经定义了相关操作的虚方法,因此我们需要作的是重写这些虚方法。由于与Membership有关的信息都是存储在数据库中,因而OracleMembershipProvider与SqlMembershipProvider类的主要区别还是在于对数据库的访问。对于SQL Server而言,我们利用aspnet_regsql工具为Membership建立了相关的数据表以及存储过程。也许是因为知识产权的原因,Microsoft并没有为Oracle数据库提供类似的工具,因而需要我们自己去创建membership的数据表。此外,由于没有创建Oracle数据库的存储过程,因而OracleMembershipProvider类中的实现是直接调用SQL语句。以CreateUser()方法为例,剔除那些繁杂的参数判断与安全性判断,SqlMembershipProvider类的实现如下:
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
      MembershipUser user1;
      //前面的代码略;
      try
      {
            SqlConnectionHolder holder1 = null;
            try
            {
                  holder1 = SqlConnectionHelper.GetConnection(this._sqlConnectionString, true);
                  this.CheckSchemaVersion(holder1.Connection);
                  DateTime time1 = this.RoundToSeconds(DateTime.UtcNow);
                  SqlCommand command1 = new SqlCommand(”dbo.aspnet_Membership_CreateUser”, holder1.Connection);
                  command1.CommandTimeout = this.CommandTimeout;
                  command1.CommandType = CommandType.StoredProcedure;
                  command1.Parameters.Add(this.CreateInputParam(”@ApplicationName”, SqlDbType.NVarChar, this.ApplicationName));
                  command1.Parameters.Add(this.CreateInputParam(”@UserName”, SqlDbType.NVarChar, username));
                  command1.Parameters.Add(this.CreateInputParam(”@Password”, SqlDbType.NVarChar, text2));
                  command1.Parameters.Add(this.CreateInputParam(”@PasswordSalt”, SqlDbType.NVarChar, text1));
                  command1.Parameters.Add(this.CreateInputParam(”@Email”, SqlDbType.NVarChar, email));
                  command1.Parameters.Add(this.CreateInputParam(”@PasswordQuestion”, SqlDbType.NVarChar, passwordQuestion));
                  command1.Parameters.Add(this.CreateInputParam(”@PasswordAnswer”, SqlDbType.NVarChar, text3));
                  command1.Parameters.Add(this.CreateInputParam(”@IsApproved”, SqlDbType.Bit, isApproved));
                  command1.Parameters.Add(this.CreateInputParam(”@UniqueEmail”, SqlDbType.Int, this.RequiresUniqueEmail ? 1 : 0));
                  command1.Parameters.Add(this.CreateInputParam(”@PasswordFormat”, SqlDbType.Int, (int) this.PasswordFormat));
                  command1.Parameters.Add(this.CreateInputParam(”@CurrentTimeUtc”, SqlDbType.DateTime, time1));
                  SqlParameter parameter1 = this.CreateInputParam(”@UserId”, SqlDbType.UniqueIdentifier, providerUserKey);
                  parameter1.Direction = ParameterDirection.InputOutput;
                  command1.Parameters.Add(parameter1);
                  parameter1 = new SqlParameter(”@ReturnValue”, SqlDbType.Int);
                  parameter1.Direction = ParameterDirection.ReturnValue;
                  command1.Parameters.Add(parameter1);
                  command1.ExecuteNonQuery();
                  int num3 = (parameter1.Value != null) ? ((int) parameter1.Value) : -1;
                  if ((num3 < 0) || (num3 > 11))
                  {
                        num3 = 11;
                  }
                  status = (MembershipCreateStatus) num3;
                  if (num3 != 0)
                  {
                        return null;
                  }
                  providerUserKey = new Guid(command1.Parameters[”@UserId”].Value.ToString());
                  time1 = time1.ToLocalTime();
                  user1 = new MembershipUser(this.Name, username, providerUserKey, email, passwordQuestion, null, isApproved, false, time1, time1, time1, time1, new DateTime(0×6da, 1, 1));
            }
            finally
            {
                  if (holder1 != null)
                  {
                        holder1.Close();
                        holder1 = null;
                  }
            }
      }
      catch
      {
            throw;
      }
      return user1;
}

代码中,aspnet_Membership_CreateUser为aspnet_regsql工具为membership创建的存储过程,它的功能就是创建一个用户。

OracleMembershipProvider类中对CreateUser()方法的定义如下:
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object userId, out MembershipCreateStatus status) {
    //前面的代码略;
 //Create connection
 OracleConnection connection = new OracleConnection(OracleHelper.ConnectionStringMembership);
 connection.Open();
 OracleTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
 try {
  DateTime dt = DateTime.Now;
  bool isUserNew = true;

  // Step 1: Check if the user exists in the Users table: create if not   
  int uid = GetUserID(transaction, applicationId, username, true, false, dt, out isUserNew);
  if(uid == 0) { // User not created successfully!
   status = MembershipCreateStatus.ProviderError;
   return null;
  }
  // Step 2: Check if the user exists in the Membership table: Error if yes.
  if(IsUserInMembership(transaction, uid)) {
   status = MembershipCreateStatus.DuplicateUserName;
   return null;
  }
  // Step 3: Check if Email is duplicate
  if(IsEmailInMembership(transaction, email, applicationId)) {
   status = MembershipCreateStatus.DuplicateEmail;
   return null;
  }
  // Step 4: Create user in Membership table     
  int pFormat = (int)passwordFormat;
  if(!InsertUser(transaction, uid, email, pass, pFormat, salt, “”, “”, isApproved, dt)) {
   status = MembershipCreateStatus.ProviderError;
   return null;
  }
  // Step 5: Update activity date if user is not new
  if(!isUserNew) {
   if(!UpdateLastActivityDate(transaction, uid, dt)) {
    status = MembershipCreateStatus.ProviderError;
    return null;
   }
  }
  status = MembershipCreateStatus.Success;
  return new MembershipUser(this.Name, username, uid, email, passwordQuestion, null, isApproved, false, dt, dt, dt, dt, DateTime.MinValue);
 }
 catch(Exception) {
  if(status == MembershipCreateStatus.Success)
   status = MembershipCreateStatus.ProviderError;
  throw;
 }
 finally {
  if(status == MembershipCreateStatus.Success)
   transaction.Commit();
  else
   transaction.Rollback();
  connection.Close();
  connection.Dispose();
 }
}

代码中,InsertUser()方法就是负责用户的创建,而在之前则需要判断创建的用户是否已经存在。InsertUser()方法的定义如下:
private static bool InsertUser(OracleTransaction transaction, int userId, string email, string password, int passFormat, string passSalt, string passQuestion, string passAnswer, bool isApproved, DateTime dt) {

 string insert = “INSERT INTO MEMBERSHIP (USERID, EMAIL, PASSWORD, PASSWORDFORMAT, PASSWORDSALT, PASSWORDQUESTION, PASSWORDANSWER, ISAPPROVED, CREATEDDATE, LASTLOGINDATE, LASTPASSWORDCHANGEDDATE) VALUES (:UserID, :Email, :P ass, :P asswordFormat, :P asswordSalt, :P asswordQuestion, :P asswordAnswer, :IsApproved, :CDate, :LLDate, :LPCDate)”;
 OracleParameter[] insertParms = { new OracleParameter(”:UserID”, OracleType.Number, 10), new OracleParameter(”:Email”, OracleType.VarChar, 128), new OracleParameter(”:Pass”, OracleType.VarChar, 128), new OracleParameter(”:PasswordFormat”, OracleType.Number, 10), new OracleParameter(”:PasswordSalt”, OracleType.VarChar, 128), new OracleParameter(”:PasswordQuestion”, OracleType.VarChar, 256), new OracleParameter(”:PasswordAnswer”, OracleType.VarChar, 128), new OracleParameter(”:IsApproved”, OracleType.VarChar, 1), new OracleParameter(”:CDate”, OracleType.DateTime), new OracleParameter(”:LLDate”, OracleType.DateTime), new OracleParameter(”:LPCDate”, OracleType.DateTime) };
 insertParms[0].Value = userId;
 insertParms[1].Value = email;
 insertParms[2].Value = password;
 insertParms[3].Value = passFormat;
 insertParms[4].Value = passSalt;
 insertParms[5].Value = passQuestion;
 insertParms[6].Value = passAnswer;
 insertParms[7].Value = OracleHelper.OraBit(isApproved);
 insertParms[8].Value = dt;
 insertParms[9].Value = dt;
 insertParms[10].Value = dt;

 if(OracleHelper.ExecuteNonQuery(transaction, CommandType.Text, insert, insertParms) != 1)
  return false;
 else
  return true;
}

在为Membership建立了Provider类后,还需要在配置文件中配置相关的配置节,例如SqlMembershipProvider的配置:

对于OracleMembershipProvider而言,配置大致相似:

   type="PetShop.Membership.OracleMembershipProvider"
   connectionStringName="OraMembershipConnString"
   enablePasswordRetrieval="false"
   enablePasswordReset="false"
   requiresUniqueEmail="false"
   requiresQuestionAndAnswer="false"
   minRequiredPasswordLength="7"
   minRequiredNonalphanumericCharacters="1"
   applicationName=".NET Pet Shop 4.0"
   hashAlgorithmType="SHA1"
   passwordFormat="Hashed"/>
有关配置节属性的意义,可以参考MSDN等相关文档。

6.4.3  ASP.NET登录控件

这里所谓的登录控件并不是指一个控件,而是ASP.NET 2.0新提供的一组用于解决用户登录的控件。登录控件与Membership进行集成,快速简便地实现用户登录的处理。ASP.NET登录控件包括Login控件、LoginView控件、LoginStatus控件、LoginName控件、PasswordRescovery控件、CreateUserWizard控件以及ChangePassword控件。
PetShop 4.0犹如一本展示登录控件用法的完美教程。我们可以从诸如SignIn、NewUser等页面中,看到ASP.NET登录控件的使用方法。例如在SignIn.aspx中,用到了Login控件。在该控件中,可以包含TextBox、Button等类型的控件,用法如下所示:
又例如NewUser.aspx中对CreateUserWizard控件的使用:
RequireEmail="False" SkinID="NewUser">
使用了登录控件后,我们毋需编写与用户登录相关的代码,登录控件已经为我们完成了相关的功能,这就大大地简化了这个系统的设计与实现。

6.4.4  Master Page特性

Master Page相当于是整个Web站点的统一模板,建立的Master Page文件扩展名为.master。它可以包含静态文本、html元素和服务器控件。Master Page由特殊的@Master指令识别,如:
< %@ Master Language="C#" CodeFile="MasterPage.master.cs" Inherits="MasterPage" %>

使用Master Page可以为网站建立一个统一的样式,且能够利用它方便地创建一组控件和代码,然后将其应用于一组页。对于那些样式与功能相似的页而言,利用Master Page就可以集中处理为Master Page,一旦进行修改,就可以在一个位置上进行更新。

在PetShop 4.0中,建立了名为MasterPage.master的Master Page,它包含了header、LoginView控件、导航菜单以及用于呈现内容的html元素,如图6-3所示: 

6-3.gif

图6-3 PetShop 4.0的Master Page

@Master指令的定义如下:
< %@ Master Language="C#" AutoEventWireup="true" CodeFile="MasterPage.master.cs" Inherits="PetShop.Web.MasterPage" %>

Master Page同样利用codebehind技术,以PetShop 4.0的Master Page为例,codebehind的代码放在文件MasterPage.master.cs中:
public partial class MasterPage : System.Web.UI.MasterPage {

    private const string HEADER_PREFIX = “.NET Pet Shop :: {0}”;

    protected void Page_PreRender(object sender, EventArgs e) { 
        ltlHeader.Text = Page.Header.Title;
        Page.Header.Title = string.Format(HEADER_PREFIX, Page.Header.Title);         
    }
    protected void btnSearch_Click(object sender, EventArgs e) {
        WebUtility.SearchRedirect(txtSearch.Text);   
    }
}

注意Master Page页面不再继承自System.Web.UI.Page,而是继承System.Web.UI.MasterPage类。与Page类继承TemplateControl类不同,它是UserControl类的子类。因此,可以应用在Master Page上的有效指令与UserControl的可用指令相同,例如AutoEventWireup、ClassName、CodeFile、EnableViewState、WarningLevel等。

每一个与Master Page相关的内容页必须在@Page指令的MasterPageFile属性中引用相关的Master Page。例如PetShop 4.0中的CheckOut内容页,其@Page指令的定义如下:
< %@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true" CodeFile="CheckOut.aspx.cs" Inherits="PetShop.Web.CheckOut" Title="Check Out" %>

Master Page可以进行嵌套,例如我们建立了父Master Page页面Parent.master,那么在子Master Page中,可以利用master属性指定其父MasterPage:
< %@ Master Language="C#" master="Parent.master"%>

而内容页则可以根据情况指向Parent.master或者Child.master页面。

虽然说Master Page大部分情况下是以声明方式创建,但我们也可以建立一个类继承System.Web.UI.MasterPage,从而完成对Master Page的编程式创建。但在采用这种方式的同时,应该同时创建.master文件。此外对Master Page的调用也可以利用编程的方式完成,例如动态地添加Master Page,我们重写内容页的Page_PreInit()方法,如下所示:
void Page_PreInit(Object sender, EventArgs e)
{
    this.MasterPageFile = “~/NewMaster.master”;
}

之所以重写Page_PreInit()方法,是因为Master Page会在内容页初始化阶段进行合并,也即是说是在PreInit阶段完成Master Page的分配。
ASP.NET 2.0引入的新特性,并不仅仅限于上述介绍的内容。例如Theme、Wizard控件等新特性在PetShop 4.0中也得到了大量的应用。虽然ASP.NET 2.0及时地推陈出新,对表示层的设计有所改善,然而作为ASP.NET 2.0的其中一部分,它们仅仅是对现有框架缺失的弥补与改进,属于“锦上添花”的范畴,对于整个表示层设计技术而言,起到的推动作用却非常有限。

直到AJAX(Asynchronous JavaScript and XML)的出现,整个局面才大为改观。虽然AJAX技术带有几分“旧瓶装新酒”的味道,然而它从诞生之初,就具备了王者气象,大有席卷天下之势。各种支持AJAX技术的框架如雨后春笋般纷纷吐出新芽,支撑起百花齐放的繁荣,气势汹汹地营造出唯AJAX独尊的态势。如今,AJAX已经成为了Web应用的主流开发技术,许多业界大鳄都呲牙咧嘴开始了对这一块新领地的抢滩登陆。例如IBM、Oracle、Yahoo等公司都纷纷启动了开源的AJAX项目。微软也不甘落后,及时地推出了ASP.NET AJAX,这是一个基于ASP.NET的AJAX框架,它包括了ASP.NET AJAX服务端组件和ASP.NET AJAX客户端组件,并集成在Visual Studio中,为ASP.NET开发者提供了一个强大的AJAX应用环境。

我现在还无法预知AJAX技术在未来的走向,然而单单从表示层设计的角度而言,AJAX技术亦然带了一场全新的革命。我们或者可以期待未来的PetShop 5.0,可以在表示层设计上带来更多的惊喜。