.NET应用架构设计—用户端的防腐层作用及设计

阅读目录:

  • 1.背景介绍
  • 2.SOA架构下的显示端架构腐化
  • 3.有效使用防腐层来隔离碎片服务导致显示端逻辑腐烂
  • 4.剥离服务调用的技术组件让其依赖接口
  • 5.将服务的DTO与显示端的ViewModel之间的转换放入防腐层
    • 5.1.转换逻辑过程化,直接写在防腐层的方法中
    • 5.2.转换逻辑对象化,建立起封装、重用结构,防止进一步腐化
  • 6.防腐层的两种依赖倒置设计方法
    • 6.1.事件驱动(防腐层监听显示逻辑事件)
    • 6.2.依赖注入接口
  • 7.总结

1.背景介绍

随着现在的企业应用架构都在向着SOA方向转变,目的就是将一个庞大的业务系统按照业务进行划分,不管从公司的管理上、产品的开发上,这一系列流程来看,都是正确的。SOA确实带来了解决现在大型企业级应用系统快速膨胀的解决办法。

但是本文要说的是,我们都将目光转向到了后端,也就是服务端,而将精力和时间都重点投在了后端服务的架构设计上,渐渐的忽视了显示端的架构设计。然而显示端的逻辑也越来越复杂,显示端轻薄的架构其实已经浮现出难以应付后端服务接口快速膨胀的危险,服务接口都是按照指数级增加,基本上每一个新的业务需求都是提供新的接口,这没有问题。按照服务的设计原则,服务接口就应该有着明确的作用,而不是按照代码的思维来考虑接口的设计。

但是由此带来的问题就是组合这些接口的显示端的结构是否和这种变化是一致的,是否做好了这种变化带来显示端逻辑复杂的准备。

根据我自己的亲身体会,我发现显示端的架构设计不被重视,这里的重视不是老板是否重视,而是我们开发人员没有重视,当然这里排除时间问题。我观察过很多用户接口项目架构,结构及其简单,没有封装、没有重用,看不到任何的设计原则。这样就会导致这些代码很难随着业务的快速推动由服务接口带来的冲击,这里还有一个最大的问题就是,作为程序员的我们是否有快速重构的意识,我很喜欢这条程序员职业素质。它可以让我们敏捷的、快速的跟上由业务的发展带来的项目结构的变化。

迭代重构对项目有着微妙的作用,重构不能够过早也不能够过迟,要刚好在需要的时候重构。对于重构我的经验就是,当你面对新功能写起来比较蹩脚的时候时,这是一个重构信号,此时应该是最优的重构时间。重构不是专门的去准备时间,而是穿插在你写代码的过程中,它是你编码的一部分。所以我觉得TDD被人接受的理由也在于此。

2.SOA架构下的显示端架构腐化

显示端的架构腐化我个人觉得有两个问题导致,第一个,原本显示端的结构在传统系统架构中可以工作的很好,但是现在的整体架构变了,所以需要及时作出调整。第二,显示端的架构未能及时的重构,未能将显示端结构进行进一步分离,将显示逻辑独立可测试。

这样随着SOA接口的不断增加,显示端直接将调用服务的方法嵌入到显示逻辑中,如,ASP.NET Mvc、ASP.NET Webapi的控制器中,包括两个层面之间的DTO转换。

按照DDD的上下文设计方法,在用户显示端也是可以有选择的创建面向显示的领域模型,此模型主要处理领域在即将到达服务端之后的前期处理。毕竟一个领域实体有着多个方面的职责,如果能在显示端建立起轻量级的领域模型,对显示逻辑的重构将大有好处,当然前提是你有着复杂的领域逻辑。(我之前的上一家公司(美国知名的电子商务平台),他们的显示端有着复杂的领域逻辑,就光一个显示端就复杂的让人吃惊,如果能在此基础上引入领域模型显示端上下文,将对复杂的逻辑处理很有好好处,当然这只是我未经验证的猜测而已,仅供参考。)

对显示端领域模型处理有兴趣的可以参考本人写的有关这方面的两篇文章:

.NET应用架构设计—面向查询的领域驱动设计实践(调整传统三层架构,外加维护型的业务开关)

.NET应用架构设计—面向查询服务的参数化查询设计(分解业务点,单独配置各自的数据查询契约)

原本干净的显示逻辑多了很多无关的服务调用细节,还有很多转换逻辑,判断逻辑,而这些东西原本不属于这个地方,让他们放在合适的地方对显示逻辑的重构、重用很有帮助。

如果不将其移出显示逻辑中,那么随着服务接口的不断增加和扩展,将直接导致你修改显示逻辑代码,如果你的显示逻辑代码是MVC、Webapi共用的逻辑,那么情况就更加复杂了,最后显示逻辑里面将被ViewModel与Service Dto之间的转换占领,你很难找到有价值的逻辑了。

3.有效使用防腐层来隔离碎片服务导致显示端逻辑腐烂

解决这些问题的方法就是引入防腐层,尽管防腐层的初衷是为了解决系统集成时的领域模型之间的转换,但是我觉得现在的系统架构和集成有着很多相似之处,我们可以适当的借鉴这些好的设计方法来解决相似的问题。

引入防腐层之后,将原本不该出现在显示逻辑中的代码全部搬到防腐层中来,在防腐层中建立起OO机制,让这些OO对象能够和显示逻辑一起搭配使用。

图1:

将用户层分层三个子层,UiLayer,Show Logic Layer,Anticorrosive Layer,最后一个是服务的接口组,所有的服务接口调用均需要从防腐层走。

我们需要将Show Logic Layer中的服务调用,类型转换代码迁移到Anticorrsoive Layer中,在这里可以对象化转换逻辑也可以不对象化,具体可以看下项目是否需要。如果业务确实比较复杂的时候,那么我们为了封装、重用就需要进行对象化。

4.剥离服务调用的技术组件让其依赖接口

首先要做的就是将逻辑代码中的服务对象重构成面向接口的,然后让其动态的依赖注入到逻辑类型中。在ASP.NETWEBAPI中,我们基本上将显示逻辑都写在这里面,我也将使用此方式来演示本章例子,但是如果你的MVC项目和WEBAPI项目共用显示逻辑就需要将其提出来形成独立的项目(Show Logic Layer)。

 1 using OrderManager.Port.Models;
 2 using System.Collections.Generic;
 3 using System.Web.Http;
 4
 5 namespace OrderManager.Port.Controllers
 6 {
 7     public class OrderController : ApiController
 8     {
 9         [HttpGet]
10         public OrderViewModel GetOrderById(long oId)
11         {
12             OrderService.Contract.OrderServiceClient client = new OrderService.Contract.OrderServiceClient();
13             var order = client.GetOrderByOid(oId);
14
15             if (order == null) return null;
16
17             return AutoMapper.Mapper.DynamicMap<OrderViewModel>(order);
18         }
19     }
20 } 

这是一段很简单的调用Order服务的代码,首先需要实例化一个服务契约中包含的客户端代理,然后通过代理调用远程服务方法GetOrderByOid(long oId)。执行一个简单的判断,最后输出OrderViewModel。

如果所有的逻辑都这么简单我想就不需要什么防腐层了,像这种类型的显示代码是极其简单的,我这里的目的不是为了显示多么的复杂的代码如何写,而是将服务调用调用的代码重构层接口,然后注入进OrderController实例中。目的就是为了能够在后续的迭代重构中对该控制器进行单元测试,这可能有点麻烦,但是为了长久的利益还是需要的。

 1 using OrderManager.Port.Component;
 2 using OrderManager.Port.Models;
 3 using System.Collections.Generic;
 4 using System.Web.Http;
 5
 6 namespace OrderManager.Port.Controllers
 7 {
 8     public class OrderController : ApiController
 9     {
10         private readonly IOrderServiceClient orderServiceClient;
11         public OrderController(IOrderServiceClient orderServiceClient)
12         {
13             this.orderServiceClient = orderServiceClient;
14         }
15
16         [HttpGet]
17         public OrderViewModel GetOrderById(long oId)
18         {
19             var order = orderServiceClient.GetOrderByOid(oId);
20
21             if (order == null) return null;
22
23             return AutoMapper.Mapper.DynamicMap<OrderViewModel>(order);
24         }
25     }
26 } 

为了能在运行时动态的注入到控制器中,你需要做一些基础工作,扩展MVC控制器的初始化代码。这样我们就可以对OrderController进行完整的单元测试。

刚才说了,如果显示逻辑都是这样的及其简单,那么一切都没有问题了,真实的显示逻辑非常的复杂而且多变,并不是所有的类型转换都能使用Automapper这一类动态映射工具解决,有些类型之间的转换还有逻辑在里面。GetOrderById(long oId)方法是为了演示此处的重构服务调用组件用的。

大部分情况下我们是需要组合多个服务调用的,将其多个结果组合起来返回给前端的,这里的OrderViewModel对象里面的Items属性类型OrderItem类型中包含了一个Product类型属性,在正常情况下我们只需要获取订单的条目就行了,但是有些时候确实需要将条目中具体的产品信息也要返回给前台进行部分信息的展现。

 1 using System.Collections.Generic;
 2
 3 namespace OrderManager.Port.Models
 4 {
 5     public class OrderViewModel
 6     {
 7         public long OId { get; set; }
 8
 9         public string OName { get; set; }
10
11         public string Address { get; set; }
12
13         public List<OrderItem> Items { get; set; }
14     }
15 } 

在OrderViewModel中的Items属性是一个List<OrderItem>集合,我们再看OrderItem属性。

 1 using System.Collections.Generic;
 2
 3 namespace OrderManager.Port.Models
 4 {
 5     public class OrderItem
 6     {
 7         public long OitemId { get; set; }
 8
 9         public long Pid { get; set; }
10
11         public float Price { get; set; }
12
13         public int Number { get; set; }
14
15         public Product Product { get; set; }
16     }
17 } 

它里面包含了一个Product实例,有些时候需要将该属性赋上值。

 1 namespace OrderManager.Port.Models
 2 {
 3     public class Product
 4     {
 5         public long Pid { get; set; }
 6
 7         public string PName { get; set; }
 8
 9         public long PGroup { get; set; }
10
11         public string Production { get; set; }
12     }
13 } 

产品类型中的一些信息主要是用来作为订单条目展现时能够更加的人性化一点,你只给一个产品ID,不能够让用户知道是哪个具体的商品。

我们接着看一个随着业务变化带来的代码急速膨胀的例子,该例子中我们需要根据OrderItem中的Pid获取Product完整信息。

 1 using OrderManager.Port.Component;
 2 using OrderManager.Port.Models;
 3 using System.Collections.Generic;
 4 using System.Web.Http;
 5 using System.Linq;
 6
 7 namespace OrderManager.Port.Controllers
 8 {
 9     public class OrderController : ApiController
10     {
11         private readonly IOrderServiceClient orderServiceClient;
12
13         private readonly IProductServiceClient productServiceClient;
14         public OrderController(IOrderServiceClient orderServiceClient, IProductServiceClient productServiceClient)
15         {
16             this.orderServiceClient = orderServiceClient;
17             this.productServiceClient = productServiceClient;
18         }
19
20         [HttpGet]
21         public OrderViewModel GetOrderById(long oId)
22         {
23             var order = orderServiceClient.GetOrderByOid(oId);
24
25             if (order == null && order.Items != null && order.Items.Count > 0) return null;
26
27             var result = new OrderViewModel()
28             {
29                 OId = order.OId,
30                 Address = order.Address,
31                 OName = order.OName,
32                 Items = new System.Collections.Generic.List<OrderItem>()
33             };
34
35             if (order.Items.Count == 1)
36             {
37                 var product = productServiceClient.GetProductByPid(order.Items[0].Pid);//调用单个获取商品接口
38                 if (product != null)
39                 {
40                     result.Items.Add(ConvertOrderItem(order.Items[0], product));
41                 }
42             }
43             else
44             {
45                 List<long> pids = (from item in order.Items select item.Pid).ToList();
46
47                 var products = productServiceClient.GetProductsByIds(pids);//调用批量获取商品接口
48                 if (products != null)
49                 {
50                     result.Items = ConvertOrderItems(products, order.Items);//批量转换OrderItem类型
51                 }
52
53             }
54
55             return result;
56         }
57
58         private static OrderItem ConvertOrderItem(OrderService.OrderItem orderItem, ProductService.Contract.Product product)
59         {
60             if (product == null) return null;
61
62             return new OrderItem()
63             {
64                 Number = orderItem.Number,
65                 OitemId = orderItem.OitemId,
66                 Pid = orderItem.Pid,
67                 Price = orderItem.Price,
68
69                 Product = new Product()
70                 {
71                     Pid = product.Pid,
72                     PName = product.PName,
73                     PGroup = product.PGroup,
74                     Production = product.Production
75                 }
76             };
77         }
78
79         private static List<OrderItem> ConvertOrderItems(List<ProductService.Contract.Product> products, List<OrderService.OrderItem> orderItems)
80         {
81             var result = new List<OrderItem>();
82
83             orderItems.ForEach(item =>
84             {
85                 var orderItem = ConvertOrderItem(item, products.Where(p => p.Pid == item.Pid).FirstOrDefault());
86                 if (orderItem != null)
87                     result.Add(orderItem);
88             });
89
90             return result;
91         }
92     }
93 } 

我的第一感觉就是,显示逻辑已经基本上都是类型转换代码,而且这里我没有添加任何一个有关显示的逻辑,在这样的情况下都让代码急速膨胀了,可想而知,如果再在这些代码中加入显示逻辑,我们基本上很难在后期维护这些显示逻辑,而这些显示逻辑才是这个类的真正职责。

由此带来的问题就是重要的逻辑淹没在这些转换代码中,所以我们急需一个能够容纳这些转换代码的位置,也就是防腐层,在防腐层中我们专门来处理这些转换逻辑,当然我这里的例子是比较简单的,只包含了查询,真正的防腐层是很复杂的,它里面要处理的东西不亚于其他层面的逻辑处理。我们这里仅仅是在转换一些DTO对象而不是复杂的DomainModel对象。

5.将服务的DTO与显示端的ViewModel之间的转换放入防腐层

我们需要一个防腐层来处理这些转换代码,包括对后端服务的调用逻辑,将这部分代码移入防腐对象中之后会对我们后面重构很有帮助。

 1 namespace OrderManager.Anticorrsive
 2 {
 3     using OrderManager.Port.Component;
 4     using OrderManager.Port.Models;
 5     using System.Collections.Generic;
 6     using System.Linq;
 7
 8     /// <summary>
 9     /// OrderViewModel 防腐对象
10     /// </summary>
11     public class OrderAnticorrsive : AnticorrsiveBase<OrderViewModel>, IOrderAnticorrsive
12     {
13         private readonly IOrderServiceClient orderServiceClient;
14
15         private readonly IProductServiceClient productServiceClient;
16
17         public OrderAnticorrsive(IOrderServiceClient orderServiceClient, IProductServiceClient productServiceClient)
18         {
19             this.orderServiceClient = orderServiceClient;
20             this.productServiceClient = productServiceClient;
21         }
22
23         public OrderViewModel GetOrderViewModel(long oId)
24         {
25             var order = orderServiceClient.GetOrderByOid(oId);
26
27             if (order == null && order.Items != null && order.Items.Count > 0) return null;
28
29             var result = new OrderViewModel()
30             {
31                 OId = order.OId,
32                 Address = order.Address,
33                 OName = order.OName,
34                 Items = new System.Collections.Generic.List<OrderItem>()
35             };
36
37             if (order.Items.Count == 1)
38             {
39                 var product = productServiceClient.GetProductByPid(order.Items[0].Pid);//调用单个获取商品接口
40                 if (product != null)
41                 {
42                     result.Items.Add(ConvertOrderItem(order.Items[0], product));
43                 }
44             }
45             else
46             {
47                 List<long> pids = (from item in order.Items select item.Pid).ToList();
48
49                 var products = productServiceClient.GetProductsByIds(pids);//调用批量获取商品接口
50                 if (products != null)
51                 {
52                     result.Items = ConvertOrderItems(products, order.Items);//批量转换OrderItem类型
53                 }
54
55             }
56
57             return result;
58         }
59
60         private static OrderItem ConvertOrderItem(OrderService.OrderItem orderItem, ProductService.Contract.Product product)
61         {
62             if (product == null) return null;
63
64             return new OrderItem()
65             {
66                 Number = orderItem.Number,
67                 OitemId = orderItem.OitemId,
68                 Pid = orderItem.Pid,
69                 Price = orderItem.Price,
70
71                 Product = new Product()
72                 {
73                     Pid = product.Pid,
74                     PName = product.PName,
75                     PGroup = product.PGroup,
76                     Production = product.Production
77                 }
78             };
79         }
80
81         private static List<OrderItem> ConvertOrderItems(List<ProductService.Contract.Product> products, List<OrderService.OrderItem> orderItems)
82         {
83             var result = new List<OrderItem>();
84
85             orderItems.ForEach(item =>
86             {
87                 var orderItem = ConvertOrderItem(item, products.Where(p => p.Pid == item.Pid).FirstOrDefault());
88                 if (orderItem != null)
89                     result.Add(orderItem);
90             });
91
92             return result;
93         }
94     }
95 }

如果你觉得有必要可以将IOrderServiceClient、IProductServiceClient 两个接口放入AnticorrsiveBase<OrderViewModel>基类中。

5.1.转换逻辑过程化,直接写在防腐层的方法中

对于防腐层的设计,其实如果你的转换代码不多,业务也比较简单时,我建议直接写成过程式的代码比较简单点。将一些可以重用的代码直接使用静态的扩展方法来使用也是比较简单方便的,最大问题就是不利于后期的持续重构,我们无法预知未来的业务变化,但是我们可以使用重构来解决。

5.2.转换逻辑对象化,建立起封装、重用结构,防止进一步腐化

相对应的,可以将转换代码进行对象化,形成防腐对象,每一个对象专门用来处理某一个业务点的数据获取和转换逻辑,如果你有数据发送逻辑那么将在防腐对象中大大获益,对象化后就可以直接订阅相关控制器的依赖注入事件,如果你是过程式的代码想完成动态的转换、发送、获取会比较不方便。

6.防腐层的两种依赖倒置设计方法

我们接着看一下如何让防腐对象无干扰的进行自动化的服务调用和发送,我们希望防腐对象完全透明的在执行着防腐的职责,并不希望它会给我们实现上带来多大的开销。

6.1.事件驱动(防腐层监听显示逻辑事件)

我们可以使用事件来实现观察者模式,让防腐层对象监听某个事件,当事件触发时,自动的处理某个动作,而不是要显示的手动调用。

1 namespace OrderManager.Anticorrsive
2 {
3     public interface IOrderAnticorrsive
4     {
5         void SetController(OrderController orderController);
6
7         OrderViewModel GetOrderViewModel(long oId);
8     }
9 }

Order防腐对象接口,里面包含了一个void SetController(OrderController orderController); 重要方法,该方法是用来让防腐对象自动注册事件用的。

 1 public class OrderController : ApiController
 2 {
 3     private IOrderAnticorrsive orderAnticorrsive;
 4
 5     public OrderController(IOrderAnticorrsive orderAnticorrsive)
 6     {
 7         this.orderAnticorrsive = orderAnticorrsive;
 8
 9         this.orderAnticorrsive.SetController(this);//设置控制器到防腐对象中
10     }
11
12     public event EventHandler<OrderViewModel> SubmitOrderEvent;
13
14     [HttpGet]
15     public void SubmitOrder(OrderViewModel order)
16     {
17         this.SubmitOrderEvent(this, order);
18     }
19 }

在控制器中,每当我们发生某个业务动作时只管触发事件即可,当然主要是以发送数据为主,查询可以直接调用对象的方法。因为防腐对象起到一个与后台服务集成的桥梁,当提交订单时可能需要同时调用很多个后台服务方法,用事件处理会比较方便。

 1     /// <summary>
 2     /// OrderViewModel 防腐对象
 3     /// </summary>
 4     public class OrderAnticorrsive : AnticorrsiveBase<OrderViewModel>, IOrderAnticorrsive
 5     {
 6         public void SetController(OrderController orderController)
 7         {
 8             orderController.SubmitOrderEvent += orderController_SubmitOrderEvent;
 9         }
10
11         private void orderController_SubmitOrderEvent(object sender, OrderViewModel e)
12         {
13             //提交订单的逻辑
14         }
15     }
16 }

6.2.依赖注入接口

依赖注入接口是完全为了将控制器与防腐对象之间隔离用的,上述代码中我是将接口定义在了防腐对象层中,那么也就是说控制器对象所在的项目需要引用防腐层,在处理事件和方法同时使用时会显得有点不伦不类的,既有接口又有方法,其实这就是一种平衡吧,越纯粹的东西越要付出一些代价。

如果我们定义纯粹的依赖注入接口让防腐对象去实现,那么在触发事件时就需要专门的方法来执行事件的触发,因为不在本类中的事件是没办法触发的。

7.总结

本篇文章是我对在UI层使用防腐层架构设计思想的一个简单总结,目的只有一个,提供一个参考,谢谢大家。

 

作者:王清培

出处:http://www.cnblogs.com/wangiqngpei557/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面

时间: 2024-10-24 03:47:12

.NET应用架构设计—用户端的防腐层作用及设计的相关文章

关系型数据库设计-用户表和订单表 怎么设计

问题描述 关系型数据库设计-用户表和订单表 怎么设计 如果一个电商用关系型数据库, 假设有一个用户表,有一个订单表,订单表中有一个用户ID 的字段, 那查询某个用户的所有订单时岂不是要遍历整个订单表?没有在互联网公司工作过,不知道是怎么设计的,求解答. 解决方案 数据库可以使用索引,对userid列做了索引,再查询的时候就不需要全表遍历.这和互联网公司没有关系,基本的数据库常识你都没学会.

网站设计用户体验:网站搜索框的设计

1.搜索框的意义       用户总是和你想的不一样,用户很多时候是不会按照你设定的导航走的,他们更多的时候是上来就直接输入关键字进行搜索.对于购物者而言,如果清楚的知道想要什么,他们会很明确的使用搜索框,这种情况下购物者显示出了明确的购物意图,因此搜索框能够把该意图转变为真实的购买行为.来自Fast Search的数据显示,30%的购物者进入电子商务网站后会立刻使用搜索框,超过30%的人通过导航没有找到需要的物品后转而使用搜索框.       如果一个网站没有足够合理的信息架构体系,那么搜索引

回归架构本真:从规划、思维到设计,构建坚不可摧的架构根基

   关于什么是架构,业界从来没有一个统一的定义.Martin Fowler在<企业应用架构模式>中也没有对其给出定义,只是提到能够统一的内容有两点: 最高层次的系统分解: 系统中不易改变的决定.   <软件架构设计>一书则将架构定义总结为组成派和决策派: 组成派:架构=组件+交互:软件系统的架构将系统描述为计算组件及组件之间的交互. 决策派:架构=重要决策集:软件架构是在一些重要方面所作出的决策的集合.   而架构的概念最初来源于建筑,因此,我想从建筑的角度去思考这个问题.Wik

手机横屏模式设计:用户关注的功能和流程

文章描述:换个角度看风景--手机产品设计之横屏模式. 做设计的过程,是思大于行的过程.一个有价值的设计,不是设计多么华丽.多么创新就有多么成功,那些优秀的设计师都会知道他为什么去做这样一款设计,他的设计本质价值是什么.互联网产品或者手机产品更是如此,设计师要知道你的产品要解决一个什么核心问题,然后再提出一个足够优雅的解决方案,这样才能解决用户的"痛点"问题,给予用户最贴心的设计. 在我刚开始做设计的时候,就很喜欢在一些细节之处增加复杂的动画效果,来彰显设计的标新立异,但是殊不知,每个动

交互设计用户研究:Web交互设计优化

文章描述:Web交互设计优化的简易check list. "优化已有产品的体验",这是用户体验相关岗位职责中常见的描述.我们的产品常常是在快速的迭代过程中不断完善的,就像孩子生下来需要养育才能长大一样,优化已有功能/产品,和设计新功能/产品同样重要,不可偏废. 但是,相比实现新功能,已有功能的优化总是显得没有那么紧迫而且很零散,导致了迭代优化的计划总是被归入"重要不紧急"甚至是"不重要不紧急"的象限,变成了东一棒子西一榔锤的买卖.我们可以通过可用

网易首页改版:提高用户体验的简约大气包容设计

新版网易首页,文章页,手机版网易首页(手机上网3g.163.com)10月12日同时上线.本次改版从整体到细节都有一个质的飞跃,新版继承了网易一贯简约.大气.包容的设计品质,达到以用户为中心提高用户体验.促进频道发展的改版设计目的. 相关文章:网页设计经验分享:再谈网易首页的改版 1,字号变大 为了适应网友日益增加的显示器分辨率需要,网易首页主体内容区的字号,由12号变大为14号. 2,分栏比例的改进: 新版网易首页栏宽比例调整,用户更容易从左栏获取完整信息,提高阅读感受.资讯板块更适合高效阅读

移动端界面中的版式设计原理(下)

  上篇教程:<超实用!移动端界面中的版式设计原理(上)> 4. 留白的艺术 不单单是文字和图片需要设计,留白也是构成页面排版必不可少的因素.所有的白都是"有目的的留白",带有明确的目的来控制页面的空间构成. 常见的手法有几种,第一通过留白来减轻页面带给用户的负担. 首屏对一个应用来说十分重要,因此一些比较复杂的应用首评都堆积了大量的入口.如果无节制的添加,页面中包含的内容太多时,会给人一种页面狭窄的感觉,给用户带来强烈的压迫感,所以元素太多有时候反而不是好事.留白能使页面

《UCD火花集2:有效的互联网产品设计 交互/信息设计 用户研究讨论》一17.3 过年回家有感:他们的互联网

17.3 过年回家有感:他们的互联网 UCD火花集2:有效的互联网产品设计 交互/信息设计 用户研究讨论 文/李杰 这次从北京这个可以算是中国互联网最发达的城市回到温州农村的老家,看到了爸爸妈妈,弟弟妹妹们是怎样用互联网的,让我这个做互联网产品的很受触动. 因为家里有不少亲戚在国外打工,QQ视频成了妈妈要开通宽带的唯一动力.以前都是我们过年回家再自己去开个假期宽带,这一次妈妈为了QQ视频主动提前去办理了宽带包年.对于从来没独立操作过电脑的爸妈们来说,学会开电脑,用鼠标双击企鹅图标,在打开的窗口中

管理-数据库中 我如何设计用户权限表

问题描述 数据库中 我如何设计用户权限表 有三个用户 学生,教师,管理员 如何设计权限,我有个表 字段名 有 编号,账号,密码 解决方案 如果你想简单,老师能做的事情管理员一定能做,学生能做的事情老师一定能做,是这种包含的关系,那么只需要一个表示权限的整数就可以了. 比如3=管理员 2=老师 1=学生 0=没有登录的用户 而你的不同操作,分别指定所需要的权限,然后和这个表里的数字比较,大,就不执行,小于等于就执行. 解决方案二: 以上的回答都很有道理,但是我还是感觉不妥,我认为,在数据库中建立了