数据哲学论

​1. 所有公司都已经是某种程度的数据驱动。

2. 数据驱动不是增长的充分条件,也不是必要条件。

3. 数据很贵,性价比一般。

4. 数据的目标分为三个层次:观察、描述、行动。

5. 数据只能驱动符合特定模式的问题持续改进。

6. 数据项目也是项目。

7. 没有银弹。

图好玩的一个戏仿,不要当真。

为什么感觉豆瓣在走下坡?

知乎上的一个问题:http://zhi.hu/WhTD

—–

豆瓣的问题在于它太过偏执于“长尾”(1) 和“自发秩序”(2) 了。

如果你仔细看,豆瓣的大部分产品都遵循类似的思路:首先满足用户的“长尾需求”,再想办法把同质用户汇聚成组,最后相同兴趣用户的组内行为可以形成正反馈强化整个系统的运转。当然,具体到实现这个流程的方式还可以再细分,比如利用算法的(FM)或者利用社交网络(小组)来分组;“组”的概念比较弱的(书影音用户利用友邻关系和猜你喜欢形成的“隐性组”)或者形式感非常强的(小组、九点、广场、阿尔法城)。

大体上讲,虽然单个产品各有成败,但沿着这个思路去解决用户的“长尾需求”是没有大问题的。

问题在于豆瓣把这个思路贯彻到了偏执的程度。于是就有了不断重复的广场、部落、阿尔法城这些“自发生长”的理念高于用户实际需求的“玩意”,以及在有热点的垂直领域里也偏去玩草根 provider 和 consumer 的小圈子——读书的征文比赛、音乐人、同城的小型演出票务(可惜没有一个的草根电影人圈子,不然就圆满了)。甚至在运营、推广层面也践行“不运营,自生长”的策略 (3)。

但是如果按照长尾(帕累托)分布不严谨的表述“20/80法则”来看,费了这么大力气满足的可只是20%用户(对长尾里80%事物)的兴趣……

当然这其中有一些美好的假设,比如从长尾中涌现80%人会感兴趣的热门内容。的确,豆瓣里也涌现出了“失恋33天”这样的 case,但是最终把握住这些 case 的可不是“不运营”的豆瓣。至于《长尾理论》里暗示的“随着互联网的发展,用户数量/兴趣曲线会变平缓,整体长尾化”也好像并没发生。

回到问题“为什么感觉豆瓣在走下坡”,“感觉”两个字用在这里很好。豆瓣也没走下坡,书影音评论这看家的东西不还好着。只是这些年早前就不关注长尾而是玩热点的别人家已经把制造热点这个事情玩烂了,现在都去忙移动互联网和穿戴计算了。而豆瓣还在这儿纠结“长尾”和“自生长”纠结了六七年了,自然让人有种不进则退的感觉。

一定要说个原因,大概就是世界不是豆瓣想像的那个样子吧。

1. 长尾年代,http://book.douban.com/review/1095674/
2. 豆瓣是一座仁者见仁、智者见智的城市,http://www.douban.com/note/12053438/
3. 杨勃:豆瓣社区不运营 用产品来表达,http://tech.163.com/…

About Face 3 – Part I Understanding Goal-Directed Design

Introduction

“About Face 3″(以下简称AF3)主要关注于交互设计(Interaction Design)。

术语“交互设计”最早出现于19世纪80年代中期。当时负责设计第一台笔记本电脑GRiD Compass的两位工业设计师Bill Moggridge、Bill Verplank首先使用了这个术语。但是直到10年后,甚至可以说到了AF的第二版于2003年出版后,这个词才成为广泛使用。

一个比交互设计应用更泛滥的词是用户体验(User Experience)。AF3中认为用户体验设计可以被划分为三部分:

  1. 内容(Content):内容如何结构,如何组织。这部分由信息架构(Information Architecture)负责。
  2. 形式(Form):由工业设计和视觉设计负责。
  3. 行为(Behavior):由交互设计负责。

按:这三部分同样符合“结构-表现-行为”这一模式。所以它们和Web技术之间有着同样的结构。

AF3认为一个好的产品团队需要四个部分的协作:设计、工程、市场、管理。其中设计决定产品的形式和行为;工程负责开发;市场面向客户;管理负责协调。

按:AF3整体的一个问题是:对设计有一种“报复式的强调”。AF3介绍的方法包含两大部分,主体部分是设计本身的原则、模式等等;但除此之外还涵盖了传统软工中的需求和分析。AF3的立论点是,设计和用户的目标密不可分,所以设计师必须极深地介入需求分析,甚至说由设计师来完成需求分析。从上面的团队组成和职责也可以看出,实际上这是一个由设计师主导的团队。

AF3提供一系列交互设计需要的:原则(Principle)、模式(Pattern)、过程(Process)。交互设计的质量依赖于语境(Context),即用户是谁、他们要做什么、他们的动机是什么。大而无当的原则可以让设计更简单,但不能让设计更好。

———————————

Chapter 1 Goal-Directed Design

工业设计师Victor Papanek定义设计为:“the conscious and intuitive effort to impose meaningful order”。

AF3认为面向人(human-oriented)的设计活动包括:

  1. 理解用户的愿望、需要、动机、语境
  2. 理解商业、技术和领域中的机会、需求和限制
  3. 使用以上知识构造满足以下要求的产品:其内容、形式、行为是有用的、可用的以及符合预期的,同时它在经济上是有利的,在技术也是可行的。

按:注意此处设计含义的泛化,内容已经成为设计的一部分。

糟糕设计的主要原因:

  1. 忽视用户;
  2. 满足需求和实现功能发生冲突(特别是开发人员同时负责设计的情况);
  3. 缺乏理解用户需求的过程(让用户参与设计流程不能解决问题,因为领域知识不等于设计知识)。

交互设计需要基于对用户的理解以及认知原则。因此目前广泛使用的面向任务(task)的开发过程达不成好的设计:理解用户的任务有助于对系统进行细分,但是不能帮助设计师理解用户为什么使用这个系统——即用户的目标。设计师的工作不仅仅是理解用户的任务,他们还需要辨识出最重要的用户,进而决定他们的目标是什么,以及为什么有这个目标。

按:再一次注意设计师只能的泛化

AF3提出了“面向目标的设计过程(Goal-Directed Design Process)”,以下简称GDD。GDD方法首先专注于研究用户实际如何使用产品,进而把研究结果转化为设计方案。

GDD强调设计对产品的重要性。它认为设计既要辨识用户需求,又要关注产品的细节——换言之,设计即是产品定义。因此GDD要求设计师深度介入需求分析工作,理解用户的目标,并通过一系列系统化方法,通过使用模型,把需求转化为最终的设计。

GDDP方法包含六个主要步骤:

  1. 研究:通过定性的研究辨识出行为模式(behavior pattern)
  2. 建模:利用行为模式创建领域模型(domain model),以及角色模型(personas)——也即用户模型。前者主要包含流程信息,后者是具备不同动机、目标的用户组。
  3. 需求定义:联系角色与流程,生成语境场景(context scenario)——用故事板的形式描述用户如何使用产品。同时商业目标、品牌属性、技术限制也需要构建场景时加以考虑。
  4. 框架定义:定义产品行为(behavior)、视觉设计(visual design)以及物理形态(physical form)的基本框架。对于交互设计,另有两种方法对其进行指导——原则(自底向上)以及模式(自顶向下)。
  5. 精化(refine):丰富细节,验证覆盖度,产出设计规范。
  6. 开发支持:协助开发,必要时调整设计。

成功的交互设计必须时刻关于用户的目标。

交互设计不是猜谜。(Interaction design is not guesswork.)

———————————

Chapter 2 Implementation Models and Mental Models

实现模型指系统内部实现(每秒24格画面)。心智模型指用户看待系统的方式(图像在屏幕上移动)。

按:心智模型应该是心理学的一个术语,不清楚此处的应用是否准确。

实现模型和心智模型存在鸿沟。因此需要Represented模型,即(在数字产品中)设计师选择将程序功能暴露给用户的方式。

好的Represented模型更接近的心智模型(画面加速、减速),而开发人员进行设计的缺陷在于其设计结果常常更接近实现模型(每秒格数更多更少)。

机械时代和信息时代需要不同的Represented模型。使用机械产品模型作为信息系统的隐喻,往往会限制信息系统的设计。好的设计需要信息时代的思维模式。

———————————

Chapter 3 Beginners, Experts, and Intermediates

设计的一个难题是如何在统一的设计中兼顾初学者和专家。

然而大部分用户既不是初学者也不是专家,而是中等用户(Intermediates)。实际上,大部分初学者会很快变成中等用户,但是他们永远也不会变为专家。

开发人员设计的交互往往更适合专家(比如,列出所有可能的操作,不区分操作的优先级);市场人员则需要更适合初学者的交互(方便推广)。

设计需要为中等用户进行优化,这包括三方面内容:

  1. 初学者成为中等用户的学习曲线低——想象初学者聪明并且忙碌;
  2. 希望成为专家的用户不会遭遇障碍——专家积极地寻找并学习产品可能实现他们需求的方式;
  3. 永久性的中等用户(perpetual intermediates)能对产品保持满意——中等用户会使用手册;他们会很快区分出(对他们来说的)常用功能和不常用功能;他们通常知道高级功能的存在,虽然他们可能不知道如何使用高级功能。

———————————

Chapter 4 Understanding Users: Qualitative Research

按:本章提供的其实是一种广义的用户研究方法,该方法并不限于设计

定性的用户研究用来帮助设计师理解产品的领域、语境和限制,包括:

  • 潜在用户的行为、态度、能力倾向(aptitude)
  • 产品的领域——技术、商业和环境的语境
  • Vocabulary and other social aspects of the domain in question(按:不理解
  • 已有产品的使用方式

定性研究需要回答的问题包括:

  • 产品是如何介入用户生活的?
  • 用户使用产品的目的是什么?以及完成这一目的的基本步骤有哪些?
  • 哪些体验可能使用户产生兴趣?
  • 用户目前完成工作所遭遇的问题有哪些?

定性研究包含以下工作:

—–

1. Stakeholder访谈:

  • 新产品的设计需要从理解产品在商业和技术上的语境出发。
  • Stakeholder包括:大老板们;经理;开发、商务等各部门的高手。
  • 单独地访谈各个Stakeholder。
  • 可能从Stakeholder得到的重要信息包括:产品的愿景、预算和Schedule、技术限制和机会、商业目的。

2. 领域专家(Subject matter expert)访谈:

  • 需要,但是专家可能有偏见,因为:他们是专业用户;他们具备领域知识但不是设计师。
  • 对于复杂和专业化的领域来说,领域专家访谈以及让专家持续介入设计过程是必须的。

3. 客户(Customer)访谈:

  • 客户和用户不同。客户是掏钱的人。

4. 用户(User)访谈:

  • 用户是设计的聚焦之处
  • 可能从用户更得到的信息包括:
    • 现有(或类似)产品如何介入用户生活;
    • 用户完成工作需要具备的领域知识;
    • 目前产品支持以及不支持的功能;
    • 用户使用产品的目的和动机;
    • 用户心智模型(用户如何理解产品);
    • 目前产品存在什么问题和给用户带来什么挫败感;

5. 用户观察:

  • 用户访谈脱离了产品实际使用的环境。为了更好理解用户如何使用产品,可能的话需要进行用户观察以获得第一手资料。
  • 可以采用一种将沉浸式观察和访谈相结合的方法:Ethnographic Interviews——用户在使用产品的同时接受访谈。本章后半段详细介绍了如何进行一次Ethnographic Interview。

6. 文字资料分析:分析各种文字资料,包括:市场计划、市场研究、用户调研、技术规范、竞争对手研究等等。

7. 已有产品、原型以及竞品分析

—–

其他常用的用户研究方法还有:焦点小组——脱离语境的数据未必反映真实情况、基于市场的用户分析(market demographics and market segments)——理解谁愿意买不等于得到好的设计、可用性和用户测试——在有备选方法后有意义、Card sorting、Task analysis。

———————————

Chapter 5 Modeling Users: Personas and Goals

角色模型(Personas)是作为研究结果的行为模式、心智模型以及用户目标的形式化。角色模型可以帮助设计师对用户分类,确定不同用户的需求,进而确定目标用户的类型——包含所有可能特性的产品,不能取悦任何人(a product with every possible feature pleases nobody)。

角色模型可以帮助设计师避免产品设计中一些常见的问题:

  • 弹性(elastic)用户:每个设计师都对用户需求有不同的理解。最终用户需求成为所有人需求的并集;
  • 主观设计:设计师按自己的目标、动机、心智模型来设计产品;
  • 边缘情况:关注那些可能发生但不是目标用户需求的情况。

角色模型的一些基本要点:

  • 基于用户研究结果;
  • 每个用户模型被描述为一个人,但代表一组用户;
  • 包含一组行为;
  • 具备自身的动机;
  • 除用户外,也可能是客户(customer personas)或者被服务者(served personas)——例如医疗设备的病人。

与传统软工方法中的用户角色(Role)相比,角色模型(personas)更关注用户的目标,更倾向用描述性的话语来表达自身。Personas可能是多个具有同样行为和目标的Role;也可能是对具有不同目标的同一Role的细分。

按:我在原书上标注了这段话,但现在看意义有限。因为书中并没有定义Role获取方法,所以没法精确说明Personas和Role的区别。

当不能通过研究获得严格的角色模型时,也可以使用靠猜想获得的临时角色模型(Provisional Personas)。但使用临时角色模型具有以下风险:

  • 关注于错误的设计目标;
  • 即使关注了正确的目标,也可能错失重要的行为;
  • 难以容易说服不认同你观点的人;
  • 错误的使用,可能导致他人不认同角色模型。

目标(Goals)对于角色模型至关重要。目标是用户行为的动机,其必须由研究数据中获得。每个目标需要能够用一句简单的句子来描述。

Norman提出了三个层次的认知加工(Cognitive Processing):

  • 本能的(Visceral):及时的,在进一步交互之前,在视觉或其他感觉方面的反应;
  • 行为的(Behavioral):简单的,日常的行为——这是人类最主要的活动;
  • 反射的(Reflection):基于以前的经验,有意识的行为;

基于以上三个层次的认知,存在三种类型的用户目标(User Goals):

  • 体验目标(Experience Goal):
    • 与本能认知相关——用户想感受什么;
    • 用户希望在使用产品时希望获得的感受。这类目标关注于,视听特征、交互感受(动画的流畅度、按钮“硬度”)、物理设计;
    • 交互、视觉、工业设计师需要将这类目标转化为具备恰当感觉、情绪、音调的形式、行为、手势等;
    • 面向本能的设计意味着在深入使用之前,对产品的第一感觉进行设计。
  • 最终目标(End Goal):
    • 与行为认知相关——用户想做什么;
    • 用户使用产品完成功能的动机。这类目标关注于,产品交互设计、信息架构、功能方面的工业设计;
    • 交互设计师需要以最终目标为基础设计产品的行为、任务、外观、感受;
    • 面向行为的设计使产品的行为符合用户的行为、隐含假设以及心智模型。
  • 生活目标(Life Goal):
    • 与反射认知相关——用户想成为谁;
    • 用户长期的期望、动机以及自我实现愿景。这类目标关注于,产品的总体设计、策略以及品牌;
    • 交互设计师需要将生活目标转化为high-level的系统能力、设计概念以及品牌策略。生活目标很少直接指导具体细节的设计,但是这类目标必须时刻保持在脑海里;
    • 面向反射的设计使产品与用户建立长期的联系。

除了用户目标外,设计还需关注客户目标(Customer goals)、商业与组织的目标(Business and organization goals)、技术目标(Technical goals)。

“好的设计”只有在用户为了某种目标使用产品时才有意义。成功的产品首先满足用户目标。

构造角色模型的步骤:

  1. 从访谈中辨识行为变量(behavioral variables),最常见的行为变量包括:
    • 活动(Activities):用户做什么,以及频率和数量
    • 态度(Attitudes):用户对产品领域和技术的看法如何
    • 能力倾向(Aptitudes):用户的教育程度、受过何种训练、学习能力如何
    • 动机(Motivations):用户为什么对产品领域感兴趣
    • 技能(Skills):用户在产品领域技术方面的能力
  2. 通过访谈对象与行为标量进行映射,将访谈对象分组
  3. 通过分组辨识行为模式(behavior pattern),作为角色模型的基础
  4. 综合特征与目标:针对每个行为模式,参考访谈细节给出:
    • 角色模型潜在的使用环境、典型使用场景、现有解决方案及其不足。
    • 角色模型需要一个有意义的名字,以及一些简单的人物描述。
    • 目标是角色模型定义中最重要的工作,其中最终目标(end goals)对设计用处最大。
    • 一般来讲,一个角色模型会包含0-2个体验目标、3-5个最终目标、0-1个生活目标。
  5. 检查完备与冗余
  6. 扩展角色属性和行为的描述:描述应该是对角色模型日常生活的结论性概括,但不构成一个Short Story
  7. 指定角色的类型:
    • 首要(Primary)角色:产品的每个界面对应一个首要角色;当没有唯一清晰的首要角色时,意味着:要么产品需存在多个首要角色,需要为每个首要角色进行单独设计;要么产品想做的事情太多了。
    • 次要(Secondary)角色:次要角色的大部分需求可以被为首要角色设计的界面满足,但是他们还包含一些额外的,可以在不扰乱首要角色功能的情况下,被满足的需求。当存在多余3-4个次要角色时,可能意味着产品的边界不明。
    • 补充(Supplemental)角色:潜在的,或者源于Stakeholder假设的角色。
    • 客户(Customer)角色
    • 被服务(Served)角色:不直接使用产品,但被产品直接影响的角色——例如医疗设备产品的病人。
    • 无关(Negative)角色

在用户建模阶段,除角色模型外,还可以获得一些辅助的模型:

  • 工作流(Workflow)模型:可能是角色模型级别的个人工作流,也可能是商业、组织级别的协同工作流。
  • 制品(Artifact)模型:用户完成工作需要用到的或者产出的制品。
  • 物理(Physical)模型:用户活动的物理环境。简单的物理环境可以直接在角色模型中说明,复杂的则需要单独的物理模型。

———————————

Chapter 6 The Foundations of Design: Scenarios and Requirements

在GDD方法中,从定性的研究数据到设计解决方案,通过四个不断迭代的步骤来完成:

  1. 通过研究数据以及角色模型,想象用户交互的场景(Scenario)
  2. 利用场景定义需求(Requirement)
  3. 利用需求定义基本的交互框架(Interaction Framework)
  4. 不断丰富框架的设计细节

场景(Scenario)作为设计中的概念最早由John Carroll在90年代提出。Carroll的基于场景的设计(Scenario-based Design)方法中,场景以故事的形式描述用户如何完成工作——类似于电影中的故事板,进而用于创建和演示设计方案。

AF3中的GDD方法与Carroll方法的不同之处在于:在场景获取之前,角色模型,特别是用户的目标已经明确;而场景的获取依赖于之前步骤生成的角色模型(Personas-based Scenario)。

GDD方法中定义了三类场景:

  • 语境场景(Context Scenario),或曰“日常生活场景(Day-in-the-life Scenario)”:先于任何设计,只涉及活动、感觉、愿望。
  • 关键路径产场景(Key Path Scenario):由语境场景精化得到,关注于最重要的交互,以及用户如何使用产品实现其目标(goal)。
  • 验证场景(Validation Scenario)

场景和用例(Use cases)的不同在于:用例更加技术化,更具体地描述用户的每个操作以及系统的响应。用例的问题在于,一来用例没有详细定义系统(在设计层面上)的行为;二来用例没有优先级,因此更适合用来验证系统功能的完备。

需求(Requirement)定义产品是什么以及产品做什么。设计定义产品如何做。需求先于设计。需求不是功能(function)或者特性(feature),而是用户或者公司业务的需要(need)。

按:大白话,这也是我为什么说AF3的Part I与其说是设计方法,不如说是需求方法。

需求定义的步骤:

  1. 建立问题和愿景描述:
    • 问题描述(Problem statements)反映了需要改变的现状,例如:公司X的客户满意度很低,因为用户没有适当的工具完成任务X、Y、Z以帮助他们实现目标G;
    • 愿景描述(Vision statements)是问题描述的反面,例如:新设计的产品X可以使用户可以更好(精确、有效等等)地完成任务X、Y、Z,并解决现有的问题A、B、C,进而帮助用户完成目标G。新设计将会显著提高用户的满意度。
  2. 头脑风暴:对于产品的头脑风暴,目的是排除一些在研究阶段产生的思维定势。
  3. 辨识角色期望(Personas expectations):
    针对每个首要和次要角色,需要辨识:

    • 一些影响用户期望的因素,例如:态度、经验、社会身份、文化等等
    • 对于使用产品体验的期望
    • 对产品行为的期望
    • 角色认为产品应该包含的基本元素以及数据
  4. 构建语境场景:
    • 语境场景很像故事,其关注于用户活动、动机以及心智模型;
    • 语境场景high-level地从用户的视角展示了产品使用模式(Usage pattern);
    • 语境场景不关注产品和交互的细节;
    • 语境场景不应该被已有产品所局限,假设任何想法都可以神奇地实现(Pretend the interfa is magic)。
    • 按:大概是全书精化所在之一,书中的例子很有说明性,限于篇幅不再引用。
  5. 辨识需求:
    需求由对象、行为和语境组成。 例如:在一个预约条目中(语境)直接给某人(对象)打电话(行为)。按:GDD强调这个需求应该是一个need,而不是task或feature。但仅以上面例子,我感到其中的差异很微妙。此外,这里的结构很像从场景中发现用例,再从用例中辨识名词(类)和动词(方法)的方法。需求可以被进一步划分为:

    • 数据需求:需要被展示在系统中的对象和信息;
    • 功能需求:在对象上执行的操作;
    • 其他需求:其他一些需要,特别是限制。例如:商业需求、品牌体验需求、技术需求、客户和合作伙伴需求。

———————————

Chapter 7 From Requirements to Design: The Framework and Refinement

设计框架(Design Framework)定义了用户体验的整体结构。在此阶段不必考虑细节,只需要产生产品的Big Picture。纸面原型(Paper Prototype)可以作为本阶段讨论用的有效工具。设计框架包含三部分:

  • 交互框架:使用场景和需求创建产品界面和行为的骨架
  • 视觉设计框架
  • 工业设计框架

定义交互框架的步骤:

  1. 定义产品形态(Web, Phone或者其他什么)、使用姿态、输入方式——这些定义依赖于需求定义中获取的语境场景
  2. 定义功能(functional)以及数据(data)元素:
    • 定义时需要经常回顾场景、角色目标和心智模型;
    • 将系统想象为一个“人”,有利于实现结构化的交互细节;
    • 基于场景的设计方法是一种top-down不断细化的方法,而已有最佳实践的设计原则(Principle)和模式(Pattern)是对top-down方法一个补充;
    • 除非非常明确的理由,不应该破坏已有的最佳实践。
  3. 确定功能的分组和层次:需要考虑角色的工作流程以及某个任务的相关任务
  4. 交互框架骨架(Sketch):
    • 主要任务是画线框图
    • 尝试多种布局
    • 迭代
    • 一开始建议使用白板直接绘图,当细节完善到一定程度后再使用CAD工具(Firework、Illustrator、Visio、PPT、OmniGraffle)
  5. 构造关键路径场景(Key Path Scenarios):
    • 关注任务(task)级别
    • 角色的目标(goal)必须在考虑范围内
    • 使用故事板(storybroading)描述关键路径场景
  6. 步骤3-5可能存在一些变体,例如:
    • 从功能出发的变体:先辨识关键路径,再分组,再创建骨架
    • 从视觉出发的变体:先分组+骨架;再关键路径,回头检查分组是否和关键路径切合
    • 按:我觉得显然应该从功能出发,因为这更不容易出现错误…… 当然可能是思维定势
  7. 使用验证场景(Validation Scenarios)检查设计:
    • 设计能否满足关键路径场景的变体
    • 设计能否满足一些不频繁发生但必要的场景(Necessary use scenarios):不频繁意味着不需要诸如快捷键、用户定制等功能
    • 设计能否满足边缘场景(Edge case use scenarios):边缘场景不应该被忽视,但是针对其的设计不应该影响其他场景

按:后面两部分没有很认真的看。

定义视觉设计框架的步骤:

  1. 开发视觉语言(Visual Language Studies):需要考虑角色的体验目标、产品品牌关键词、以及公司品牌
  2. 将视觉风格应用到原型

定义工业设计框架的步骤:

  1. 与交互设计师合作定义产品形态和输入方式
  2. 开发粗略原型
  3. 开发形式语言(Form Language Studies)

学好编程,报复社会

功能:通过Amazon Cloud Reader,获得已购买书籍的原文。

步骤:

  1. 访问 https://read.amazon.com/
  2. 右键想获得原文的书,选择Download & Pin
  3. 等待下载结束,书上出现绿色别针
  4. 点击进入阅读
  5. 打开链接(失效了,参见下面的源代码)。把其中文本复制到Amazon Reader的地址栏中
    (Safari下过;但在Chrome下测试出现诡异的错误。如果Chrome没效果,可以按F12进入Console Tab,把“javascript:”后面的部分粘贴到命令行中)
  6. 等一会儿,然后Ctrl-A & Ctrl-C
  7. 回馈社会

原理:Kindle Reader用了localStorage database来存储Pin到本地书籍,这些数据库中的内容除了被lz压缩外,没有特殊处理,所以只要把数据取出来解压缩即可。

不足:不能获得图片(图片也是被放进database的,实现起来不难,只是我懒得改了);文本是带html标签的,直接贴到支持html标签的编辑器中可能会出问题。

下载:没绕之前的源代码(原链接失效了,新的放在 gist 里,可能也用不了,参见 comment 或者自己调试)

当我们嘲笑UML的时候,我们应该嘲笑什么

按:上周偶尔谈起UML,想到自己UML、UP(统一过程)的知识有5-6年没更新了,干脆趁没忘光都吐出来。吐槽文,达意第一,术语使用不严谨。

——————————————————-

为了嘲笑UML,我们必须追溯UML的初衷,于是我们必须了解现实世界和软件世界之间天然的隔阂。虽然人们对软件已经习以为常,但是软件的构造并不像其他物理工具的制造那样透明。对软件一无所知的人来说,“编程”一词或多或少包含了“魔法”的成分。

如果对软件的内在机制有所了解,就可以更容易地表达软件与现实之间的鸿沟——即,描述现实世界的自然语言和描述软件世界的机器语言之间的失配(mismatch)。现实世界的需求不能轻易地被机器语言表达,这是鸿沟所在,或者更专业地说,是软件构造复杂度的来源。

一直以来,计算机行业用两种方法降低这种复杂度。第一种方法是对机器语言进行封装——通过引入高级语言、面向对象等概念,使编程语言可以更直接地描述现实世界。

(题外话,这是为什么从数(美)学上看编程语言越来越dirty。因为虽然Lisp很美——美到某本书某页最下方巴掌大的Lisp代码包含了计算的本质,但这种美却不能帮助Lisp更好地描述dirty的现实世界。)

但因为任何编程语言本质上都和图灵机等价,所以编程语言与现实世界的接近程度是有极限的。现存的编程语言依然难以直接描述复杂的现实。

于是计算机行业用第二个办法来弥合现实与软件的隔阂,这就是建模(modelling)——通过建模将现实世界抽象为模型,使现实世界更加清晰精确;编程语言不再直接描述现实世界,而是描述现实世界的模型。

“建模”包含两部分内容:首先是行为(“建”),即如何构造模型的方法;其次是结果(“模”),即作为方法制品的模型。

UML模型是一种用于构造软件的模型,是现实世界和软件世界的有效中介物。UML模型被用来描述现实世界的结构,并保证描述结果和软件结构天然近似。

但UML不是一种建模方法,UML没有定义将现实世界转化为UML模型的建模方法。实际上,使用UML模型的建模方法被称为“Unified Software Development Process(统一软件开发过程,缩写为UP)”。

UP的官方定义语是“用例驱动,以架构为中心,迭代增量的开发过程”。UP以模型为中介,降低软件开发的复杂度,帮助人们更容易地将现实世界的需求转化为软件。UML模型仅仅是UP的制品,而且仅仅是制品中的重要部分。一个未必恰当的比喻:UP是催动UML招式的内功心法。

遗憾的是现实中UP被丢弃、被遗忘、被不为人知。于是产生了各式各样对UML的误用:

– 比如,以为UML用例图就是用例;以为画小人圈圈就描述了需求。实际上,UP中首先要用文档附以领域模型和业务模型描述需求。每个用例都是从需求中抽取,并且是包含参与者、前置条件、后置保证、主要事件流、扩展事件流的完整文字描述。用例图仅仅用来追溯索引用例的图示。

– 比如,以为UML只包含类图;以为类图是银弹;以为使用UML就是画一张事无巨细的类图。其实UP中包含构造系统类图的完整流程方法,这包括:首先抽象现实世界的概念关系构造领域模型类图。而后以用例为中介精化领域类图,从用例中抽取包含新辨识概念关系的分析类图。而分析类图又会在后面的过程中被精化为设计类图,直到被用来指导具体实现。

– 比如,因为各种误用,以为软件开发的复杂性是UML造成的,以为摆脱UML就摆脱了软件开发的复杂性。实际上,软件开发天然复杂。摆脱UML不意味着人们可以用编程语言直接描述(具备一定复杂度的)最终系统。相反人们还是会以“土法炼钢”的方式构造自己的中介物。

此处我也无意为UP布道。不过我想说当我们像上面那样使用UML并造成混乱的时候,我们不应该嘲笑UML,UML应该嘲笑我们,因为我们才是混乱的根源。

当我们嘲笑UML的时候,我们到底应该嘲笑什么?

UML或曰UP的最大问题在于:它们认为模型是解决一切问题的银弹。

实际上以建模来弥平现实与软件的鸿沟,需要经历两个步骤:首先是对现实进行抽象,将现实转化为一种更精确更易理解的模型;第二步则是将模型“具象”到软件世界,即用编程语言实现获得的模型。其中的第一个步骤中,模型作为最终制品很重要;但第二个步骤必须落实到编程语言对软件实现上。

UP没有认清这一点,所以UP没有定义出模型使用的边界,没有给出在抽象过程结束后抛弃模型的时机,也因此没有将注意力集中到编程语言对软件的实现上。相反UP要求对模型不断精化,要求模型不断接近最终的软件。

这导致UML或者更广义建模研究的终极理想成为:通过精化的模型直接生成运行软件,或者更直白地说企图用模型替代编程语言。

然而无论直观感觉,还是多年的研究结果都表明:在企图精确描述软件世界时,模型并不比编程语言更优越——即,在深入到一定细节后,表达等量的内容,模型和编程语言同样复杂(如果前者不是更复杂的话)。

所以以模型替代编程语言的所有努力看上去是注定会失败的。UML是所有这些徒劳无功的形象代言人。

因此,我想,只有当我们看到那些事无巨细的类图、极端精确的时序图,看到那些双向转换模型的纷繁规则,看到那些神似编程语言的“建模语言”时,我们才可以去嘲笑UML。

因为这些多少代表了UML/UP以为自负可以达到,但其实不可能达到的境地。

在线代码着色

写论文或ppt的时候,偶尔需要贴一点代码。

早年用Ultraedit的时候就直接截屏。后来转到Vim之后,问题就来了。Vim的字体和文档完全不能搭,只好单独找地方着色,于是陆陆续续用过以下方案:

  • Eclipse,之前还偶尔用这大玩意,后来根本就不装了;
  • Google Code,svn过去让google染色。后来我觉得这样太sb了;
  • Quick Highlighter,online版本合用的只找到这个。可是每次都要提交到后台,刷页面很麻烦,支持语言种类虽然多,但其实完全用不到,于是放弃了;
  • SyntaxHighlighter,后来一直用。建个专门的文件,每次改这个文件,时间长了也不爽……

前两天写ppt时实在受不了了,今天痛定思痛,基于SyntaxHighlighter写了一个Online的。

有需要的就拿去用吧,http://qizhao.sakinijino.com/tools/sh.html

p.s. 目前在非webkit的浏览器下好像会重复下载brush的js,导致着色的性能问题。暂时没搞清楚原因,先凑合着吧。

你们再也不能说我没有主页了吧!

总被人批评,作为合格的Garbage Paper Generator,怎么能没有自己的主页呢!

所以,现在,我有了!http://qizhao.sakinijino.com

又及:那个,所有在页面顶上看到下面这张图的,

———-Ready? Go!————–

尼玛用的是chrome皮肤的ie6, ie7有没有!!!360浏览器有没有!!!搜狗有没有!!!!腾讯套套有没有!!!有没有!!有没有!!!!!!

尼玛知不知道就因为你用ie 6搞死多少淫!!!
ie 6的bug能出本书了,你懂不懂!!!浮动双边距懂不懂!!!居中不能懂不懂!!!盒模型懂不懂!!!懂不懂!!!!
不知道神马情况,一滚屏字都没了,google都google不出神马毛病有没有!!!有没有!!!!!

我就想用个HTML5的hash change event好不好!!!!不过是让单页面支持后退按钮好不好!!!我塔玛都是为了你好,好不好!!!好不好!!!!
尼玛爱用爱伊六!!!!我得拿iframe模拟有木有!!iframe真塔玛无所不能有木有!!!一大堆巨丑的判断分支有没有!!!基于巧合的编程有没有!!!有没有啊!!!!

草泥马。

尼玛好好看看上面那句话!!!!尼玛的浏览器过时了懂不懂!!!!!你懂不懂!!!!
安全!!!!!懂不懂!!!! 快速!!!!!懂不懂!!!!最塔玛重要的!!!免费!!!!你懂不懂!!!!懂不懂!!!!!不是只有三六零才免费!!!!懂不懂!!!!
价格便宜量又足!!!!!好不好!!!!!好不好!!!!!!

尼玛用着珂萝米皮肤的爱伊六的亲们!!亲们伤得起!!!我真伤不起!!!!!!

尼玛又及:尼玛谁咆一个的升级浏览器通知图片出来!!!有没有!!!!

动态语言中的设计模式

今儿迁主机的时候,顺便把旧文档分类整理了一下。发现自己在04年还写过这么一篇…… 居然还投给了《程序员》…… 那时候真是太有闲情逸致了,我操……

读了读,除了“检视”这种港台腔外。发现我那会儿就有把简单事写复杂的天赋…… 恩,以下愿者上钩……

——时光倒流的分割线———

动态语言中的设计模式

Grady Booch在为《Design Patterns》一书所写的序言中谈到,“软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径”。 从设计模式出现的那一天起,这众多的模式就成为面向对象软件开发过程中一笔宝贵的财富,而对设计模式的总结工作也从来没有停止过。先是GoF的《Design Patterns》中23种经典模式的编目和命名,而后广大开发人员在此基础上不断扩充。这种种工作都为人们积累了重要的设计经验。

然而尽管设计模式本身应该与具体语言实现无关,但设计模式是从实践中总结而来的,因此在成文过程中它又不可避免的与特定的语言相关联。目前由于在工业中C++、Java等静态语言占据主导地位,所以人们总结出来的设计模式通常也是基于这些语言的。

如今计算机硬件仍然依照摩尔定律所提出的趋势飞速发展,软件的复杂度也越来越高,相对而言软件开发效率却一直没有质的飞跃。因此执行效率相对低下但开发效率较高的动态语言愈发受到人们的关注。但是想在动态语言中使用目前总结出的大量设计模式却存在一定的困难,因为静态语言与动态语言的语义差异很大,如果只是照搬目前这些设计模式并不能充分发挥动态语言的语言特性,有时候把某些静态语言的模式运用到动态语言上甚至是作茧自缚、画蛇添足了。

Python作为一种完全面向对象的动态语言,因为其简洁、易用、灵活的特点已经越来越受到开发人员的青睐。以下行文中就将用Python实践几种简单的模式,以此来逐步展示Python作为动态语言所具有的语言特性对于设计模式实现的影响。

1.    运用动态类型特性实现灵活性更高的Abstract Factory

不妨让我们从《Design Patterns》中所提到的第一种模式Abstract Factory开始。

实际上同样是面向对象的语言,把C++的代码翻译成Python的代码并不难。下面Python代码就是由《Design Patterns》中提供的用于创建迷宫相关产品的抽象工厂类MazeFactory翻译得到:


class MazeFactory:
  def makemaze(self):
    return Maze()
  def makewall(self):
    return Wall()
  def makeroom(self, n):
    return Room(n)
  def makedoor(self, r1, r2):
    return Door(r1, r2)

这个类就可以用于创建Maze的基本产品了,同时我们可以继承这个类已得到其他系列产品的工厂,比如魔法迷宫EnchantedMaze的抽象工厂:


class EnchantedMazeFactory(MazeFactory):
  def makeroom(self, n):
    return EnchantedRoom (n)
  def makedoor(self, r1, r2):
    return EnchantedDoor (r1, r2)

这样我们就不再需要将有关Maze风格的代码硬编码到具体Maze的创建过程中,而当创建不同风格的Maze时只需要替换不同的Factory就行了。
不过这么简单的翻译C++代码很无趣,而且也不能充分体现Python作为动态语言为编程人员带来的便利。下面让我们来看一看Python语言中的动态特性能为我们做些什么。

略加察看后我们就会发现这样实现的MazeFactory具有一个不足之处:很难向MazeFactory里添加新种类的产品。当需要支持新类型的产品时(比如一个陷阱产品Trap以及EnchantedTrap),我们需要修改MazeFactory以及它的所有子类,这是一件很枯燥的事情。而且我们还不得不改变MazeFactory的接口,这更是我们不愿意看到的。

对于这个问题GoF的书中向我们介绍了一个比一般Abstract Factory更灵活的解决办法,即只实现一个统一的make函数,通过传递给make不同的参数来确定要创建的产品的类型。

然而对于C++这样的静态语言,实现这种解决方法很困难。因为为了确定make返回值的类型,我们不得不将所有的产品继承自一个公共基类。但是Abstract Factory考虑的只是产品的风格系列,而同一系列不同类型的产品间逻辑上可能不存在明确的公共基类——比如Maze和Wall。而且即使使用公共基类也会有导致大量的向下强制类型转换出现。这些都是我们编程时希望避免的。

此时动态语言中动态类型特性的优势充分体现出来。动态类型允许一个变量在运行时刻绑定到不同类型的对象上,也就是说所有变量——包括函数的参数和返回值——都可以是任意类型的。因此我们不必要求Maze、Room等不同类型产品具有公共基类,也能很容易就能实现这种拥有统一make函数的工厂:


class Maze:…
class Wall:…
class Room:…

…

class MazeFactory(object):(注1)
  def make(self, typename, *args):
    if typename == 'maze': return Maze()
    elif typename == 'wall': return Wall()
    elif typename == 'room': return Room(args[0])
    else typename == 'door': return Door(args[0], args[1])

class EnchantedMazeFactory(MazeFactory):
  def make(self, typename, *args):
    if typename == 'room': return EnchantedRoom (args[0])
    elif typename == 'door': return EnchantedDoor (args[0], args[1])
    else: return super(EnchantedMazeFactory, self).make(typename, args)

而创建Maze的代码可能是:


mf = EnchantedMazeFactory ()
mz = mf.make('maze')
r1 = mf.make('room', 1)
mz.addroom(r1)

make函数第三个参数*args表示把从第三个开始以后所有的参数接受为一个list,产品的构造函数从这个list中取出相应的参数用于构造对象。super是内置函数,用于返回一个变量的基类对象。

这样利用Python动态类型的特性(注2),我们用很小的代价实现了一个具有更多灵活性的MazeFactory了。现在当我们要支持新的产品类型时,我们只需要添加标示新产品的参数,而保持了MazeFactory接口的稳定。

不过估计你看到这一堆if、else会感到很不爽(其实就是个switch),我也是。更好的做法是编制一个产品的字典,利用这个字典来索引要产生类型,后面的部分我们将具体介绍这个方法。

动态类型是动态语言的一个基本特性,它能简化类层次、去除不必要的强制类型转换,提高语言表达能力以及自由性。实际上有很多模式都因为静态类型检查的限制造成了不必要的类层次和强制类型转换。比如一个用于遍历包含许多不同类型元素容器的Iterator,就可能要求所有的元素都继承自同一基类以便实现getCurrentItem,而实际上这些不同类型的元素逻辑上并不需要一个公共基类。利用动态类型特性可以使这种模式的实现更加简单灵活。

2.    动态语言不需要一些静态语言中的设计模式

前面提到我们需要编制一个产品的字典,利用这个字典索引要产生类型,这样我们就可以通过类似index['room']的代码来创建产品了。同时我还希望赋予这个MazeFactory动态配置产品类型的能力。用什么模式?Prototype?没错,这的确是运用Prototype的地方,让我们来实现它:


import copy
class MazeFactory:
  def __init__(self):
    self.index = {'maze': Maze(),
      'wall': Wall(),
      'room': Room(),
      'door': Door()}
  def make(self, typename,):
    return copy.deepcopy(self.index[typename])
  def registtype(self, typename, instance):
    self.index[typename] = instance
  def unregisttype(self, typename):
    del self.index[typename]

copy是Python的一个内部模块,其中包含deepcopy函数用于深拷贝对象。registtype、unregisttype函数则用来动态增删产品类型。
似模似样,还不错。不过Python中有没有更好的做法呢?

首先检视一下Prototype的初衷。因为C++并不提供类一级的对象——也就是说类本身不是对象,这就给生成产品以及动态控制产品类型造成了一定的困难,Proto Type模式的出现就是为了解决这个问题。然而像Smalltalk一样,对于Python来说类本身(也包括函数,模块)也是一个对象——type类的对象,可以对对象进行的每样操作都可以使用到类上。利用这点便利,我们不使用Prototype模式就可以实现这个MazeFactory:


class MazeFactory:
  def __init__(self):
    self.index = {'maze': Maze,
      'wall': Wall,
      'room': Room,
      'door': Door}
  def make(self, typename, *args):
    return apply(self.index[typename], args)
  def registtype(self, typename, type):
    self.index[typename] = type
  def unregisttype(self, typename):
    del self.index[typename]

apply是一个内置函数,它接受两个参数一个函数、一个参数list,它的作用是把args作为参数传给类构造函数运行。

现在当我们要添加新的产品类型时,在字典中添加新的索引值对即可。比如我要在MazeFactory中添加一个新产品Trap,只需要在index里加入:'trap': Trap。而动态添加类型时也只需要传入相应的类即可:mf.registtype('trap', Trap)

可以看到我们已经得到了一个静态、动态都比较容易管理产品类型很灵活的工厂。

设计模式中一部分模式的作用就是让程序更灵活,拥有更多的动态特性。而动态语言可能在语言一级就已经支持了这些特性,所以这部分模式也就不需要了。Prototype是一个例子,类似的还有Template Method。实际上因为上面提到的动态类型特性,任何函数都可以接受任意类型的变量做参数,所以Template Method模式也就没有任何价值了。

3.    运行时刻修改类结构来实现Singleton

Singleton是一个非常有用的模式(注3),比如前面的MazeFactory就很有可能需要做成一个单件。让我们来尝试实现这个模式。首先还是按照C++的方法来实现一下:


class Singleton(object):
  instance = None
  def __new__(cls):
    if cls.instance is None:
      cls.instance = object.__new__(cls)
    return cls.instance

类的__new__方法是一个特定的函数,它在对象创建前被执行返回一个cls类的对象。instance则负责保存单件对象。

依循C++的思路,这个程序完全正确,但是看起来缺少一点感觉。让我们查看一下Singleton对象创建的过程,第一次构造对象时创建一个新的对象,以后构造时则不再创建而只是返回第一次创建的结果。

可以看到这是一个典型的运行时刻修改类结构的例子。Python来做自是手到擒来:


class Singleton(object):
  def __new__(cls):
    cls.instance = object.__new__(cls)
    cls.__new__ = cls._new
    return cls.instance
  def _new(type, cls):
    return cls.instance
    _new = classmethod(_new)(注4

classmethod函数的作用是把一个成员方法变成一个类方法。

整个类的代码很简单,第一次进行构造时创建对象,同时用_new函数替换__new__函数,而以后__new__(实际上就是_new)只是返回第一次创建的对象。

能够在运行时刻修改程序结构是动态语言区别于静态语言的一个重要特点,Python中它主要表现在可以动态添加、删除、修改对象(包括类和函数)的方法和变量。这个特性可以用来帮助实现Singleton、Decorator、Strategy等一些要求运行时刻动态改变对象职能的模式。

4.    用Functional Programming简化Command模式

最后要谈到的一点就是Functional Programming。在设计模式里谈到FP可能不很切合。如果我们注意一下会发现GoF《Design Patterns》的副标题是“Elements of Reusable Object-Oriented Software”(可复用面向对象软件的基础)。实际上我们日常谈到的设计模式主要是针对OO Programming来说——因为工业中主要应用的是面向对象编程语言。而FP是区别于OOP的另一种编程风格。

但是Python作为一种面向对象的动态语言,它对FP和OOP的编程都提供了良好的支持。在OO的设计模式中运用一些Functional Programming的思想也会收获一定的好处。下面以Command模式为例讲述这一点。

因为有时候请求操作的对象不知道被请求操作对象的任何信息(打开文档的按钮对象不会知道任何文档对象的信息)。Command模式用以解决这个问题,根本目的在于把被调用操作的对象与实现该操作的对象解耦。而Command模式的实现方式是“把函数层面的任务提升到了类的层面”(注5)。因为FP中函数本身就是第一类对象,所以我们不必再像C++中那样运用继承组合来迂回地实现这一点。FP实现Command模式是非常简单的。


class Button:
  def click(self):pass

这样我们就已经有了一个Button类。看起来什么都没有?别急,先让我们实现一个用于打开文档的Button看看:


opendoc = Button()
opendoc.click = doc.open

现在当opendoc这个按钮被点击时,仅仅需要执行opendoc.click,就等价调用了doc.open这个函数了。这段代码看似简单,但观察下会发现Button和Document已经被很好地解耦,只是FP的实现中间没有那么多迂回,而思路、效果与Command模式是一样的。

再来看看MacroCommand。MacroCommand的目的在于一次执行一系列的Command——也就是函数。有了FP的办法,我们也不必再用Composite模式搞得很复杂,而是利用匿名函数lambda关键字和内置FP函数map实现为:

marcocommand = lambda commands: map(lambda command: command(), commands)

map依次把commands中每个的command传给第一个参数lambda command: command(),而这个参数本身也是一个函数,用来执行传入的command。现在如果我们要实现一个Find & Replace的Button,我们只要这么写:


findreplace = Button()
findreplace.click = lambda: marcocommand([doc.find, doc.replace])

当然Function Programming本身是很复杂的,仅仅在设计模式中一些简单的运用不能充分体现FP的思维方法和能力。但FP作为区别于OOP的编程风格,将其适当用在设计模式中,的确可以优雅简单地实现一些原本复杂的模式。而不同编程风格的尝试和融合也有利于拓宽我们的思路。

结语

通过Python的模式实践,我们可以看到动态语言的种种特性对各种设计模式的实现方式的影响,它们在简化了部分模式实现的同时更带来了较大的灵活性。当然天下没有免费的午餐,动态语言在语言一级提供更大灵活性是以执行效率的下降为代价的,个中的利弊还要广大开发人员去权衡。

本文只是浮光掠影地进行了一些简单的讲述,其中不免有许多疏漏。要想更好地掌握动态语言中设计模式的应用,必须要对各种设计模式以及动态语言的特性有着清晰的了解,并在实践中不断尝试才能做到。

注1: Python中继承自object的class表示一个type,否则是一个classobj。一些特定的操作必须是type类型才能执行,比如super函数要求传入一个type类型的参数、__new__函数也只有type类型才支持。如果需要用到这样操作,则必须让你的class继承自object。

注2: 值得注意的是Python实际上是一种强类型动态类型的语言,而不是弱类型的语言。关于强弱类型的具体解释可以参考这篇文章《Typing: Strong vs. Weak, Static vs. Dynamic》By Aahz。

注3: 实际上由于Singleton模式不能很好地适用于多线程环境,多线程环境中使用更多的可能是Singleton的变种Double-checked locking模式。不过理解了Python中Singleton实现,DCL也是大同小异,所以这里仍然用基本的Singleton模式来解说。

注4: Python2.4中提供新的语法Decorators for Functions and Methods来使类似操作更容易的实现。具体的语法和相关说明见PEP 318

注5: 语自《Agile Software Development》By Robert Martin

2004-11-25左右成稿

话说我这些年在学校里都干了啥

大抵就是俩礼拜能弄完的玩意,陆陆续续搞上好几年,最终结果还拿不出手。当然作为“学术研究”,你必须得算上那些副产品的garbage papers……

其实这也是代码整理开源计划的一部分,虽然这部分代码属于爹不疼娘不爱那种(也不全是我写的,不过估计就我这留着底),= =|||……

链接:源代码Github)、试用装iMashup,js没package到一起,首次加载巨慢,耐心)、视频(Youtube优酷)。哦,差点忘了说,这是一个长得像webos的mashup工具(别问为神马)……

你们为什么不踢踢球?

话说我07年底08年初的时候,一心觉得这世界上所有的活动都该被SNS装起来,加之那会儿博士读到天天踢球。所以动心思写了个网站,目的是帮着大家组球队、约训练、约比赛、一起看球等等。

当时的野心其实比上面说的大些,想至少把校内足球这部分吃下去。可惜几个工科宅男又能干啥?上线没几个月,随着博士各种事情如山倒来,很快也就放下了。

最近整理代码时又给翻出来。现在看08年初写的东西(iphone开卖不久,fb plateform还不为人知,gae还得申请),自己也嫌界面土气、文案简直是一坨、功能设计复杂到自己都忘了怎么用…… 不过决定还是给架起来,至少等明年春暖花开,想踢球的时候还能用不是。不被运行的代码也是会哭泣的!

最后感谢雷指赞助的域名,www.wefootball.org

又及:这网站本是开源计划的一部分。只是当时后端也有马博三分之一的工作量在那,商量一下才好。但马博最近又忙得见不到人,于是非得等等才有下文了。

以下截图杀猫: