由于最近两个月工作比较清闲,个人也比较“上进”,利用工作空余时间,也继续学习了一下,某天突然想起struts2和struts1的区别的时候,发现为什么struts1要用servlet,而struts2要用filter呢?一时又发现,servlet和filter有什么区别呢?于是看了看web.xml,一时又发现,咦,servlet、filter、listener?还有个interceptor?对于这几个概念,本应是初学者就掌握的东东了,可惜本人基础学的不好,只能是现在补课。于是就有了这篇博客。

慢慢来吧,需要补课的地方还有很多很多呀。初学的时候都不知道他们存在呢。呵呵。

下面从几个方面阐述一下题目中四个概念的区别与联系:

1、概念

2、生命周期

3、职责

4、执行过程

一、概念:

1、servlet:servlet是一种运行服务器端的java应用程序,具有独立于平台和协议的特性,并且可以动态的生成web页面,它工作在客户端请求与服务器响应的中间层。

2、filter:filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter不像Servlet,它不能产生一个请求或者响应,它只是修改对某一资源的请求,或者修改从某一的响应。

3、listener:监听器,从字面上可以看出listener主要用来监听只用。通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应。通俗的语言说就是在application,session,request三个对象创建消亡或者往其中添加修改删除属性时自动执行代码的功能组件。

4、interceptor:是在面向切面编程的,就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法,比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。

5、servlet、filter、listener是配置到web.xml中,interceptor不配置到web.xml中,struts的拦截器配置到struts.xml中。spring的拦截器配置到spring.xml中。

二、生命周期:

1、servlet:servlet的生命周期始于它被装入web服务器的内存时,并在web服务器终止或重新装入servlet时结束。servlet一旦被装入web服务器,一般不会从web服务器内存中删除,直至web服务器关闭或重新结束。
(1)、装入:启动服务器时加载Servlet的实例;
(2)、初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成;
(3)、调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法;
(4)、销毁:停止服务器时调用destroy()方法,销毁实例。

2、filter:(一定要实现javax.servlet包的Filter接口的三个方法init()、doFilter()、destroy(),空实现也行)
(1)、启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
(2)、每一次请求时都只调用方法doFilter()进行处理;
(3)、停止服务器时调用destroy()方法,销毁实例。

3、listener:类似于servlet和filter

web.xml 的加载顺序是:context- param -> listener -> filter -> servlet

4、interceptor:以struts的拦截器为例,加载了struts.xml以后,初始化相应拦截器。当action请求来时调用intercept方法,服务器停止销毁interceptor。

三、职责

1、servlet:

创建并返回一个包含基于客户请求性质的动态内容的完整的html页面;
创建可嵌入到现有的html页面中的一部分html页面(html片段);
读取客户端发来的隐藏数据;
读取客户端发来的显示数据;
与其他服务器资源(包括数据库和java的应用程序)进行通信;
通过状态代码和响应头向客户端发送隐藏数据。

2、filter:

filter能够在一个请求到达servlet之前预处理用户请求,也可以在离开servlet时处理http响应:
在执行servlet之前,首先执行filter程序,并为之做一些预处理工作;
根据程序需要修改请求和响应;
在servlet被调用之后截获servlet的执行

3、listener:职责如概念。

servlet2.4规范中提供了8个listener接口,可以将其分为三类,分别如下:
第一类:与servletContext有关的listner接口。包括:ServletContextListener、ServletContextAttributeListener
第二类:与HttpSession有关的Listner接口。包括:HttpSessionListner、HttpSessionAttributeListener、HttpSessionBindingListener、 HttpSessionActivationListener;
第三类:与ServletRequest有关的Listener接口,包括:ServletRequestListner、ServletRequestAttributeListener

4、interceptor:与过滤器十分相似,通过层层拦截,处理用户的请求和响应。

 

备注:web.xml 的加载顺序是:context-param -> listener -> filter -> servlet 。了解了这几个概念的区别以后,不难理论这个加载顺序了。

 

四、几个区别:

1,servlet 流程是短的,url传来之后,就对其进行处理,之后返回或转向到某一自己指定的页面。它主要用来在 业务处理之前进行控制.
2,filter 流程是线性的, url传来之后,检查之后,可保持原来的流程继续向下执行,被下一个filter, servlet接收等,而servlet 处理之后,不会继续向下传递。filter功能可用来保持流程继续按照原来的方式进行下去,或者主导流程,而servlet的功能主要用来主导流程。
filter可用来进行字符编码的过滤,检测用户是否登陆的过滤,禁止页面缓存等
3, servlet,filter都是针对url之类的,而listener是针对对象的操作的,如session的创建,session.setAttribute的发生,在这样的事件发生时做一些事情。
可用来进行:Spring整合Struts,为Struts的action注入属性,web应用定时任务的实现,在线人数的统计等

4,interceptor 拦截器,类似于filter,不过在struts.xml中配置,不是在web.xml,并且不是针对URL的,而是针对action,当页面提交action时,进行过滤操作,相当于struts1.x提供的plug-in机制,可以看作,前者是struts1.x自带的filter,而interceptor 是struts2 提供的filter.
与filter不同点:(1)不在web.xml中配置,而是在struts.xml中完成配置,与action在一起
( 2 ) 可由action自己指定用哪个interceptor 来在接收之前做事

5,struts2中的过滤器和拦截器的区别与联系:

(1)、拦截器是基于java反射机制的,而过滤器是基于函数回调的。
(2)、过滤器依赖与servlet容器,而拦截器不依赖与servlet容器。
(3)、拦截器只能对Action请求起作用,而过滤器则可以对几乎所有请求起作用。
(4)、拦截器可以访问Action上下文、值栈里的对象,而过滤器不能。
(5)、在Action的生命周期中,拦截器可以多次调用,而过滤器只能在容器初始化时被调用一次。

五、执行流程图:

1、servlet:

2、filter:

3、listener:

4、interceptor:

from: http://drops.wooyun.org/tips/1166

0x00 背景


在统计了Alexa top 100万网站的header安全分析之后(2012年11月 - 2013年3月 - 2013年11月),我们发现其实如何正确的设置一个header并不是一件容易的事情。尽管有数不胜数的网站会使用大量有关安全方面的header,但并没有一个像样的平台能够为开发者们提供必要的信息,以辨别那些常见的错误设置。或者说,即使这些安全方面的header设置正确了,也没有一个平台能够为开发者提供一个系统的测试方法,用来测试正确与否。这些header如果设置错误了不仅会产生安全的假象,甚至会对网站的安全产生威胁。veracode认为安全性header是网络防护中非常重要的一环,并且他希望让开发者们能够简捷、正确地设置站点。如果您对某一header或设置有任何疑问,我们有极好的资源能够追踪到浏览器支持情况。

 

0x01 细节


1. X-XSS-Protection

目的

这个header主要是用来防止浏览器中的反射性xss。现在,只有IE,chrome和safari(webkit)支持这个header。

正确的设置

0 – 关闭对浏览器的xss防护
1 – 开启xss防护
1; mode=block – 开启xss防护并通知浏览器阻止而不是过滤用户注入的脚本。
1; report=http://site.com/report – 这个只有chrome和webkit内核的浏览器支持,这种模式告诉浏览器当发现疑似xss攻击的时候就将这部分数据post到指定地址。

通常不正确的设置

0; mode=block; – 记住当配置为0的时候,即使加了mode=block选项也是没有效果的。需要指出的是,chrome在发现这种错误的配置后还是会开启xss防护。
1 mode=block; – 数字和选项之间必须是用分号分割,逗号和空格都是错误的。但是这种错误配置情况下,IE和chrome还是默认会清洗xss攻击,但是不会阻拦。

如何检测

如果过滤器检测或阻拦了一个反射性xss以后,IE会弹出一个对话框。当设置为1时,chrome会隐藏对反射性xss的输出。如果是设置为 1; mode=block ,那么chrome会直接将user-agent置为一个空值:, URL 这种形式。

参考文献

Post from Microsoft on the X-XSS-Protection Header
Chromium X-XSS-Protection Header Parsing Source
Discussion of report format in WebKit bugzilla

2. X-Content-Type-Options

目的

这个header主要用来防止在IE9、chrome和safari中的MIME类型混淆攻击。firefox目前对此还存在争议。通常浏览器可以通过嗅探内容本身的方法来决定它是什么类型,而不是看响应中的content-type值。通过设置 X-Content-Type-Options:如果content-type和期望的类型匹配,则不需要嗅探,只能从外部加载确定类型的资源。举个例子,如果加载了一个样式表,那么资源的MIME类型只能是text/css,对于IE中的脚本资源,以下的内容类型是有效的:

application/ecmascript
application/javascript
application/x-javascript
text/ecmascript
text/javascript
text/jscript
text/x-javascript
text/vbs
text/vbscript

对于chrome,则支持下面的MIME 类型:

text/javascript
text/ecmascript
application/javascript
application/ecmascript
application/x-javascript
text/javascript1.1
text/javascript1.2
text/javascript1.3
text/jscript
text/live script

正确的设置

nosniff – 这个是唯一正确的设置,必须这样。

通常不正确的设置

‘nosniff’ – 引号是不允许的
: nosniff – 冒号也是错误的

如何检测

在IE和chrome中打开开发者工具,在控制台中观察配置了nosniff和没有配置nosniff的输出有啥区别。

参考文献

Microsoft Post on Reducing MIME type security risks
Chromium Source for parsing nosniff from response
Chromium Source list of JS MIME types
MIME Sniffing Living Standard

3. X-Frame-Options

目的

这个header主要用来配置哪些网站可以通过frame来加载资源。它主要是用来防止UI redressing 补偿样式攻击。IE8和firefox 18以后的版本都开始支持ALLOW-FROM。chrome和safari都不支持ALLOW-FROM,但是WebKit已经在研究这个了。

正确的设置

DENY – 禁止所有的资源(本地或远程)试图通过frame来加载其他也支持X-Frame-Options 的资源。
SAMEORIGIN – 只允许遵守同源策略的资源(和站点同源)通过frame加载那些受保护的资源。
ALLOW-FROM http://www.example.com – 允许指定的资源(必须带上协议http或者https)通过frame来加载受保护的资源。这个配置只在IE和firefox下面有效。其他浏览器则默认允许任何源的资源(在X-Frame-Options没设置的情况下)。

通常不正确的设置

ALLOW FROM http://example.com – ALLOW和FROM 之间只能通过连字符来连接,空格是错误的。
ALLOW-FROM example.com – ALLOW-FROM选项后面必须跟上一个URI而且要有明确的协议(http或者https)

如何检测

可以通过访问test cases 来查看各种各样的选项 和浏览器对这些frame中的资源的响应。

参考文献

X-Frame-Options RFC
Combating ClickJacking With X-Frame-Options

4. Strict-Transport-Security

目的

Strict Transport Security (STS) 是用来配置浏览器和服务器之间安全的通信。它主要是用来防止中间人攻击,因为它强制所有的通信都走TLS。目前IE还不支持 STS头。需要注意的是,在普通的http请求中配置STS是没有作用的,因为攻击者很容易就能更改这些值。为了防止这样的现象发生,很多浏览器内置了一个配置了STS的站点list。

正确的设置
注意下面的值必须在https中才有效,如果是在http中配置会没有效果。

max-age=31536000 – 告诉浏览器将域名缓存到STS list里面,时间是一年。
max-age=31536000; includeSubDomains – 告诉浏览器将域名缓存到STS list里面并且包含所有的子域名,时间是一年。
max-age=0 – 告诉浏览器移除在STS缓存里的域名,或者不保存此域名。

通常不正确的设置

直接将includeSubDomains设置为 https://www.example.com ,但是用户依然可以通过 http://example.com 来访问此站点。如果example.com 并没有跳转到 https://example.com 并设置 STS header,那么访问 http://www.example.com 就会直接被浏览器重定向到 https://www.example.com
max-age=60 – 这个只设置域名保存时间为60秒。这个时间太短了,可能并不能很好的保护用户,可以尝试先通过http来访问站点,这样可以缩短传输时间。
max-age=31536000 includeSubDomains – max-age 和 includeSubDomains 直接必须用分号分割。这种情况下,即使max-age的值设置的没有问题,chrome也不会将此站点保存到STS缓存中。
max-age=31536000, includeSubDomains – 同上面情况一样。
max-age=0 – 尽管这样在技术上是没有问题的,但是很多站点可能在处理起来会出差错,因为0可能意味着永远不过期。

如何检测

判断一个主机是否在你的STS缓存中,chrome可以通过访问chrome://net-internals/#hsts,首先,通过域名请求选项来确认此域名是否在你的STS缓存中。然后,通过https访问这个网站,尝试再次请求返回的STS头,来决定是否添加正确。

参考文献

Strict Transport Security RFC6797
Wikipedia page on Strict Transport Security (with examples)

5. Public-Key-Pins (起草中)

目的

有关这个header的详细描述还在起草中,但是它已经有了很清晰的安全作用,所以我还是把它放在了这个list里面。Public-Key-Pins (PKP)的目的主要是允许网站经营者提供一个哈希过的公共密钥存储在用户的浏览器缓存里。跟Strict-Transport-Security功能相似的是,它能保护用户免遭中间人攻击。这个header可能包含多层的哈希运算,比如pin-sha256=base64(sha256(SPKI)),具体是先将 X.509 证书下的Subject Public Key Info (SPKI) 做sha256哈希运算,然后再做base64编码。然而,这些规定有可能更改,例如有人指出,在引号中封装哈希是无效的,而且在33版本的chrome中也不会保存pkp的哈希到缓存中。

这个header和 STS的作用很像,因为它规定了最大子域名的数量。此外,pkp还提供了一个Public-Key-Pins-Report-Only 头用来报告异常,但是不会强制阻塞证书信息。当然,这些chrome都是不支持的。

正确的设置

max-age=3000; pin-sha256=”d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=”; – 规定此站点有3000秒的时间来对x.509证书项目中的公共密钥信息(引号里面的内容)做sha256哈希运算再做base64编码。
max-age=3000; pin-sha256=”d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=”; report-uri=”http://example.com/pkp-report” – 同上面一样,区别是可以报告异常。

通常不正确的设置

max-age=3000; pin-sha256=d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=; – Not encapsulating the hash value in quotes leads to Chrome 33 not adding the keys to the PKP cache. This mistake was observed in all but one of the four sites that returned this or the report-only header response.没有添加引号,这样的话chrome不会将这个key添加到PKP缓存中。我们的调查中发现有四分之一的网站存在此问题。

如何检测

如果站点成功的将pkp哈希存入了客户端缓存,那么使用和Strict-Transport-Security (STS)相同的方法查看pubkey_hashes 信息就可以了。

参考文献

Public-Key-Pins draft specification
Chrome issue tracking the implementation of Public-Key-Pins
Chromium source code for processing Public-Key-Pins header

6. Access-Control-Allow-Origin

目的

Access-Control-Allow-Origin是从Cross Origin Resource Sharing (CORS)中分离出来的。这个header是决定哪些网站可以访问资源,通过定义一个通配符来决定是单一的网站还是所有网站可以访问我们的资源。需要注意的是,如果定义了通配符,那么 Access-Control-Allow-Credentials选项就无效了,而且user-agent的cookies不会在请求里发送。

正确的设置

 – 通配符允许任何远程资源来访问含有Access-Control-Allow-Origin 的内容。
http://www.example.com – 只允许特定站点才能访问(http://[host], 或者 https://[host])

*通常不正确的设置

http://example.com, http://web2.example.com – 多个站点是不支持的,只能配置一个站点。
.example.com – 只允许单一的站点
http://
.example.com – 同上面一样

如何检测

很容易就能确定这个header是否被设置的正确,因为如果设置错误的话,CORS请求就会接收不到数据。

参考文献

W3C CORS specification
Content-Security-Policy 1.0

目的

csp是一些指令的集合,可以用来限制页面加载各种各样的资源。目前,IE浏览器只支持CSP的一部分,而且仅支持X-Content-Security-Policy header。 相比较而言,Chrome和Firefox则支持CSP的1.0版本,csp的1.1版本则还在开发中。通过恰当的配置csp,可以防止站点遭受很多类型的攻击,譬如xss和UI补偿等相关问题。

csp总共有10种配置形式,每一种都可以用来限制站点何时加载和加载何种类型的资源。他们分别是:

default-src:这种形式默认设置为 script-src, object-src, style-src, img-src, media-src, frame-src, font-src和connect-src.如果上述设置一个都没有的话,user-agent就会被用来作为default-src的值。

script-src:这种形式也有两个附加的设置。

1、unsafe-inline:允许资源执行脚本。举个例子,on事件的值会被编码到html代码中,或者脚本元素的text的内容会被混入到受保护的资源中。
2、unsafe-eval:允许资源动态的执行函数,比如eval,setTimeout, setInterval, new等函数。

object-src – 决定从哪里加载和执行插件。
style-src – 决定从哪里加载css和样式标记。
img-src – 决定从哪里加载图片。
media-src – 决定从哪里加载视频和音频资源。
frame-src – 决定哪里的frames 可以被嵌入。
font-src – 决定从哪里加载字体。
connect-src – 限制在 XMLHttpRequest, WebSocket 和 EventSource 中可以使用哪些类型的资源。
sandbox – 这是一个可选形式,它决定了沙盒的策略,如何将内容嵌入到沙盒中以保证安全。

当这个策略被特定的url违反了,我们也可以用报告地址直接发送报告。这样做有利于debug和当攻击发生时通知我们。

此外, 我们可以定义Content-Security-Policy-Report-Only 的 header不强制遵守csp,但是会发送潜在的威胁到一个报告地址。它遵守和csp一样的语法和规则。

正确的设置

View cspplayground.com compliant examples

通常不正确的设置:

View cspplayground.com violation examples

如何检测

在chrome和firefox中打开开发者工具或者firebug,在控制台查看是否有潜在的威胁。

参考文献

Content Security Policy 1.0 Specification
Content Security Policy 1.1 Working Draft Specification
cspplayground.com’s excellent collection of resources for CSP
Sublime Text 2 Plugin for validating HTML/JS while you code your site

关于Alexa排名top 100万网站的安全header的研究

离站提示JS工具:Ouibounce,当离开网站时给出一个提醒,从jobbole看到的.

tip:

和bootstrap的model 结合时要在参数的callback中 手动 用$('#share').modal();触发,官网没说,这个要注意.

demo如下:

[repo owner=”carlsednaoui” name=”ouibounce”]

tabindentjs很多程序员都会习惯性的在Textarea中按Tab键进行缩进,结果是——焦点移动到下一个控件去了。

tabIndent.js就是一个专门用来解决这个问题的小巧脚本,只需要在页面中引用它,并调用

tabIndent.renderAll();

即可处理class为tabindent的Textarea。

[repo owner=”julianlam” name=”tabIndent.js”]

Heatmap.js用来生成基于用户自定义数据上的web 热图,内嵌html5 画布元素。

可根据以下数据来源绘制热图:

  • 静态数据
  • 鼠标移动
  • 鼠标点击
    支持浏览器:Firefox 3.6+, Chrome 10, Safari 5, Opera 11 and IE 9+.

heatmapjs

[repo owner=”pa7” name=”heatmap.js”]

【光影下的壮丽风景】Eric Hinesp拍摄照片基本都是自然风光,并且蕴含了壮丽的风云场景。在他的照片里大部分使用长曝光的技术手段,让云霞、水面或星空呈现一种柔软,恬淡的弱光氛围。WeiboImg20140822_234458_9 WeiboImg20140822_234458_1 WeiboImg20140822_234458_2 WeiboImg20140822_234458_3 WeiboImg20140822_234458_4 WeiboImg20140822_234458_5 WeiboImg20140822_234458_6 WeiboImg20140822_234458_7 WeiboImg20140822_234458_8

作者是阿里巴巴安全工程师@卷成团变成个球的CasperKid君 。文章是CK在2011年编写的,在当下仍具有非常重要参考价值。很多web 站点存在上传验证方式不严格的安全缺陷,是web 渗透中关键的突破口 ,站长小伙伴要注意哦!

0x00 上传检测流程概述
0x01 客户端检测绕过(javascript 检测)
0x02 服务端检测绕过(MIME 类型检测)
0x03 服务端检测绕过(目录路径检测)
0x04 服务端检测绕过(文件扩展名检测)

  • 黑名单检测
  • 白名单检测
  • .htaccess 文件攻击
    0x05 服务端检测绕过(文件内容检测)
  • 文件幻数检测
  • 文件相关信息检测
  • 文件加载检测
    0x06 解析攻击
  • 网络渗透的本质
  • 直接解析
  • 本地文件包含解析
  • .htaccess 解析
  • web 应用程序解析漏洞及其原理
    0x07 上传攻击框架
  • 轻量级检测绕过攻击
  • 路径/扩展名检测绕过攻击
  • 文件内容性检测绕过攻击
  • 上传攻击框架
  • 结语

下载: Upload_Attack_Framework.pdf

from : http://zingson.com/72.html

弊端是,没有人还记得面向对象原本要解决的问题是什么。

1、面向对象原本要解决什么?(或者说有什么优良特性)

似乎很简单,但实际又很不简单:面向对象三要素封装、继承、多态。(警告:事实上,从业界如此总结出这面向对象三要素的一刹那开始,就已经开始犯错了!)。

封装:封装的意义,在于明确标识出会访问某个数据结构(用面向对象的术语来说就是 类成员变量)的所有接口。

有了封装,就可以明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者;而外部调用者也可以知道自己不可以碰哪里。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。

继承+多态:继承和多态必须一起说。一旦割裂,就说明理解上已经误入歧途了。

先说继承:继承同时具有两种含义:其一是继承基类的方法,并做出自己的扩展——号称解决了代码重用问题;其二是声明某个子类兼容于某基类(或者说,接口上完全兼容于基类),外部调用者可无需关注其差别。

再说多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。

很显然,多态实际上是依附于继承的第二种含义的。让它与封装、继承这两个概念并列,是不符合逻辑的。不假思索的就把它们当作可并列概念使用的人,显然是从一开始就被误导了。

实践中,继承的第一种含义(实现继承)意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。

继承的第二种含义非常重要。它又叫“接口继承”。

接口继承实质上是要求“做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的所有对象”——这在程序设计上,叫做归一化。

20140818090411_38748

归一化使得外部使用者可以不加区分的处理所有接口兼容的对象集合——就好象linux的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、磁盘、网络还是屏幕(当然,如果你需要,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。

归一化的实例:

  • a、一切对象都可以序列化/toString
  • b、一切UI对象都是个window,都可以响应窗口事件。
    ——必须注意,是一切(符合xx条件的)对象皆可以做什么,而不是“一切皆对象”。后者毫无意义。

显然,归一化可以大大简化使用者的处理逻辑:这和带兵打仗是类似的,班长需要知道每个战士的姓名/性格/特长,否则就不知道该派谁去对付对面山坡上的狙击手;而连长呢,只需知道自己手下哪个班/排擅长什么就行了,然后安排他们各自去守一段战线;到了师长/军长那里,他更关注战场形势的转变及预期……没有这种层层简化、而是必须直接指挥到每个人的话,累死军长都没法指挥哪怕只是一场形势明朗的冲突——光一个个打完电话就能把他累成哑巴。

软件设计同样。比如说,消息循环在派发消息时,只需知道所有UI对象都是个window,都可以响应窗口消息就足够了;它没必要知道每个UI对象究竟是什么——该对象自己知道收到消息该怎么做。

合理划分功能层级、适时砍掉不必要的繁杂信息,一层层向上提供简洁却又完备的信息/接口,高层模块才不会被累死——KISS是最难也是最优的软件设计方法,没有之一。

总结:面向对象的好处实际就这么两点。

一是通过封装明确定义了何谓接口、何谓接口内部实现、何谓接口的外部调用者,使得大家各司其职,不得越界;

二是通过继承+多态这种内置机制,在语言的层面支持归一化的设计,并使得内行可以从代码本身看到这个设计——但,注意仅仅只是支持归一化的设计。不懂如何做出这种设计的外行仍然不可能从瞎胡闹的设计中得到任何好处。
显然,不用面向对象语言、不用class,一样可以做归一化的设计(如老掉牙的泛文件概念、游戏行业的一切皆精灵),一样可以封装(通过定义模块和接口),只是用面向对象语言可以直接用语言元素显式声明这些而已;
而用了面向对象语言,满篇都是class,并不等于就有了归一化的设计。甚至,因为被这些花哨的东西迷惑,反而更加不知道什么才是设计。

2、人们以为面向对象是什么、以及因此制造出的悲剧以及闹剧

误解一、面向对象语言支持用语言元素直接声明封装性和接口兼容性,所以用面向对象语言写出来的东西一定更清晰、易懂。事实上,既然class意味着声明了封装、继承意味着声明了接口兼容,那么错误的类设计显然就是错误的声明、盲目定义的类就是无意义的喋喋不休。而错误的声明比没有声明更糟;通篇毫无意义的喋喋不休还不如错误的声明。除非你真正做出了漂亮的设计,然后用面向对象的语法把这个设计声明出来——仅仅声明真正有设计、真正需要人们注意的地方,而不是到处瞎叫唤——否则不可能得到任何好处。一切皆对象实质上是在鼓励堆砌毫无意义的喋喋不休。大部分人——注意,不是个别人——甚至被这种无意义的喋喋不休搞出了神经质,以至于非要在喋喋不休中找出意义:没错,我说的就是设计模式驱动编程,以及如此理解面向对象编程。

误解二、面向对象三要素是封装、继承、多态,所以只要是面向对象语言写的程序,就一定“继承”了语言的这三个优良特性。事实上,如前所述,封装、继承、多态只是语言层面对良好设计的支持,并不能导向良好的设计。如果你的设计做不出真正的封装性、不懂得何谓归一化,那它用什么写出来都是垃圾。

误解三、把软件写成面向对象的至少是无害的。要了解事实上是什么,需要先科普几个概念。

什么是真正的封装?——回答我,封装是不是等于“把不想让别人看到、以后可能修改的东西用private隐藏起来”?显然不是。如果功能得不到满足、或者未曾预料到真正发生的需求变更,那么你怎么把一个成员变量/函数放到private里面的,将来就必须怎么把它挪出来。你越瞎搞,越去搞某些华而不实的“灵活性”——比如某种设计模式——真正的需求来临时,你要动的地方就越多。

真正的封装是,经过深入的思考,做出良好的抽象,给出“完整且最小”的接口,并使得内部细节可以对外透明(注意:对外透明的意思是,外部调用者可以顺利的得到自己想要的任何功能,完全意识不到内部细节的存在;而不是外部调用者为了完成某个功能、却被碍手碍脚的private声明弄得火冒三丈;最终只能通过怪异、复杂甚至奇葩的机制,才能更改他必须关注的细节——而且这种访问往往被实现的如此复杂,以至于稍不注意就会酿成大祸)。一个设计,只有达到了这个高度,才能真正做到所谓的“封装性”,才能真正杜绝对内部细节的访问。否则,生硬放进private里面的东西,最后还得生硬的被拖出来——当然,这种东西经常会被美化成“访问函数”之类渣渣(不是说访问函数是渣渣,而是说因为设计不良、不得不以访问函数之类玩意儿在封装上到处挖洞洞这种行为是渣渣)。

一个典型的例子,就是C++的new和过于灵活的内存使用方式之间的耦合。这个耦合就导致了new[]/delete[]、placement new/placement delete之类怪异的东西:这些东西必须成对使用,怎么分配就必须怎么释放,任何错误搭配都可能导致程序崩溃——这是为了兼容C、以及得到更高执行效率的无奈之举;但,它更是“抽象层次过于复杂,以至于无法做出真正透明的设计”的典型案例:只能说,c++设计者是真正的大师,如此复杂的东西在他手里,才仅仅付出了如此之小的代价。(更准确点说,是new/delete和c++的其它语言元素之间是非正交的;于是当同时使用这些语言元素时,就不可避免的出现了彼此扯淡的现象。即new/delete这个操作对其它语言元素非透明:在c++的设计里,是通过把new/delete分成两层,一是内存分配、二是在分配的内存上初始化,然后暴露这个分层细节,从而在最大程度上实现了封装——但比之其它真正能彼此透明的语言元素间的关系,new/delete显然过于复杂了)。这个案例,可以非常直观的说明“设计出真正对外透明的封装”究竟会有多难。

接口继承真正的好处是什么?是用了继承就显得比较高大上吗?显然不是。接口继承没有任何好处。它只是声明某些对象在某些场景下,可以用归一化的方式处理而已。换句话说,如果不存在“需要不加区分的处理类似的一系列对象”的场合,那么继承不过是在装X罢了。

封装可应付需求变更、归一化可简化(类的使用者的)设计:以上,就是面向对象最最基本的好处。——其它一切,都不过是在这两个基础上的衍生而已。换言之,如果得不到这两个基本好处,那么也就没有任何衍生好处——应付需求变更/简化设计并不是打打嘴炮就能做到的。

了解了如上两点,那么,很显然:
1、如果你没有做出好的抽象、甚至完全不知道需要做好的抽象就忙着去“封装”,那么你只是在“封”和“装”而已。
这种“封”和“装”的行为只会制造累赘和虚假的承诺;这些累赘以及必然会变卦的承诺,必然会为未来的维护带来更多的麻烦,甚至拖垮整个项目。
正是这种累赘和虚假的承诺的拖累,而不是所谓的为了应付“需求改变”所必需的“灵活性”,才是大多数面向对象项目代码量暴增的元凶。
2、没有真正的抓到一类事物(在当前应用场景下)的根本,就去设计继承结构,是必不会有所得的。不仅如此,请注意我强调了在当前应用场景下。这是因为,分类是一个极其主观的东西,不存在普适的分类法。

举例来说,我要研究种族歧视,那么必然以肤色分类;换到法医学,那就按死因分类;生物学呢,则搞门科目属种…… 想象下,需求是“时尚女装”,你却按“窒息死亡/溺水死亡/中毒死亡之体征”来了个分类……你说后面这软件还能写吗?类似的,我遇到过写游戏的却去纠结“武器装备该不该从游戏角色继承”的神人。你觉得呢?事实上,游戏界真正的抽象方法之一是:一切都是个有位置能感受时间流逝的精灵;而某个“感受到时间流逝显示不同图片的对象”,其实就是游戏主角;而“当收到碰撞事件时,改变主角下一轮显示的图片组的”,就是游戏逻辑。

看看它和“武器装备该不该从游戏角色继承”能差多远。想想到得后来,以游戏角色为基类的方案会变成什么样子?为什么会这样?
——你还敢说面向对象无害吗?
——在真正明白何谓封装、何谓归一化之前,每一次写下class,就在错误的道路上又多走了一步。
——设计真正需要关注的核心其实很简单,就是封装和归一化。一个项目开始的时候,“class”写的越早,就离这个核心越远。
——过去鼓吹的各种面向对象方法论、甚至某些语言本身,恰恰正是在怂恿甚至逼迫开发者尽可能早、尽可能多的写class。

误解四、只有面向对象语言写的程序才是面向对象的。事实上,unix系统提出泛文件概念时,面向对象语言根本就不存在;游戏界的精灵这个基础抽象,最初是用C甚至汇编写的;……。面向对象其实是汲取以上各种成功设计的经验才提出来的。所以,面向对象的设计,不必非要c++/java之类支持面向对象的语言才能实现;它们不过是在你做出了面向对象的设计之后,能让你写得更惬意一些罢了——但,如果一个项目无需或无法做出面向对象的设计,某些面向对象语言反而会让你很难受。

用面向对象语言写程序,和一个程序的设计是面向对象的,两者是八杆子打不着的两码事。纯C写的linux kernel事实上比c++/java之类语言搞出来的大多数项目更加面向对象——只是绝大部分人都自以为自己到处瞎写class的面条代码才是面向对象的正统、而死脑筋的linus搞的泛文件抽象不过是过程式思维搞出来的老古董。

——这个误解之深,甚至达到连wiki词条里面,都把OOP定义为“用支持面向对象的语言写程序”的程度。——恐怕这也是没有人说泛文件设计思想是个骗局、而面向对象却被业界大牛们严厉抨击的根本原因了:真正的封装、归一化精髓被抛弃,浮于表面的、喋喋不休的class/设计模式却成了”正统“!

总结: 面向对象其实是对过去成功的设计经验的总结。但那些成功的设计,不是因为用了封装/归一化而成功,而是切合自己面对的问题,给出了恰到好处的设计。让一个初学者知道自己应该向封装/归一化这个方向前进,是好的;用一个面向对象的条条框框把他们框在里面、甚至使得他们以为写下class是完全无需思索的、真正应该追求的是设计模式,则是罪恶的。事实上,class写的越随意,才越需要设计模式;就着错误的实现写得越多、特性用得越多,它就越发的死板,以至于必须更加多得多的特性、模式、甚至语法hack,才能勉强完成需求。只有经过真正的深思熟虑,才有可能做到KISS。到处鼓噪的面向对象编程的最大弊端,是把软件设计工作偷换概念,变成了“就着class及相关教条瞎胡闹,不管有没有好处先插一杠子”,甚至使得人们忘记去关注“抽象是否真正简化了面对的问题”。