进攻性编程

如何使您的代码同时更加简洁和行为规范

您是否曾经有过一个表现异常古怪的应用程序? 您知道,您单击一个按钮并没有任何反应。 或屏幕突然变黑。 否则应用程序进入“奇怪状态”,您必须重新启动它才能使事情重新开始工作。

如果您经历过这种情况,那么您可能是特定形式的防御性编程的受害者,我想将其称为“偏执狂编程”。 防守者会受到警惕和推理。 偏执狂的人会害怕并且以奇怪的方式行事。 在本文中,我将提供一种替代方法:“进攻性”编程。

谨慎的读者

这样的偏执编程看起来像什么? 这是Java中的一个典型示例:

public String badlyImplementedGetData(String urlAsString) {
	// Convert the string URL into a real URL
	URL url = null;
	try {
		url = new URL(urlAsString);
	} catch (MalformedURLException e) {
		logger.error("Malformed URL", e);
	}

	// Open the connection to the server
	HttpURLConnection connection = null;
	try {
		connection = (HttpURLConnection) url.openConnection();
	} catch (IOException e) {
		logger.error("Could not connect to " + url, e);
	}

	// Read the data from the connection
	StringBuilder builder = new StringBuilder();
	try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
		String line;
		while ((line = reader.readLine()) != null) {
			builder.append(line);
		}
	} catch (Exception e) {
		logger.error("Failed to read data from " + url, e);
	}
	return builder.toString();
}

此代码只是将URL的内容作为字符串读取。 数量惊人的代码可以完成非常简单的任务,但是Java就是这样。

此代码有什么问题? 该代码似乎可以处理所有可能发生的错误,但是它以一种可怕的方式进行处理:它只是忽略它们并继续。 Java的经过检查的示例(这是一个非常糟糕的发明)暗含鼓励这种做法,但是其他语言也具有类似的行为。

如果出现问题怎么办:

  • 如果传入的URL是无效的URL(例如,“ http // ..”而不是“ http://…”), connection = (HttpURLConnection) url.openConnection(); NullPointerException: connection = (HttpURLConnection) url.openConnection(); 。 此时,获取错误报告的可怜的开发人员已经失去了原始错误的所有上下文,我们甚至都不知道哪个URL导致了问题。
  • 如果所涉及的网站不存在,则情况将变得非常糟糕:该方法将返回一个空字符串。 为什么? StringBuilder builder = new StringBuilder();的结果StringBuilder builder = new StringBuilder(); 仍会从方法中返回。

一些开发人员认为这样的代码很好,因为我们的应用程序不会崩溃。 我认为可能发生的事情比应用程序崩溃更糟。 在这种情况下,该错误只会导致错误的行为,而无需任何解释。 例如,该屏幕可能为空白,但该应用程序未报告任何错误。

让我们看一下以令人反感的方式重写的代码:

public String getData(String url) throws IOException {
	HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();

	// Read the data from the connection
	StringBuilder builder = new StringBuilder();
	try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
		String line;
		while ((line = reader.readLine()) != null) {
			builder.append(line);
		}
	}
	return builder.toString();
}

throws IOException语句(在Java中是必需的,但我不知道其他语言)指示此方法可能会失败,并且必须准备调用方法来处理此问题。

这段代码更加简洁,如果出现错误,则用户和日志(大概)会收到正确的错误消息。

第1课:不要在本地处理异常。

防护线

那么应该如何处理这种错误呢? 为了进行良好的错误处理,我们必须考虑应用程序的整个体系结构。 假设我们有一个应用程序,它使用一些URL的内容定期更新UI。

public static void startTimer() {
	Timer timer = new Timer();
	timer.scheduleAtFixedRate(timerTask(SERVER_URL), 0, 1000);
}

private static TimerTask timerTask(final String url) {
	return new TimerTask() {
		@Override
		public void run() {
			try {
				String data = getData(url);
				updateUi(data);
			} catch (Exception e) {
				logger.error("Failed to execute task", e);
			}
		}
	};
}

这就是我们想要的想法! 大多数不可预料的错误是无法恢复的,但是我们不希望因为它而停止计时器,对吗?

如果我们这样做会怎样?

首先,一种常见的做法是在RuntimeExceptions中包装Java的(破碎的)检查异常:

public static String getData(String urlAsString) {
	try {
		URL url = new URL(urlAsString);
		HttpURLConnection connection = (HttpURLConnection) url.openConnection();

		// Read the data from the connection
		StringBuilder builder = new StringBuilder();
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
			String line;
			while ((line = reader.readLine()) != null) {
				builder.append(line);
			}
		}
		return builder.toString();
	} catch (IOException e) {
		throw new RuntimeException(e.getMessage(), e);
	}
}

实际上,整个库的编写仅比隐藏Java语言的此丑陋功能有价值。

现在,我们可以简化计时器:

public static void startTimer() {
	Timer timer = new Timer();
	timer.scheduleAtFixedRate(timerTask(SERVER_URL), 0, 1000);
}

private static TimerTask timerTask(final String url) {
	return new TimerTask() {
		@Override
		public void run() {
			updateUi(getData(url));
		}
	};
}

如果我们使用错误的URL(或服务器已关闭)运行此代码,则情况会变得很糟:我们收到一条错误消息,显示为标准错误,并且计时器死了。

在这一时间点,应该显而易见的是:此代码重试是否存在导致NullPointerException的错误,或者服务器是否正好立即停机。

尽管第二种情况很好,但第一种情况可能不是:导致每次代码均失败的错误现在将在日志中清除错误消息。 也许我们最好只是消磨计时器?

public static void startTimer() // ...

public static String getData(String urlAsString) // ...

private static TimerTask timerTask(final String url) {
	return new TimerTask() {
		@Override
		public void run() {
			try {
				String data = getData(url);
				updateUi(data);
			} catch (IOException e) {
				logger.error("Failed to execute task", e);
			}
		}
	};
}

第2课:恢复并不总是一件好事:您必须考虑错误是由环境引起的,例如网络问题,以及由错误引起的问题,这些错误只有在有人更新程序后才会消失。

你真的在那里吗?

假设我们有WorkOrders其上具有任务的WorkOrders 。 每个任务都由某人执行。 我们想收集参与WorkOrder的人员。 您可能遇到过这样的代码:

public static Set findWorkers(WorkOrder workOrder) {
	Set people = new HashSet();

	Jobs jobs = workOrder.getJobs();
	if (jobs != null) {
		List jobList = jobs.getJobs();
		if (jobList != null) {
			for (Job job : jobList) {
				Contact contact = job.getContact();
				if (contact != null) {
					Email email = contact.getEmail();
					if (email != null) {
						people.add(email.getText());
					}
				}
			}
		}
	}
	return people;
}

在这段代码中,我们不相信正在发生的事情,对吧? 假设我们收到了一些烂数据。 在这种情况下,代码将愉快地检查数据并返回一个空集。 我们实际上不会检测到数据不符合我们的期望。

让我们清理一下:

public static Set findWorkers(WorkOrder workOrder) {
	Set people = new HashSet();
	for (Job job : workOrder.getJobs().getJobs()) {
		people.add(job.getContact().getEmail().getText());
	}
	return people;
}

哇! 所有代码都去了哪里? 突然之间,很容易再次思考并理解代码。 而且,如果我们正在处理的工作单的结构有问题,我们的代码将给我们带来崩溃的麻烦!

空检查是偏执编程最隐蔽的来源之一,它们繁殖很快。 图像,您从生产中得到了一个错误报告–该代码只是崩溃,其中包含以下代码中的NullPointerException(对于您,C#头为NullReferenceException):

public String getCustomerName() {
	return customer.getName();
}

人们压力很大! 你是做什么? 当然,您还要添加另一个null检查:

public String getCustomerName() {
        if (customer == null) return null;
	return customer.getName();
}

您编译代码并交付它。 稍后,您将收到另一个报告:以下代码中有一个空指针异常:

public String getOrderDescription() {
   return getOrderDate() + " " + getCustomerName().substring(0,10) + "...";
}

如此一来,空检查在代码中的传播就开始了。 只需从一开始就解决问题,然后加以解决:不要接受null。

顺便说一句,如果您想知道我们是否可以使解析代码接受空引用并且仍然很简单,那么我们可以。 假设带有工作订单的示例来自XML文件。 在这种情况下,我最喜欢的解决方法是:

public static Set findWorkers(XmlElement workOrder) {
	Set people = new HashSet();
	for (XmlElement email : workOrder.findRequiredChildren("jobs", "job", "contact", "email")) {
		people.add(email.text());
	}
	return people;
}

当然,这需要比Java迄今为止所拥有的更合适的库。

第3课:空检查会隐藏错误并繁殖更多的空检查。

结论

当试图采取防御措施时,程序员通常最终会变得偏执-即拼命地解决他们所看到的问题,而不是处理根本原因。 一种让您的代码崩溃并在源代码处进行修复的攻击性策略将使您的代码更简洁,更不易出错。

隐藏错误会使错误滋生。 炸毁您的应用程序会迫使您解决实际的问题。

参考:来自JCG合作伙伴 Johannes Brodwall的具有攻击性的编程 ,位于“ 更大的盒子内思考”博客上。

翻译自: https://www.javacodegeeks.com/2013/09/offensive-programming.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值