如何写出优美的代码(四)

(本文思想基本来自于经典著作《重构》一书)

上一篇 http://www.cnblogs.com/ceys/archive/2012/03/09/2388356.html

上篇《希望这不是年轻人的蠢话》惹了很多争议,有人踩,有人顶。我说话确实做不到四平八稳,更说不出什么心灵鸡汤。如果要比喻可能有点像辣椒酱,不管你喜不喜欢,这玩意可以帮助下饭。不多说,继续把代码风格这系列写完。

三、简化条件表达式和函数调用

条件语句要尽可能简单,分别用独立函数表示它们。

如果条件表达式根据对象类型不同而选择不同的行为,将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。

将查询和修改函数分离。在多线程系统中,赋值前通常需要完成检查。这个时候仍把查询修改分开,在建立一个新函数,调用各自独立的查询和修改函数,并声明为synchronized。如果未被声明为synchronized,应该将她们的可见范围限制在包级别或private级别。

尽量用函数,对象取代参数。比如常见的数值范围参数,可以用对象取代:

class DateRange {
	DateRange(Date start, Date end) {
		_start = start;
		_end = end;
	}
	Date getStart() {
		return _start;
	}
	Date getEnd() {
		return _end;
	}
	private final Date _start;
	private final Date _end;
}

如果在创建对象时不仅仅是做简单的建构动作,将构造函数替换为工厂函数。这样在派生子类的过程中,可以以工厂函数取代类型码。例如:

static Employee create(String name) {
	try{
		return (Employee) Class.forName(name).newInstance();
	} catch (Exception e) {
		throw new IllegalArgumentException("Unable to instantiate" + name);
	}
}

当调用者可以预先检查,不要用异常,而是做条件判断。

四、处理概况关系

当两个子类有相同的字段,或是有产生完全相同结果的函数,或是本体几乎一致的构造函数,将它们移至超类。

当若干客户使用类接口中的同一子集,或者两个类的接口有部分相同,将相同的子集提炼到一个独立接口中。类之间彼此互用的方式有若干种:用到某类的所有责任区;只是用类责任区中的一个特定子集;需要与所有协助处理某些特定请求的类合作。对后两种情况,将真正用到的这部分责任分离通常很有意义。在java中可以利用接口实现上述需求。例如,TimeSheet类表示员工为客户工作的时间表,从中可以计算客户应该支付的费用,为了计算费用,TimeSheet需要知道员工级别即是否有特殊技能:

double charge(Employee emp, int days) {
	int base - emp.getRate() * days
	if (emp.hasSpecialSkill())
		return base * 1.05;
	else return base;
}

我们可以针对员工技能级别定义一个接口:

interface Billable {
	public int getRate():
	public boolean hasSpecialSkill();
}

class Employee implements Billable ...

double charge(Billable emp, int days) {
	int base = emp.getRate() * days;
	if (emp.hasSpecialSkill())
		return base * 1.05;
	else return base;
}

如有一些子类,其中相应的某些函数以相同顺序执行类似的操作,将这些操作分别放进独立函数中,并保持他们都有相同的签名,于是原函数就变得相同了。然后将原函数上移至超类。

posted @ 2012-04-30 23:56 大俗人 阅读(1998) 评论(0) 编辑

数据库-并发控制

当多个事务在数据库中并发执行时,数据的一致性可能受到破坏。系统有必要控制各事务之间的相互作用,这是通过并发控制机制的多种机制中的一种来实现的。

避免事务"饿死",授权加锁的条件:不存在在数据项Q上持有与M型锁冲突的锁的其他事务;不存在等待对数据项Q加锁且先于Ti申请加锁的事务。

常用的机制是各种封锁协议,时间戳排序,有效性检查,多版本机制。

封锁协议是一组阐明了事务合适对数据库中的数据项加锁解锁的规则。两阶段封锁协议仅在一个事务未曾释放任何数据项时允许该事务封锁新数据项。该协议保证可串行性,但不能避免死锁。在缺少有关数据项存取方式的信息是,两阶段封锁协议对保证可串行化来说不仅是必要的而且是充分的。

树形协议规则:1、Ti的首次加锁可以对任何数据项进行。2、此后Ti对数据项Q加锁的前提是Ti持有Q的父项上的锁。3、对数据项解锁可以随时进行。4、数据项被Ti加锁并解锁后,Ti不能再对该数据项加锁。

时间戳排序机制通过事先在每对事务之间选择一个顺序来保证可串行性。系统中的每个事务对应一个唯一的固定的时间戳。事务的时间戳决定了事务的可串行化顺序。这样,如果事务Ti的时间戳小于事务Tj时间戳,则该机制保证产生的调度等价于事务Ti出现在事务Tj之前的一个串行调度。该机制通过回滚违反该次序的事务来保证这一点。

Thomas写规则:假设事务Ti发出write(Q)操作:

1、若TS(Ti)<R-timestamp(Q),则Ti产生的Q值是先前所需要的值,但系统已假定该值不会被产生。因此,write操作被拒绝,Ti回滚。

2、若TS(Ti)<W-timestamp(Q),则Ti试图写入的Q值已过时。因此,这个write操作可悲忽略。

3、其它情况是执行write操作,将W-timestamp(Q)视为TS(Ti)。

在大部分事务是只读事务,这样事务见冲突频度较低的情形下,有效性检查机制是一个适当的并发控制机制。系统中的每个事务对应一个唯一的固定的时间戳,串行性次序是由事务的时间戳决定的。在该机制中,事务不会被延迟。不过,事务要完成必须通过有效性检查,如果事务未通过有效性检查,则盖世五回滚到初始状态。

某些情况下把多个数据项聚为一组,将它们作为聚集数据项来处理效果可能更好,这就导致了多级粒度。小数据项嵌套于大数据项之中。这种层次结果可以图形化地表示为树。封锁按从根结点到叶结点的顺序进行,解锁则按从叶结点到根结点的顺序进行。

多版本并发控制机制基于每个事务写数据项时为该数据项创建一个新版本。读操作发出时,系统选择其中的一个版本进行读取。利用时间戳,并发控制机制保证确保可串行性的方式选取要读取的版本。

多版本最常用的技术是时间戳。对于系统中的每个事务Ti,我们将一个静态的唯一的时间戳与之关联,即为TS(Ti)。对于每个数据项Q,有一个版本序列<Q1,Q2,…Qm>与之关联。

防止死锁的一种方法是使用抢占与事务回滚;另一种方法是死锁检测与恢复机制。系统处于死锁状态当且仅当等待图中包含环。

posted @ 2012-03-16 18:07 大俗人 阅读(1507) 评论(2) 编辑

如何写出优美的代码(三)

(该文思想来自于经典著作《编程珠玑》)

 

看到有朋友评论说,美的代码不仅仅是规范上面的事。规范的代码可以让我们减少Debug的难度,增加可扩展性。当遇到性能问题的时候,我们就需要改进算法了。

编程珠玑的开篇提到了一个给最多一千万个7位电话号码排序的问题。首先我们看这个问题的常规解决方案:

C版本:

int intcomp(int *x,int *y){
	return *x - *y
}
int a[10000000]
int main(void){
	int i,n = 0;
	while (scanf("%d", &a[n] != EOF)
		n++;
	qsort(a, n, sizeof(int), intcomp);
	for (i = 0; i < n; i++)
		printf("%d\n", a[i]);
	return 0;
}

C++版本:

int main(void){
	set<int> S;
	int i;
	set<int>::iterator j;
	while (cin >> i)
		S.insert(i);
	for (j = S.begin(); j != S.end(); ++j)
		cout << *j << "\n";
	return 0;
}

但是,条件有限制:可用内存只有1MB。我们看到这么小的内存,直接想法是在磁盘上归并排序把。但是,进一步想,如果用32位整数存储电话号码,可以存250000个号码。这样,可以把号码按大小顺序分成40段,依次放到内存里快排。

再进一步分析该问题的特殊性:1)电话号码没有重复,2)数字小于1000万,3)需要排序的仅仅是电话号码,没有其他关联数据。由此想到,可以用一个1000万个位的字符串表示这个文件,当且仅当整数i在文件中存在时,第i位为1。还可以用位图表示集合,上代码:

//32位整数
#define BITSPERWORD 32 
#define SHIFT 5
#define MASK 0x1F
#define N 10000000
int a[1 + N/BITSPERWORD];

void set(int i) { a[i>>SHIFT] |= (1<<(i & MASK)); }
void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int test(int i) { return a[i>>SHIFT] & (i>>(i & MASK)); }

int main(void) {
	int i;
	for (i = 0; i< N; i++)
		clr(i);
	while (scanf("%d", &i) != EOF)
		set(i);
	for (i = 0; i < N; i++)
		if (test(i))
			printf("%d\n", i);
	return 0;
}

这个例子主要证明了正确理解问题的重要性。明确理解问题,针对特定条件去思考,是写出优雅算法的第一步。这里用的位图的技术非常简洁,原作者引用的一句话给我很大启发:“设计者确定其设计达到了完美的标准不是不能再增加任何东西,而是不能再减少任何东西。”

posted @ 2012-03-14 01:14 大俗人 阅读(1972) 评论(4) 编辑

如何写出优美的代码(二)

(本文思想基本来自于经典著作《重构》一书)

上一篇 http://www.cnblogs.com/ceys/archive/2012/03/05/2379842.html#commentform

 

 

上一篇文章主要讲了怎么给函数整容。现在我们大家基本上都使用面向对象语言,什么样的“对象”才是优美的呢?

类中的函数、字段应该和该类最紧密相关,如果和另一个类有更多交互,搬移它。搬移字段时,如果很多函数引用了该字段,可以把该字段用get/set方法自我封装起来。这样搬移时只需要修改get/set访问函数。

一个类应该是一个清楚的抽象,处理一些明确的责任。否则建立一个新类,用以表现从旧类中分离出来的责任。提炼类是改善并发程序的一种常用技术,因为提炼后可以给两个类分别加锁。需要注意,提炼出新类,要考虑要不要对用户公开这个新类。如果允许任何用户修改其对象的任何部分,它就成为引用对象;如果不允许任何人不通过对象A修改它的任何部分,则可以将其设为不可修改的,或为它提供不可修改的接口,将A中所有与其相关的函数委托之,从而完全隐藏这个类。

每个对象应该尽可能少了解系统的其它部分,当客户通过一个委托类调用另一个对象,在服务类上建立客户需要的所有函数,隐藏委托关系,减少耦合。然而,如果受托类的功能越来越多,服务类就完全变成了一个中介。很难说隐藏到什么程度是合适的。

当需要为服务类提供一些额外函数,但无法修改这个类时,建立一个新类,使它包含这些额外函数,成为源类的子类或包装类。如果函数较少,直接引入外加函数即可。

子类的工作量比较少,但它有两个问题:第一,必须在对象创建期实施,如果对象创建之后,就不能用了;第二,子类化会产生一个子类对象,如果有其它对象引用了旧对象,就同时又两个对象保存了原数据,如果原数据允许被修改,一个修改动作无法同时改变两份副本。这个时候就要用包装类。包装类需要为原始类的所有函数提供委托函数。 

 

二、重新组织数据

 

面向对象语言有一个有用的特征:允许定义新类型。这样我们可以利用对象,组织数据。

如果有一个数据项,需要与其他数据和行为一起使用才有意义,将数据项变为对象。

如果一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象,将这个值对象变成引用对象。每个引用对象都代表真实世界中的一个事物,可以以==检查两个对象是否相等。值对象由其所含数值定义,不在意副本存在。这里举个简单例子:

值对象:

class Customer {
	public Customer(String name) {
		_name = name;
	}
	public String getName() {
		return _name;
	}
	private final String _name;
}
class Order {
	public Order(String customerName) {
		_customer = new Customer(customerName);
	}
	//set,get...
	private Customer _customer;
	//other function use Customer...
}

这里,就算多分订单属于同一客户,每个Order对象还是拥有各自的Customer对象。现在把它改成引用对象,即每个客户名称只对应一个Customer对象:

class Customer {
	public static Customer getNamed (String name) {
		return (Customer) _instances.get(name);
	}
	private Customer (String name) {
		_name = name;
	}
	private static Dictionary _instances = new Hashtable();
	static void loadCustomers() {
		new Customer("Gang Li").store();
	}
	private void store() {
		_instances.put(this.getName(), this);
	}
	//...
}
class Order {
	public Order (String customer) {
		_customer = Customer.getNamed(customer);
	}
	//...
}	

我们首先创建工厂函数,以控制Customer对象的创建过程。然后,需要决定如何访问Customer对象,可以利用另一个对象访问,但这里Order类没有一个明显的字段可用于访问,所以可以创建一个对象保存所有Customer对象。这里为了简化,利用Customer里的静态字典保存。让Customer类作为访问点。然后,决定何时创建Customer对象。这里为了简化,在loadCustomer里预先把需要使用的Customer对象创建好。

引用对象可能造成内存区域之间错综复杂的关联。在分布系统和并发系统中,不可变的值对象特别有用,因为无需考虑他们的同步问题。这里的不可变指的是对象自身不能改变,比如money对象。当判断相等时需覆写equals()和hashCode()。(实现hashCode的简单办法是把equals()使用的所有字段安慰异或操作)。

如果两个类都需要使用对方特性,但只有一条单向连接,添加一个反向指针,并使修改函数能同时更新两条连接。以上面的代码为例:

class Customer {
	//客户拥有多份订单
	private Set _orders = new HashSet();
	//添加只包内可见的辅助函数,让Order可以直接访问_orders集合
	Set friendOrders() {
		return _orders;
	}
	//如果需要在Customer中也能修改连接,就让它调用控制函数
	void addOrder(Order arg) {
		arg.setCustomer(this);
	}
class Order...
	void setCustomer(Customer arg) {
		if (_customer != null)
		_customer.friendOrders().remove(this);
		_customer = arg;
		if (_customer != null)
		_customer.friendOrders().add(this);
	}	

如果一个数组,其元素各自代表不同的东西,以对象替换数组

以字面常量取代魔法数

如果有个函数返回一个集合,让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。如果函数返回集合自身,会让用户在拥有者不知情的情况下修改集合修改集合。所以不该为集合提供设置函数。但可以提供为集合增减元素的函数。例如:

class Person{
	//封装集合操作
	public void addCourse (Course arg) {
		_courses.add(arg);
	}
	public void removeCourse (Course arg) {
		_courses.remove(arg);
	}
	public void initializeCourses(Set arg) {
		Assert.isTrue(_courses.isEmpty());
		Iterator iter = arg.iterator();
		while (iter.hasNext()) {
			addCourse((Course) iter.next());
		}
	}
	//确保没有用户通过取值函数修改集合
	public Set getCourses() {
		return Collections.unmodifiableSet(_courses);
	}
	private Set _courses = new HashSet();
}
posted @ 2012-03-09 22:19 大俗人 阅读(1992) 评论(0) 编辑

如何写出优美的代码(一)

(本文思想基本来自于经典著作《重构》一书)

 

我愿意把代码想象成女性,我希望在我面前的是一个美女,受不了丑陋的那部分。

优美的代码可理解性高,修改成本低。不过优美的代码是不容易一次写出的。我的代码刚刚写完就是一坨屎。

比如有很多重复代码,同一个类的两个函数含有相同的表达式。

函数过长,以至于需要注释来解释区分。函数的参数超过3个。这些参数总是在一起出现,却没有为它们产生一个新的对象。

当有不同原因的改动时,有的类需要在不同方向上变化;当出于一个原因改动时,却有不同的小类需要做出小修改

某些类中的函数拥有很多其它类中的数据

……

不能忍了!还好,我院引进了先进的“整容技术”。让我们一起来见识一下:


一、重新组织函数


1、提炼函数

这里有几个要点:

a)创建一个函数,根据它“做什么”命名;

b)我比较喜欢让每个函数都只返回一个值,返回的临时变量在函数里命名一致,return result;

2、以查询取代临时变量

这样同一个类中的所有函数都可以获得这份信息:

a)找出只被赋值一次的临时变量,声明为final,编译测试,通过将其提炼为独立函数;

3、引入解释性变量

用于当局部变量时提炼函数难以进行时,尤其适用于条件逻辑。

4、分解临时变量

不要让临时变量承担一个以上的责任,每次复制,创造独立的临时变量,声明为final。

5、在java中,不要对参数赋值,建立一个临时变量,把待处理的参数赋予它:

java参数都是按值传递,给参数重新赋值,不会改变被传递对象的状态。可以修改函数对象,但重新赋值没有意义。

6、以函数对象取代函数

建立一个新类,在其中建立一个final字段,用以保存源对象,同时建立对应原临时变量的字段;

建立构造函数,接受源对象及原函数的所有参数作为参数;

这样由于所有局部变量现在都成了字段,所以可以任意分解这个大函数,不必传递任何参数。

7、替换算法

posted @ 2012-03-05 01:22 大俗人 阅读(2986) 评论(10) 编辑

数据库-查询优化器

摘要: 数据库都会对sql做查询优化处理,这个过程是怎样的呢?首先,查询优化器使用等价规则系统的产生与给定表达式等价的表达式。概念上,只要表达式中有任何则表达式与等价规则的某一边想匹配就产生一个新的表达式。为了减少空间需求,大多优化器都会让有公共子表达式的表达式指向共享子表达式。此外,若把执行代价估计考虑进去,可以避免检查某些表达式。关系代数的等价规则见:http://jsjedu.hxu.edu.cn/sjkyl/4/4.2.4.htm产生表达式后,需要定义每个运算使用什么算法以及如何协调各运算执行。选择执行计划的方法之一是简单的为每个运算选择一个代价最小的算法。但是,考虑到总体,例如尽管在给定层次阅读全文
posted @ 2012-02-15 10:45 大俗人 阅读(73) 评论(0) 编辑

数据库-表达式计算

摘要: 象这样一个sql:select * from account r where balance<2500 join customer s on r.customer-name=s.name一个表达式中包含多个运算,该怎样计算呢?一种方法是以一定的顺序每次执行一个操作,每次计算的结果被实体化到一个临时关系中以备后用。实体化计算的代价包括所有运算的代价和把中间结果写回磁盘的代价。其中磁盘I/O的代价很高。另一种方法是在流水线上同时执行多个运算,一个运算结果传递给下一个,而不必保存到临时关系中。如例所示的连接运算,其左端输入来自流水线,由于是流水线输入,处理连接运算所需的输入不能一下子全部获得。阅读全文
posted @ 2012-02-14 17:31 大俗人 阅读(61) 评论(0) 编辑

数据库-连接运算

摘要: 本博客会陆续写一些和操作数据有关的基本算法。内容都很基础,算是帮助大家回顾记忆。也可以给和我一样,刚接触数据库,数据挖掘等技术的同学,提供一个迅速了解基本算法的文档。我认为多多体会基本算法,不光是为了编程、性能优化,还可以学习到很多分析解决问题的方法。好了,不多废话,欢迎大家来评论;如文中有错误,也欢迎大家来拍砖哈~我们查询数据时经常会用到联合查询select r,s from R join S on R.id=S.rid这个连接是怎么运算的呢?代价有多大呢?我们来分析一下。先来看下运算结果集的大小:若R∩S=空,则连接为笛卡尔积。 若R∩S是R的码,可知s的一个元组至多与r的一个元组连接。因阅读全文
posted @ 2012-02-10 16:50 大俗人 阅读(1092) 评论(0) 编辑
仅列出标题  

公告

统计