楼主最近在研究大数据,当然要想真正的了解大数据或者是处理大数据仅凭我们自己会的这些三脚猫的功夫是不够的,遂本人就开始学习一些技术和方法,当然最重要的还是技术背后的思维。Hadoop,一头勇猛的大象,我看了几个星期的书,发现要想真正的去学习Hadoop就要去读Google的那三篇比较著名的论文。当然,在这里我才刚刚把今天的主角请了出来——Google。
最近不知怎么了,发现自己已经深深的被Google所吸引。可是学校不给力,要想连接Google的香港服务器要等上半年,当然这要归功于我们国家日益发达的防火墙和学校无限多的路由交换机。可是这还是阻挡不了我去Googel学习的欲望,举一个最简单的例子,楼主在百度上搜索自己的名字,发现有十几页是和自己相关的,然后又在Google上搜索,发现搜索的数量要比百度多上几倍。我在学校的实验室自己搭了两个服务器,挂的是JSP和PHP网站,但是都是木有备案的那种,百度的网络爬虫爬了一年多都木有发现我们的网站,而在Googel却非常容易的搜索到我们的网站。当然,在这里我不是想要从洋媚外,否则网友会骂死我的,我想说的是Google的技术已经深深的震撼了我(例如Google trends和Youtube)。
话说我发现好像有点跑题了,Google为什么是在用最简单的方式赚钱?一个最简单的例子是Google不卖搜索排名,给Google再多的钱它也不会将你的广告挂在它的首页上的,它在提供广告的时候会告诉你这是由它提供的广告,并且也不是你给钱它就会给你挂广告的(Google很像一个个性鲜明的人,只做自己认为对的事情)。与其形成鲜明对比的就是国内的搜索引擎了,他们费劲心思的将广告融入到搜索到的网页中,形成以假乱真的效果,他们变相的贩卖搜索排名,牺牲广大网民的时间来为自己赚钱。记着前几天去一家公司面试(当然只是去玩玩而已),他们想招SEO优化工程师,我说不用针对百度来做,要针对Google来做。技术问我为什么,我说你们应该向客户坦白一件事情,我们的工程师再厉害也敌不过百度工程师敲几下键盘,百度推广是个例子,你们应该向客户推荐一下购买百度推广,而Google相对而言是非常公平的(所谓相对而言是Google的算法也是有漏洞的,只是我们不知道而已)。
Google给我一种非常质朴的感觉,我也喜欢使用Google搜索,不仅仅是因为他搜索的东西够多,更大一方面在于他搜索的结果足够的公平,而其他搜索引擎已经伤了我们这些一穷二百的学生的心了。而一些不懂互联网的网民还在这种环境下受各种各样广告的干扰。其实我不是想说Google你太大无私了,为了公益事业而干搜索引擎,其实不然,Google的广告收入远高于百度的广告收入,那么为什么我说Google在用最简单的方式来赚钱呢?原因很简单,美国人(Google)真正的把消费者当成了上帝,而在我们中国人眼里,顾客就是金钱,我们只知道浏览器那端的网民是消费品的潜在用户,却不知中国网民也是懂得感情的,中国网民也是知道什么叫用户体验的。当一个广告频频出现,当广告的内容良莠不齐,当你把广告当成你要搜索的东西的时候,亲们,你们是一种什么感受?但是Google不会,他知道顾客的时间是宝贵的,为了让用户得到最佳体验,他不会随便的添加广告,更不会让广告误导到网民群众,他们在用技术赚钱(根据用户的历史搜索记录智能的提供广告等),他们让网民感受到了一种公平性,让网民感觉到这是一种公益平台(尽管他不是),当网民的心情愉悦的时候,他会从网民变成消费者的,或许正是因为如此Google的销售额才会居高不下。
顾客就是上帝是句老话,人要踏踏实实要朴实也是一句老话,而这两件事都是简单的事情,是一个企业很容易做到的(当然除了大部分的互联网公司)。Google始终考虑用户的体验,它将这简单的事情做到,所以赚到了钱。中国的互联网企业什么时候能觉悟呢?这是个问题。
【再次声明,本文不含对企业的个人看法,只是从程序员&用户的角度谈了谈感受】
一 Single Responsibility Principle——单一职责原则
核心思想: 一个类应该只有一个引起它变化的原因.
假设存在这样的设计. Rectangle类具有两个方法,一个方法是计算矩形的面积 , 另一个方法是把矩形绘制在屏幕上.
CaculateArea方法只会进行简单的数学运算,而Draw方法则调用GUI组件实现绘制矩形的功能. 显然,这个类就包含了两个不同的职责了. 那这样又会带来什么问题呢? 考虑这样一个场景:现在有一个几何学应用程序调用了这一个类,已便实现计算面积的功能,在这个程序中不需要用到绘制矩形的功能. 问题一:部署几何应用程序需要把GUI组件一同部署,而且这个组件根本没有使用到.问题二:对Rectangle类的改变,比如Draw方法改用另外一套GUI组件,必须对几何应用程序进行一次重新部署.
可见,一个类如果承担的职责过多,就等于把职责耦合在一起了,容易导致脆弱的设计,带来额外的麻烦. 在实际开发中, 业务规则的处理和数据持久化一般是不同时存在同一个类中的,业务规则往往会频繁地变化,而持久化的方式却不会经常性地变化.如果这两个职责混合在同一个类中,业务规则频繁变化导致类的修改,只调用持久化方法的类也必须跟着重新编译,部署的次数常常会超过我们希望的次数. 对业务规则和持久化任务的职责分离就是遵循单一职责原则的体现.
对上述Recangle类可进行这样的修改:
二 Open Closed Principle——开放封闭原则
核心思想:对扩展开放,对修改封闭.
"需求总是变化的." 拥抱变化似乎就是软件开发的真理之一. 经常会有这样令人沮丧的情景出现:新的需求来了,对不起,我的代码设计必须大幅度推倒重来. 设计的坏味道让我们深受其害,那么怎样的设计才能面对需求的改变却可以保持相对稳定呢?
针对这样的问题,OCP给了我们如下的建议:在发生变化的时候,不要修改类的源代码,要通过添加新代码来增强现有类的行为.
对扩展开放,对修改封闭,这两个特征似乎就是相互矛盾的. 通常观念来讲,扩展不就是修改源代码吗?怎么可能在不改动源代码的情况下去更改它的行为呢?
答案就是抽象(Interface 和 抽象基类).实现OCP的核心思想就是对抽象编程. 让类依赖于固定的抽象,对修改就是封闭的; 而通过面向对象的继承和多态机制,通过覆写方法改变固有行为,实现新的扩展方法,对于扩展就是开放的.
来看一个例子. 实现一个能够根据客户端的调用要求绘制圆形和长方形的应用程序. 初始设计如下:
public class Draw { public void DrawRectangle() { //绘制长方形 } public void DrawCircle() { //绘制圆形 } } public enum Sharp { /// <summary> /// 长方形 /// </summary> Rectangle , /// <summary> /// 圆形 /// </summary> Circle , } public class DrawProcess { private Draw _draw = new Draw(); public void Draw(Sharp sharp) { switch (sharp) { case Sharp.Rectangle: _draw.DrawRectangle(); break; case Sharp.Circle: _draw.DrawCircle(); break; default: throw new Exception("调用出错!"); } } } //调用代码 DrawProcess draw = new DrawProcess(); draw.Draw(Sharp.Circle);
现在的代码可以正确地运行. 一切似乎都趋近于理想. 然而,需求的变更总是让人防不胜防. 现在程序要求要实现可以绘制正方形. 在原本的代码设计下,必须做如下的改动.
//在Draw类中添加 public void DrawSquare() { //绘制正方形 } //在枚举Sharp中添加 /// <summary> /// 正方形 /// </summary> Square , //在DrawProcess类的switch判断中添加 case Sharp.Square: _draw.DrawSquare(); break;
需求的改动产生了一系列相关模块的改动,设计的坏味道悠然而生. 现在运用OCP, 来看一下如何对代码进行一次重构.
/// <summary> /// 绘制接口 /// </summary> public interface IDraw { void Draw(); } public class Circle:IDraw { public void Draw() { //绘制圆形 } } public class Rectangle:IDraw { public void Draw() { //绘制长方形 } } public class DrawProcess { private IDraw _draw; public IDraw Draw { set { _draw = value; } } private DrawProcess() { } public DrawProcess(IDraw draw) { _draw = draw; } public void DrawSharp() { _draw.Draw(); } } //调用代码 IDraw circle = new Circle(); DrawProcess draw = new DrawProcess(circle); draw.DrawSharp();
假如现在需要有绘制正方形的功能,则只需添加一个类Square 即可.
public class Square:IDraw { public void Draw() { //绘制正方形 } }
只需新增加一个类且对其他的任何模块完全没有影响,OCP出色地完成了任务.
如果一开始就采用第二种代码设计,在需求的暴雨来临时,你会欣喜地发现你已经到家了, 躲过了被淋一身湿的悲剧. 所以在一开始设计的时候,就要时刻地思考,根据对应用领域的理解来判断最有可能变化的种类,然后构造抽象来隔离那些变化.经验在这个时候会显得非常宝贵,可能会帮上你的大忙.
OCP很美好,然而绝对的对修改关闭是不可能的,都会有无法对之封闭的变化. 同时必须清楚认识到遵循OCP的代价也是昂贵的,创建适当的抽象是要花费开发时间和精力的. 如果滥用抽象的话,无疑引入了更大的复杂性,增加维护难度.
三 Liskov Subsitution Principle——里氏替换原则
核心思想: 子类必须能够替换掉它们的父类型.
考虑如下情况:
public class ProgrammerToy { private int _state; public int State { get { return _state; } } public virtual void SetState(int state) { _state = state; } } public class CustomProgrammerToy:ProgrammerToy { public override void SetState(int state) { //派生类缺乏完整访问能力,即无法访问父类的私有成员_state //因此该类型也许不能完成其父类型能够满足的契约 } } //控制台应用程序代码 class Program { static void Main(string[] args) { ProgrammerToy toy = new CustomProgrammerToy(); toy.SetState(5); Console.Write(toy.State.ToString()); } }
从语法的角度来看, 代码没有任何问题. 不过从行为的角度来看 , 二者却存在不同. 在使用CustomProgrammerToy替换父类的时候, 输出的是0而不是5, 与既定的目标相差千里. 所以不是所有的子类都能安全地替换其父类使用.
前面谈到的开发封闭原则和里氏替换原则存在着密切的关系. 实现OCP的核心是对抽象编程, 由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展, 所以违反了里氏替换原则也必定违反了开放封闭原则.
庆幸的是, 里氏替换原则还是有规律可循的.父类尽可能使用接口或抽象类来实现,同时必须从客户的角度理解,按照客户程序的预期来保证子类和父类在行为上的相容.
四 InterFace Segregation Principle——接口隔离原则
核心思想:使用多个小的专门的接口,而不要使用一个大的总接口.
直接来看一个例子: 假设有一个使用电脑的接口
程序员类实现接口IComputerUse, 玩游戏,编程,看电影, 多好的事情.
现在有一个游戏发烧友,他也要使用电脑, 为了重用代码 , 实现OCP, 他也实现接口IComputerUse
看出什么问题了吗? GamePlayer PlayGame无可厚非,WatchMovies小消遣, 但要编程干什么?
这就是胖接口带来的弊端,会导致实现的类必须完全实现接口的所有方法, 而有些方法对客户来说是无任何用处的,在设计上这是一种"浪费". 同时,如果对胖接口进行修改, 比如程序员要使用电脑配置为服务器, 在IComputerUse上添加Server方法, 同样GamePlayer也要修改(这种修改对GamePlayer是毫无作用的),是不是就引入了额外的麻烦?
所以应该避免出现胖接口,要使接口实现高内聚(高内聚是指一个模块中各个部分都是为完成一项具体功能而协同工作,紧密联系,不可分割). 当出现了胖接口,就要考虑重构.优先推荐的方法是使用多重继承分离,即实现小接口.
将IComputerUse拆分为IComputerBeFun和IComputerProgram, Progammer类则同时实现IComputerBeFun和IComputerProgram接口,现在就各取所需了.
与OCP类似, 接口也并非拆分地越小越好, 因为太多的接口会影响程序的可读性和维护性,带来难以琢磨的麻烦. 所以设计接口的时刻要着重考虑高内聚性, 如果接口中的方法都归属于同一个逻辑划分而协同工作,那么这个接口就不应该再拆分.
五 Dependency Inversion Principle——依赖倒置原则
核心思想: 高层模块不应该依赖底层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
当一个类A存在指向另一个具体类B的引用的时候,类A就依赖于类B了。如:
/// <summary> /// 商品类 /// </summary> public class Product { public int Id { get; set; } } /// <summary> /// 商品持久化类 /// </summary> public class ProductRepository { public IList<Product> FindAll() { //假设从SQL Server数据库中获取数据 return null; } } /// <summary> /// 商品服务类 /// </summary> public class ProductService { private ProductRepository _productRepository; public IList<Product> GetProducts() { _productRepository = new ProductRepository(); return _productRepository.FindAll(); } }
(在前面单一职责原则中有提到,业务逻辑处理和对象持久化分属两个职责,所以应该拆分为两个类。)高层模块ProductService类中引用了底层模块具体类ProductRepository,所以ProductService类就直接依赖于ProductRepository了。那么这样的依赖会带来什么问题呢?
"需求总是那么不期而至"。原本ProductRepository是从SQL Server数据库中读存数据,现在要求从MySQL数据库中读存数据。由于高层模块依赖于底层模块,现在底层模块ProductRepository发生了更改,高层模块ProductService也需要跟着一起修改,回顾之前谈到的设计原则,这是不是就违反了OCP呢?OCP的核心思想是对抽象编程,DIP的思想是依赖于抽象,这也让我们更清楚地认识到,面向对象设计的时候,要综合所有的设计原则考虑。DIP给出了解决方案:在依赖之间定义一个接口,使得高层模块调用接口,而底层模块实现接口,以此来控制耦合关系。(在上面OCP的例子中,也是使用了这一个方法。)所以可以对代码做如下的重构:
/// <summary> /// 商品持久化接口 /// </summary> public interface IProductRepository { List<Product> FindAll(); } /// <summary> /// 商品持久化类 /// </summary> public class ProductRepository:IProductRepository { public IList<Product> FindAll() { //假设从SQL Server数据库中获取数据 return null; } } /// <summary> /// 商品服务类 /// </summary> public class ProductService { private IProductRepository _productRepository; private ProductService() { } //使用构造函数依赖注入 public ProductService(IProductRepository productRepository) { _productRepository = productRepository; } public IList<Product> GetProducts() { return _productRepository.FindAll(); } }
现在已对变化进行了抽象隔离,再根据OCP,我相信实现从MySQL数据库中读存数据的需求已经可以被轻松地解决掉了。
参考书籍:<敏捷软件开发(C#版)> <你必须知道的.NET> <大话设计模式> <Microsoft.NET 企业级应用架构设计>