- 通常来说,基类对派生类应该一无所知
- 一般情况下,我们把基类和派生类部署到不同的jar文件中,修改组件时,不必重新部署基组件
例外:有限状态机的实现,这种情况下派生类和基类紧密耦合,在补一个jar文件中部署
通过限制信息来控制耦合度:
- 限制类或模块中暴露的接口数量,类中方法越少越好,函数知道的变量越少越好,类拥有的实体变量越少越好
- 隐藏你的数据:工具函数、常量、临时变量
- 不要为子类创建大量受保护的变量和函数(protected修饰)
死代码就是不执行的代码,应该从系统中删除
- 不会发生的条件的if语句体
- 不会发生的switch/case条件
- 不会抛出异常的try catch代码块
- 不会被调用的小工具方法
变量和函数应该靠近被使用的地方定义
- 本地变量应该正好在其首次被使用的位置的上面声明
- 私有函数应该刚好在其首次被使用的位置下面定义
前后一致,能让代码更加易于阅读和修改
def func1():
response = HttpServletRespnse()
def func2():
response = HttpServletRespnse()
- 没有实现额默认构造函数
- 没有用到的变量
- 从不调用的函数
- 没有信息量的注释
这些都是应该移除的废物
人为耦合是指两个没有直接目的模块之间的耦合,根源是将变量、常量、函数不恰当放在临时方便的位置,这是偷懒行为,花点时间研究应该在什么地方声明函数、常量、变量。
-
类的方法只应该对其所属类中的变量和函数感兴趣
-
当方法通过某个其他对象的访问器和修改器来操作该对象内部数据,则它就依恋与该对象所属类的范围
-
我们要消除特性依恋,因为它将一个类的内部情形暴露给了另外一个类
-
特殊情况:
public class HourlyEmployeeReport { private HourlyEmployee employee; public HourlyEmployeeReport(HourlyEmployee a) { this.employee = e; } // reportHours方法依恋HourlyEmployee类 // 将格式化字符串移到HourlyEmployee会破坏好几种面向对象设计原则 String reportHours() { return String.format("Name: %s\tHours:%d.%ld\n", employee.getName(), employee.getTenthsWorked()/10, employee.getTenthsWorked()%10); } }
选择算子参数,只是把大函数切分为多个小函数的偷懒做法
- bool类型参数
- 枚举元素
- 整数或任何一种用于选择函数行为的参数
使用多个函数,通常优于向单个函数传递某些代码来选择函数行为
-
代码要尽可能具有表达力
-
联排表达式、匈牙利语标记法、魔术数字都遮蔽了作者的意图
// 意图不可捉摸 public int m_otCal() { return iThsWkd * iThsRte * (int) Math.round(0.5 * iThsRte * Math.max(0, iThsWkd - 400) ); }
代码应该放在读者自然未然期待它所在的地方
- PI常量应该出现在声明三角函数的地方
- 通常应该倾向于使用非静态方法
- 如果的确需要静态函数,确保没有机会打算让它有多态行为
让程序可读的最有力方法之一就是将计算过程打散成一系列良好命名的中间值
// 第一个匹配组是key,第二个匹配组是value
Matcher match = headerPattern.matcher(line);
if (match.find()) {
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value)
}
// 从函数调用中看不出函数的行为,是添加5天、5个小时还是5个星期?
Date newDate = date.add(5);
//
Date newDate = date.addDaysTo(5)
Date newDate = date.increaseByDays(5)
如果你必须查看函数实现或文档才知道它是做什么的,就该换个更好的函数名,或重新安排功能代码,放到有较好名称的函数中。
- 认为自己完成某个函数之前,确认自己理解了它是怎么工作的,而不仅仅是通过测试用例,你应该知道解决方案是正确的
- 可以通过重构函数,来理解函数是如何工作
- 依赖者模块不应该对被依赖着模块有假定,应当明确询问后者全部信息
// HourlyReporter假定知道HourlyReportFormatter的页面尺寸,这类假定是一种逻辑依赖
public class HourlyReporter {
private HourlyReportFormatter formatter;
private List<LineItem> page;
private final int PAGE_SIZE= 55;
public HourlyReporter(HourlyReportFormatter formatter) {
this.formatter = formatter;
page = new ArrayList<LineItem>();
}
public void generateReport(List<HourlyEmployee> employees) {
for (HourlyEmployee e : employees) {
page.add(new LineItem(e.getName(), e.getXX, e.getYY()))
if (page.size() == PAGE_SIZE) {
formatter.format(page);
page.clear()
}
}
if (page.size() > 0) {
formatter.format(page);
page.clear()
}
}
// 应该调用此函数,物理化这种依赖
int getmaxPageSize() {
return this.formatter.getPageSize()
}
}
- 多数人舒勇switch语句,因为它是最直截了当又有力的方案,而不是因为它适合当前清形,给我们的启发是在使用switch之前,先考虑使用多态
- 单个switch规则:对于给定的选择类型,不应有多于一个switch语句,在那个swithc语句的多个case,必须创建多态多项,取代系统中其他类似switch语句
每个团队都应遵循基于通用行业规范的一套编码标准
-
代码中出现原始数字通常是坏现象,应该用良好命名的常量来隐藏它
public static final double PI = 3.1415926; public static final int SECONDS_PER_DAY = 86400;
-
有些数字非常具有自我解释能力,不必总是用命名常量来隐藏
double circumference = radius * Math.PI * 2;
-
不仅仅是数字,魔鬼数字泛指任何不能自我描述的符号
// john Doe是测试数据中编号为7777的雇员 assertEquals(7777, Employee.find("John Doe").employeeNumber()); public static final int HOURLY_EMPLOYEE_ID = 7777; public static final String HOURLY_EMPLOYEE_NAME = "John Doe" assertEquals(HOURLY_EMPLOYEE_ID = 7777; , Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());
代码中的含糊和不准确要么是意见不同的结果,要么源于懒惰。无论什么原因,都要消除。
- 如果打算调用可能返回null的函数,确认自己检查了null值
- 如果查询你认为是数据库中唯一的记录,确保代码检查不存在其他记录
- 如果可能有并发更新,确认你实现了某种锁定机制
- ...
坚守结构甚于约定的设计策略。命名约定很好,但却次于强制性的结构。
- 用到良好命名的枚举的switch/case要弱于拥有抽象方法的基类
- 没人会被强迫每次都以同样方式实现switch/case语句
- 但基类却让具体类必须实现所有抽象方法
-
如果没有if或while语句的上下文,布尔逻辑就难以理解
-
应该把解释了条件意图的函数抽离出来
if (shouldbeDeleted(timer)) {}
// 要好于
if (timer.hasExired() && !timer.isRecurrent())
- 否定式要比肯定式难明白一些
if (buffer.shouldCompact()) {}
// 要好于
if (!buffer.shouldNotCompact()) {}
- 包括多段代码的函数应该转为多个更小的函数,每个只做一件事
// 三个函数次序很重要,但是代码并没有强制这种耦合
public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
// 每个函数都产生下一个函数所需的结果,没理由不按顺序调用
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
}
-
构建代码需要理由,而且理由应与代码结构相契合
-
// 举了个例子
-
不作为类工具的公共类,不应该放到其他类里面。惯例是将它置为public,并放在代码包的顶部
- 边界条件难以追踪,把处理边界条件的代码集中到一处
// 函数有两个抽象层级:横线有尺寸、hr标记自身的语法
public String render() throws Exception
{
StringBuffer html = new StringBuffer("<hr");
if (size > 0) {
html.append(" size=\"").append(size + 1).append("\"");
}
html.append(">");
return html.toString();
}
// render只构建一个hr标记,不管标记的HTML语法
public String render() throws Exception
{
HtmlTag hr = new HtmlTag("hr");
if (extraDashes > 0) {
hr.addAttribute("size", hrSize(extraDashes));
}
return hr.html();
}
private String hrSize(int height)
{
int hrSize = height + 1;
return String.format("%d", hrSize);
}
- 位于较高层级的配置性常量易于修改
- 我们不想写类似
a.getB().getC().doSomething()
的代码,导致难以修改设计和架构 - 只要简单的a.doSomething()