This is a temporary post that was not deleted. Please delete this manually. (26189ddd-10cd-432b-9693-db8002a92aac – 3bfe001a-32de-4114-a6b4-4005b770f6d7)
面试时见过很多人,开始的时候侃侃而谈,无所不知,架构,设计,一套一套的。漏洞当然有,一旦深入就有发觉他们有力不从心的感觉。很多领域我不懂,无法辨别,于是从实际的程序着手。程序都不复杂,没有高深的算法,只是让解决几个常见的问题,很多人一写程序就露馅了。
其实编程是个细致的活,好的程序会处处体现出一种气质,无处不在,从设计抉择,到具体的每一行。看过大师的作品,对比身边现实中的代码,感觉区别相当大,粗略看过,便高下立分。很多的差距体现在细节上,细节之处见真章,细节之处体现作者的修养,思考深度,习惯以及态度。看一个人的气质,全在举手投足间,自然流露。好的程序也如此,大师们以严谨的态度雕琢代码久了,之后即使是随意而为,也会很自然地体现出水平。
现实当中,程序员很多都以完成任务完成功能为准,并不考虑程序质量的其他因素。有其在敏捷盛行时意敏捷作为借口,草草完工,美其名曰 ‘快速开发,只做必须的’。这其实是一个借口,背后仅仅因为懒惰,不求甚解,不负责任等思想。多花点时间思考,更认真的对待最终会带来更多的效益,更高的生产率,而不是拖慢进度。
举个例子,下面这段程序(这不是一个菜鸟写的),我不觉得这是很草率的结果,毕竟还是想过了的,但还不够:
1: for (int start = 0; start < size; start += page) {
2: int remainSize = size - start;
3: int msgSize = page > remainSize ? remainSize : page;
4: int end = start + msgSize;
5:
6: do_range(start, end);
7: }
我花了一小段时间弄明白他要干什么, 但我仔细一想,他要干的难道不就是下面的这个么?
1: for (int start = 0; start < size; start += page) {
2: do_range(start, Math.min(start + page, size));
3: }
是的, 多思考一点, 就能得到更简单更直接的表达,不仅节省了自己将来遇到同样问题时的时间, 也节省了别人的揣摩你程序意图的 时间。这就是细节。
好吧,我不会把Java 贬得一无是处,毕竟它还是最受欢迎的编程语言之一。然而,我还是觉得,它有许多让我反感的地方,而它的成功我总觉得是因为命好。真的命好。
在所有我不喜欢的Java特性中,checked exception 排名靠前。对我来说,它带来的麻烦远比好处要多。它某种程度上鼓励不过脑子的编程习惯,并由此带来满篇糟糕混乱的代码。我喜欢使用异常,但不是Java的方式。
我最近做项目数据访问层的设计,姑且称之为DAO, 为了避免在程序中到处使用硬编码的SQL语句,我将对数据库的访问定义为接口,例如:
1: @DAO
2: public interface User {
3: @SQL("SELECT name FROM user WHERE id=:id")
4: String getName(@SQLParam("id") long id) throws SQLException ;
5:
6: // other methods.
7: }
使用时很简单: User user = DAOFacade.getDAO(User.class); user.getName(1000000);
很好,但我在声明异常的时候有些犹豫。 我们的服务器使用apache thrift ,所有的服务器方法都强制声明为 TException, 也就是说,你的DAO 方法调用要么吃掉这个SQLException, 要么把它转为TException 再抛出去,由于Java 语言限制,你是无法把SQLException直接抛出去的。根据我的经验,90% 的情况下,人们是不会对这种异常做任何有意义的处理的,但通篇的无意义的 try/catch 确实把原本干净清晰的逻辑搞得一团糟糕。有人说,直接把上面的DAO 声明为TException 不就可以了吗?没错,但 TException 是thrift 相关的,和 DAO 一毛钱关系也没有,放在这里就不伦不类。还有一种做法就是不声明任何异常,而用一个RuntimeException 这种un-checked exception 代替,但这依然会隐藏问题, 而且可能造成某种情况下的线程崩溃。 这是一种无论怎么设计都很恶心的情况,我们要选择接受其中一种恶心。
由于 DAO 接口的定义有使用者定义,我的选择就是不做强制定义。如果你的声明中没有 Exception, 我会认为你想把它吃掉,那 我会帮你吃掉异常,返回默认值,如果你声明 SQLException, 我帮你抛出,因为你可能需要对其做某些处理,比如重试。如果你声明 TException, 我认为你不想处理, 帮你转了吧。我所有的依据就是,使用者应该知道自己的行为带来的效果。
项目组其他两位成员对此有不同看法,认为使用者应该强制声明并处理异常,因为这是Java 的方式。我的方式没有办法防傻,而在实际使用中也确实有一些误用的,比如很多人不声明异常,但不清楚他的后果。在争论过后,我仔细想想,我确实犯了一个错误就是,假设程序员都是清醒的,我基于对程序员的信任而由程序员自己做决定。但我忽视了Java 语言成功的一个原因就是对程序员不信任,强制你做它认为正确但实际上未必的事。在这种语言下,人也会被带坏了。比如强制处理异常。
我接受了他们的提议,我把所有的 DAO都改为强制使用我自己定义的异常后,整个代码编译不过,于是team 花了一天的时间来修改,避免编译错误,以及思考对异常该如何处理。但最后的结果是,大家只是简单的把它们 catch ,然后转成 TException 抛出,没做任何有意义的事情。这完全符合我的预期。
这一切都是因为这恶心的 checked exception. 如果没有这东西,就不会有这些问题。7年前我做某框架设计时就痛恨过它,所以后来在微软学C#发现没有这玩意时颇感欣慰。谁知道现在又被它折腾了。
下篇再分析分析它有哪些不好的地方。
Andrew Koenig (C++ Guru) 发表了几篇文章 Elegance or Trickery? 根据几个简单的 C++ 的代码,让大家评估代码到底是 Elegance (优雅) 还是Trickery (我姑且叫 晦涩). 他对优雅的认定是,对某个具体问题,使用比较少的代价达到更多的效果的有效解决方案。我还是比较认可这个定义的,针对代码而言,这个较少的代价当然要包括对实现者以及对使用者(or 阅读者)的较少代价。比如你实现一个程序库,你倒是可以花较少的代价来实现一些功能,但要求使用者要花许多功夫才能用好它,这显然不能说是一个优雅的方案。好,回到他文章中的例子。比如这个:
*p++ = *q++;
对于多数C/C++ 程序员来说,显然要比下面的要优雅简洁,而且不费脑筋就知道怎么回事,
*p = *q; ++p; ++q;
但对于不熟悉C/C++的人,第一种理解起来要困难一些。作者还举了几个其他稍微复杂的例子,其中利用到了C++ 的某些语言特性,达到了比原始方案更加简单的效果。但是由于不是所有人都懂得这些语言特性,对这些人来说就比较难懂。他邀请读者对几种方案作出评价,那种更优雅,那种更晦涩。
很多读者觉得那些作者以为优雅的方案不容易理解,不很显而易见, 于是作者写了另一篇文章 Readability and Familiarity, 说:
Looking at the comments on my recent Elegance or Trickery? articles, I am struck by how many commenters have made what seem to be objective statements. For example, one reader said, "Trickery is when you game the language to achieve unobvious effects."
My immediate reaction to this statement is to ask: Unobvious to whom?
我深以为然,是否对一个问题有优雅的解决方案取决于你的问题域的理解深度。我当然承认优雅的方案不仅仅是短,它的 readability 也一定要好,你读起来不舒服的程序没有优雅可言。但是 readability 是个和水平有关度量,所以,你的程序需要对谁来说是显而易见和可读性好的?你不能说一个菜鸟看不懂的程序是不优雅的吧?你也不能说因为一个设计的抽象程度是你的理解能力达不到的而说这个设计很烂吧?你不能说因为你不熟悉这个东西就是它的可读性太差吧?所以,不要拿这个当作借口。
现实中有很多这样的例子,因为对某技术不懂,不熟悉,就本能反感,抵制对这技术的使用;而不是评估它所带来的好处/坏处,使用它所花费的成本等。文章中Andrew 说了这句话:
If I’m correct in believing that trickery is closely related to unfamiliarity, then this belief exposes a problem: Useful programming techniques may be rejected because they are unfamiliar, and may be unfamiliar because they are rejected.
好吧,我知道这个世界上顶尖高手实在不多,例如C++ 也只有少数人懂模板元编程的,但那种技术的目标对象也不是普通人,在那个领域,自然有它对应的优雅,可读性和熟悉程度的评判。对于多数应用层面的程序来说,使用能让中高级程序员理解的技术就可以了,然后使用优雅的方案吧。
举另一个例子吧,对文件中如下的数据, 每行用 tab 隔开两列, 取出,对右边的一列排序,然后对左边一列排序,然后保存:
1 a
1 a
2 b
1 b
3 a
1 a
2 a
4 c
6 c
6 b
0 b
1 c
你怎么做?
C#版:
var sorted = from s in File.ReadAllLines("input")
let a = s.Split(‘\t’)
orderby a[1], a[0]
select s;
File.WriteAllLines("output", sorted.ToArray());
什么?看不懂?
作者谈到了Ruby 的各个特性设计的原因,从书中看,Ruby是属于比较优雅的一种语言,写起来和读起来都应该非常舒服。符合我的审美。但对于语言的设计演化等,不及当初看《C++语言设计和演化》一书有感触。
作者还谈了些软件设计的其他方面,包括设计模式,Ajax等等,谈的不深,多数属于蜻蜓点水,但行文还是很流畅的,可作枕边读物。里面关于软件设计思想,多属于常识,编程经验不多者,学之有益。
自己弄了个VPS,搞了个博客,需要把原来在博客园(cnblogs)下的一些文章迁到这里,网上没搜到好用的工具,后来发现有个 .NET 的 xml-rpc 库(http://www.xml-rpc.net/), 根据 wordpress 支持的协议 http://codex.wordpress.org/XML-RPC_Support,写了个简单的程序。博客园能够把你的博客备份成一个 rss 文件,不保存评论,不过也凑合了。
这个XML-RPC .NET 库还是比较好用的,只需要定义一个协议的interface, 它会自动生成一个proxy 类,有点像我当年写过的一个 mock web service framework。
程序很简单,首先定义协议接口,这里我只需要 new post, 然后定义一个 blog info,存放博客数据。一个简单的函数解析 cnblogs 的 rss, 一个函数用来发布 blog.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml.XPath;
using CookComputing.XmlRpc;
namespace PortWordPress
{
public interface IBlog : IXmlRpcProxy
{
[CookComputing.XmlRpc.XmlRpcMethod("metaWeblog.newPost")]
string NewPost(int blogId, string strUserName, string strPassword, BlogInfo content, bool publish);
}
public class BlogInfo
{
public string title;
public string author;
public DateTime dateCreated;
public string description;
}
class Program
{
public static void Main(string[] args)
{
PortToWordPress(LoadInfos(@"D:\cnblogs.xml"), "http://yourblog.com/xmlrpc.php");
}
public static IEnumerable<BlogInfo> LoadInfos(string fileName) {
foreach(var item in XElement.Load(fileName).XPathSelectElements("//channel/item")) {
yield return new BlogInfo {
title = (string) item.Element("title"),
author = (string)item.Element("author"),
description = (string)item.Element("description"),
dateCreated = (DateTime)item.Element("pubDate") };
}
}
public static void PortToWordPress(IEnumerable<BlogInfo> infos, string url) {
IBlog blog = XmlRpcProxyGen.Create<IBlog>();
blog.Url = url;
foreach (var info in infos) {
blog.NewPost(0, "your name", "your password", info, true);
}
}
}
}
组里有一个抽手机的活动,有M个人,N台手机 (M>N), 从M个人里随机抽出N个,得到手机。之前用Java 写过,最近刚开始学习Erlang, 就拿它来练手。
几经修改,程序如下,很简单。
-module(test).
-export([start/0]).
-define(COUNT, 4).
-define(NAMES, [
"User1","User2", .. “userM”
]).
start() ->
random:seed(now()),
NewList = [{ N, random:uniform(1000000)} || N <- ?NAMES ],
Sorted = lists:sort(fun (L, R) -> element(2, L) < element(2, R) end, NewList),
lists:foldl(fun ({Name, _}, N) -> io:format("~s: ~wn", [Name, (N =< ?COUNT)]), N+1 end,
1, Sorted).
C# 版的也很简单:
using System;
using System.Linq;
class Program {
public static void Main(string[] args) {
string[] names = {
"User1", "User2", .. “UserM”
};
int count = 4;
Random r = new Random ();
var sorted = names.Select(n => new { Name = n, Rand = r.Next()}).OrderBy(n => n.Rand).ToArray();
for(int i = 0; i < sorted.Length; ++i) {
Console.WriteLine("{0}: {1}", sorted[i].Name, i < count);
}
}
}
同样的算法,Java 版的就复杂多了,就不贴出来了。
感觉:Erlang 的函数编程和List 的高阶函数还是比较好用的,语法上需要一段时间适应。C# 上提供 lambda 表达式和Extension method, 写的程序简单清晰。Java 就太低级了。
