State模式的经典应用场景:订单处理(c#实现)
State模式在对象内部状态发生变化的时候,改变自身的行为,这通常是通过切换内部状态对象实现的,对象将自身在各个状态的行为推给了状态对象,从而解开了行为与对象的依赖。
场景描述
在经典的订单处理场景中,订单其不同状态的时候变现了不同的行为,具体内容如下:
- 假如订单是一个新创建的订单,那么它可以被寄送,也可以被取消;
- 假如订单已经被寄送,那么它不可以被再次寄送,也不可以被取消;
- 假如订单已经被取消,那么它不可以被寄送,也不可以被取消。
上述内容中详细解释了订单状态和对应行为的关系。
遇到问题
对逻辑的第一映像,通常是通过if-else或者switch子句,通过订单内部的一个表示状态的属性,判断出当前订单是否可以寄送和取消。
可是这样严重影响了程序代码的课扩展性,想想一下,如果我需要添加一种叫做“已出库”的状态,此时订单表现为可以被取消但是不可以再次申请寄送,那我们就需要在if-else子句中添加新的逻辑;又或者我们需要改变业务规则寄送的订单可以在没有完成前取消,那么我们又需要对订单的实现代码做逻辑更改,很明显,这样对扩展性来说是一个大问题。
所以,我们的解决方案是将订单的行为推送到订单状态自身,这样即使扩展再多的订单状态或者对状态行为进行更改,也可以轻松应对,只对很少的类进行更改,并且不会牵涉到太多代码逻辑。
解决问题走起
首先创建一个订表示订单状态的枚举OrderStatus
namespace Pattern.State
{
public enum OrderStatus
{
New=0,
Shipped=1,
Canceled=2
}
}
然后创建一个借口IOrderState,定义订单的行为和保存订单的状态枚举值
namespace Pattern.State
{
public interface IOrderState
{
bool CanShip(Order order);
void Ship(Order order);
bool CanCancel(Order order);
void Cancel(Order order);
OrderStatus Status { get; }
}
}
接下来就是最重要的Order类
namespace Pattern.State
{
public class Order
{
private IOrderState orderState;
public Order(IOrderState orderState)
{
this.orderState = orderState;
}
public int Id { get; set; }
public string CustomerName { get; set; }
public string Address { get; set; }
public OrderStatus Status()
{
return orderState.Status;
}
public bool CanCancel()
{
return orderState.CanCancel(this);
}
public void Cancel()
{
if (CanCancel())
{
orderState.Cancel(this);
}
}
public bool CanShip()
{
return orderState.CanShip(this);
}
public void Ship()
{
if (CanShip())
{
orderState.Ship(this);
}
}
internal void Change(IOrderState orderState)
{
this.orderState = orderState;
}
}
}
你可以看到,本来想象中的复杂了代码逻辑没有了,代码变得更易懂易扩展,因为我们将这些行为转到了IOrderState的子类中,单个子类只维护当前状态下订单的行为:
1.NewState
namespace Pattern.State
{
public class NewState:IOrderState
{
public bool CanShip(Order order)
{
//some logic here
return true;
}
public void Ship(Order order)
{
order.Change(new ShippedState());
}
public bool CanCancel(Order order)
{
return true;
}
public void Cancel(Order order)
{
order.Change(new CanceledState());
}
public OrderStatus Status
{
get { return OrderStatus.New; }
}
}
}
2.ShippedState
namespace Pattern.State
{
public class ShippedState:IOrderState
{
public bool CanShip(Order order)
{
return false;
}
public void Ship(Order order)
{
throw new InvalidOperationException();
}
public bool CanCancel(Order order)
{
return false;
}
public void Cancel(Order order)
{
throw new InvalidOperationException();
}
public OrderStatus Status
{
get { return OrderStatus.Shipped }
}
}
}
3.CanceledState
namespace Pattern.State
{
public class CanceledState:IOrderState
{
public bool CanShip(Order order)
{
return false;
}
public void Ship(Order order)
{
throw new InvalidOperationException();
}
public bool CanCancel(Order order)
{
return false;
}
public void Cancel(Order order)
{
throw new InvalidOperationException();
}
public OrderStatus Status
{
get { return OrderStatus.Canceled; }
}
}
}
最后我们创建一个OrderFactory
namespace Pattern.State
{
public static class OrderFactory
{
public static Order CreateOrder(string customerName, string address)
{
IOrderState orderState = new NewState();
Order order = new Order(orderState);
return order;
}
}
}
最后,通过一个控制台应用程序来测试一下:
namespace Pattern.Console
{
class Program
{
static void Main(string[] args)
{
Order order = OrderFactory.CreateOrder("小白哥哥", "天津市和平区");
if (order.CanShip())
{
System.Console.WriteLine("订单当前可以寄送");
}
order.Ship();
if (!order.CanShip())
{
System.Console.WriteLine("订单当前不可以寄送");
}
System.Console.ReadKey();
}
}
}

ASP.NET MVC学习之过滤器(一)
一、前言
继前面四篇ASP.NET MVC的随笔,我们继续向下学习。上一节我们学习了关于控制器的使用,本节我们将要学习如何使用过滤器控制用户访问页面。
二、正文
以下的示例建立在ASP.NET MVC 4之上(VS2012)
1.授权过滤器
只要涉及用户的网站,都一定会涉及到什么权限的用户可以访问哪个页面。对于新手而言可能都在每个页面中单独写这个功能方法,导致的后果就是大量重复的代码,并且不便于以后的变动。有用一定经验之后,就会采用集中控制的方式,让所有的页面先执行特定的方法去判断,这样的优点就是减少了代码的重复,同时也能够灵活的配置。但是对于某些拥有特殊需求的页面就有点力所不能及,而且这种权限判断的代码会和页面的逻辑代码混为一体,难以区分。而今天介绍的授权过滤器是采用注解属性的方式去控制每个动作,并且可以灵活的配置。
首先我们在网站根目录新建一个Filter文件夹,在其中新建一个CustAuthorizeAttribute类,类中的具体代码如下所示:
1 namespace MvcStudy.Filter 2 { 3 public class CustAuthorizeAttribute : AuthorizeAttribute 4 { 5 private string[] roles; 6 7 public CustAuthorizeAttribute(params String[] role) 8 { 9 roles = role; 10 } 11 12 protected override bool AuthorizeCore(HttpContextBase httpContext) 13 { 14 String role = httpContext.Request.QueryString["role"]; 15 if (role != null) 16 { 17 return roles.Contains(role); 18 } 19 return base.AuthorizeCore(httpContext); 20 } 21 } 22 }
通过上面的代码我们知道这个过滤器将会在使用时要求使用者传入可以访问的角色有哪些,其中最终的是AuthorizeCore方法,该方法是负责判断当前的请求是有具有权限,这里我们为了演示,所以直接根据查询字符串的role的值来判断。
既然我们已经有了过滤器,下面我们新建一个Home控制器,并在Views下新建一个Home文件夹,同时在Home文件夹下新建名为Index的视图以及List的视图。接着我们打开Home控制器。
在其中写入如下代码:
1 namespace MvcStudy.Controllers 2 { 3 public class HomeController : Controller 4 { 5 [CustAuthorize("vip")] 6 public ActionResult Index() 7 { 8 return View(); 9 } 10 11 [CustAuthorize("admin")] 12 public ActionResult List() 13 { 14 return View(); 15 } 16 } 17 }
我们可以看到笔者将Index动作规定为只有role为vip才能访问,而List则是admin。接着我们运行项目,浏览器会默认打开http://localhost:7575/(端口请根据实际情况而定),但是你会发现你将会被转移到http://localhost:7575/Account/Login?ReturnUrl=%2f这个页面,因为项目中我们并没有新建这个视图,所以会报404错误。而这个是被通过授权后ASP.NET MVC默认的行为,当然我们可以改变这个行为,下面我们来改变这个行为,让其直接跳转到Login视图,修改CustAuthorizeAttribute类。
代码如下所示:
1 namespace MvcStudy.Filter 2 { 3 public class CustAuthorizeAttribute : AuthorizeAttribute 4 { 5 private string[] roles; 6 7 public CustAuthorizeAttribute(params String[] role) 8 { 9 roles = role; 10 } 11 12 protected override bool AuthorizeCore(HttpContextBase httpContext) 13 { 14 String role = httpContext.Request.QueryString["role"]; 15 if (role != null) 16 { 17 return roles.Contains(role); 18 } 19 return base.AuthorizeCore(httpContext); 20 } 21 22 protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) 23 { 24 UrlHelper url = new UrlHelper(filterContext.RequestContext); 25 filterContext.Result = new RedirectResult("/Login"); 26 } 27 } 28 }
其中关键的是我们重写了HandleUnauthorizedRequest方法,这个方法只有在AuthorizeCore返回false情况才会调用,同时该方法的参数中有一个Result属性,该属性是将最终的处理结果赋给它。
重新编译,我们继续访问默认的路径,这个时候我们可以发现转移的路径已经变成了http://localhost:7575/Login,到这里我们已经可以处理授权失败的情况了,现在我们就要开始测试几个正确的结果首先是Index,我们只需要访问该路径即可http://localhost:7575/Home/Index?role=vip(同http://localhost:7575/?role=vip),如果要访问List,我们就需要访问这个路径http://localhost:7575/Home/List?role=admin,通过这个简单的例子,我们就可以知道在ASP.NET MVC中是通过何种方式去控制用户访问页面的权限。
2.异常过滤器
在我们的开发过程我们很多的时间都在修改异常,防止异常的发生。但是在实际的运行过程中总会发生很多我们未曾发现的问题,以及其他恶意的攻击。这个时候我们就需要一个统一的机制可以控制并处理这些异常,或许我一说到异常很多人都会联想到try{}catch{},的确没错,异常的捕获是用他们,但是你有没有想过,如果一个网站导出充斥着try{}catch{},不仅仅是不美观,同时也能以控制。而ASP.NET MVC为我们提供了异常过滤器,可以为每个动作添加这个注解属性,这样我们就可以灵活的控制并处理这些异常。
首先我们在Filter文件中新建一个CustExceptionAttribute类,并在其中写入如下的代码:
1 namespace MvcStudy.Filter 2 { 3 public class CustExceptionAttribute : FilterAttribute , IExceptionFilter 4 { 5 public void OnException(ExceptionContext filterContext) 6 { 7 if (!filterContext.ExceptionHandled) 8 { 9 filterContext.Result = new RedirectResult("error.html"); 10 filterContext.ExceptionHandled = true; 11 } 12 } 13 } 14 }
我们可以看到这里我们依然还是用Result来将我们处理后的结果赋值给它,但是我们这里还判断了ExceptionHandled属性,这个属性的含义是如果其他的异常过滤器已经处理了该异常,则该属性的值为true,为了能够适应大的范围,所以笔者这里建议读者也要先判断是否已经有其他的异常过滤器已经处理过这个异常了。
下面我们在Home控制器中的Index动作中使用:
1 namespace MvcStudy.Controllers 2 { 3 public class HomeController : Controller 4 { 5 [CustException()] 6 public ActionResult Index() 7 { 8 throw new NullReferenceException(); 9 return View(); 10 } 11 } 12 }
这个时候我们访问http://localhost:7575/将会跳转至http://localhost:7575/error.html页面(因为笔者没有建这个页面所以会显示404),但是读者可以看到这里我仅仅只是根据异常跳转到一个特定的页面,其实ASP.NET MVC中已经默认为我们实现了这个注解属性,这个注解属性就是HandleError类,我们只需要传入需要捕获的异常类型,以及对应的页面即可。
下面我们修改Home控制器的Index动作:
1 namespace MvcStudy.Controllers 2 { 3 public class HomeController : Controller 4 { 5 [HandleError(ExceptionType=typeof(NullReferenceException),View="error")] 6 public ActionResult Index() 7 { 8 throw new NullReferenceException(); 9 return View(); 10 } 11 } 12 }
然后我们重新编译,并访问页面,会发现页面没有按照我们的预想跳转到特定的页面而是直接显示了错误的详情。这是因为ASP.NET MVC默认情况下没有开启自定义异常处理,所以就会出现这个默认的页面,下面我们修改Web.Config,在其中添加如下的配置:
这时我们重新编译刷新页面,但是我们会发现页面会有内容,因为这个页面调用的是Views/Shared/Error.cshtml页面。
未完待续…