public class IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
public static String generate() {
String id = "";
try {
String hostName = InetAddress.getLocalHost().getHostName();
String[] tokens = hostName.split("\\.");
if (tokens.length > 0) {
hostName = tokens[tokens.length - 1];
}
char[] randomChars = new char[8];
int count = 0;
Random random = new Random();
while (count < 8) {
int randomAscii = random.nextInt(122);
if (randomAscii >= 48 && randomAscii <= 57) {
randomChars[count] = (char)('0' + (randomAscii - 48));
count++;
} else if (randomAscii >= 65 && randomAscii <= 90) {
randomChars[count] = (char)('A' + (randomAscii - 65));
count++;
} else if (randomAscii >= 97 && randomAscii <= 122) {
randomChars[count] = (char)('a' + (randomAscii - 97));
count++;
}
}
id = String.format("%s-%d-%s", hostName,
System.currentTimeMillis(), new String(randomChars));
} catch (UnknownHostException e) {
logger.warn("Failed to get the host name.", e);
}
return id;
}
}
①现在,对照上面的检查项,我们来看一下,上面的代码有哪些问题。
其次,IdGenerator 设计成了实现类而非接口,调用者直接依赖实现而非接口,违反基于接口而非实现编程的设计思想。实际上,将 IdGenerator 设计成实现类,而不定义接口,问题也不大。如果哪天 ID 生成算法改变了,我们只需要直接修改实现类的代码就可以。但是,如果项目中需要同时存在两种 ID 生成算法,也就是要同时存在两个 IdGenerator 实现类。比如,我们需要将这个框架给更多的系统来使用。系统在使用的时候,可以灵活地选择它需要的生成算法。这个时候,我们就需要将 IdGenerator 定义为接口,并且为不同的生成算法定义不同的实现类。
②上面是参照跟业务本身无关的、通用的代码质量关注点,现在再对照业务本身的功能和非功能需求,重新审视一下代码
③具体场景具体分析,还有哪些问题呢?
IdGenearator idGenerator = new LogTraceIdGenerator();
替换为:
IdGenearator idGenerator = new UserIdGenerator();
public interface IdGenerator {
String generate();
}
public interface LogTraceIdGenerator extends IdGenerator {
}
public class RandomIdGenerator implements LogTraceIdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() {
String substrOfHostName = getLastfieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastfieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
String[] tokens = hostName.split("\\.");
substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
} catch (UnknownHostException e) {
logger.warn("Failed to get the host name.", e);
}
return substrOfHostName;
}
private String generateRandomAlphameric(int length) {
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int maxAscii = 'z';
int randomAscii = random.nextInt(maxAscii);
boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
if (isDigit|| isUppercase || isLowercase) {
randomChars[count] = (char) (randomAscii);
++count;
}
}
return new String(randomChars);
}
}
//代码使用举例
LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator();
public class RandomIdGenerator implements LogTraceIdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() {
String substrOfHostName = getLastfieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastfieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplittedByDot(hostName);
} catch (UnknownHostException e) {
logger.warn("Failed to get the host name.", e);
}
return substrOfHostName;
}
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName) {
String[] tokens = hostName.split("\\.");
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
@VisibleForTesting
protected String generateRandomAlphameric(int length) {
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int maxAscii = 'z';
int randomAscii = random.nextInt(maxAscii);
boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
if (isDigit|| isUppercase || isLowercase) {
randomChars[count] = (char) (randomAscii);
++count;
}
}
return new String(randomChars);
}
}
public String generate();
private String getLastfieldOfHostName();
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName);
@VisibleForTesting
protected String generateRandomAlphameric(int length);
public class RandomIdGeneratorTest {
@Test
public void testGetLastSubstrSplittedByDot() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualSubstr = idGenerator.getLastSubstrSplittedByDot("field1.field2.field3");
Assert.assertEquals("field3", actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot("field1");
Assert.assertEquals("field1", actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot("field1#field2$field3");
Assert.assertEquals("field1#field2#field3", actualSubstr);
}
// 此单元测试会失败,因为我们在代码中没有处理hostName为null或空字符串的情况
@Test
public void testGetLastSubstrSplittedByDot_nullOrEmpty() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualSubstr = idGenerator.getLastSubstrSplittedByDot(null);
Assert.assertNull(actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot("");
Assert.assertEquals("", actualSubstr);
}
@Test
public void testGenerateRandomAlphameric() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualRandomString = idGenerator.generateRandomAlphameric(6);
Assert.assertNotNull(actualRandomString);
Assert.assertEquals(6, actualRandomString.length());
for (char c : actualRandomString.toCharArray()) {
Assert.assertTrue(('0' < c && c < '9') || ('a' < c && c < 'z') || ('A' < c && c < 'Z'));
}
}
// 此单元测试会失败,因为我们在代码中没有处理length<=0的情况
@Test
public void testGenerateRandomAlphameric_lengthEqualsOrLessThanZero() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualRandomString = idGenerator.generateRandomAlphameric(0);
Assert.assertEquals("", actualRandomString);
actualRandomString = idGenerator.generateRandomAlphameric(-1);
Assert.assertNull(actualRandomString);
}
}
①如果我们把 generate() 函数的功能定义为:“生成一个随机唯一 ID”,那我们只要测试多次调用 generate() 函数生成的 ID 是否唯一即可。
②如果我们把 generate() 函数的功能定义为:“生成一个只包含数字、大小写字母和中划线的唯一 ID”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否只包含数字、大小写字母和中划线。
③如果我们把 generate() 函数的功能定义为:“生成唯一 ID,格式为:{主机名 substr}-{时间戳}-{8 位随机数}。在主机名获取失败时,返回:null-{时间戳}-{8 位随机数}”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否完全符合格式要求。
①首先如果你熟悉的编程语言中有异常这种语法机制,那就尽量不要使用错误码。异常相对于错误码,有诸多方面的优势,比如可以携带更多的错误信息(exception 中可以有 message、stack trace 等信息)等。在这里就不多说。
public class UserService {
private UserRepo userRepo; // 依赖注入
public User getUser(String telephone) {
// 如果用户不存在,则返回null
return null;
}
}
// 使用函数getUser()
User user = userService.getUser("18917718965");
if (user != null) { // 做NULL值判断,否则有可能会报NPE
String email = user.getEmail();
if (email != null) { // 做NULL值判断,否则有可能会报NPE
String escapedEmail = email.replaceAll("@", "#");
}
}
// 使用空集合替代NULL
public class UserService {
private UserRepo userRepo; // 依赖注入
public List<User> getUsers(String telephonePrefix) {
// 没有查找到数据
return Collections.emptyList();
}
}
// getUsers使用示例
List<User> users = userService.getUsers("189");
for (User user : users) { //这里不需要做NULL值判断
// ...
}
// 使用空字符串替代NULL
public String retrieveUppercaseLetters(String text) {
// 如果text中没有大写字母,返回空字符串,而非NULL值
return "";
}
// retrieveUppercaseLetters()使用举例
String uppercaseLetters = retrieveUppercaseLetters("wangzheng");
int length = uppercaseLetters.length();// 不需要做NULL值判断
System.out.println("Contains " + length + " upper case letters.");
/ address格式:"192.131.2.33:7896"
public void parseRedisAddress(String address) {
this.host = RedisConfig.DEFAULT_HOST;
this.port = RedisConfig.DEFAULT_PORT;
if (StringUtils.isBlank(address)) {
return;
}
String[] ipAndPort = address.split(":");
if (ipAndPort.length != 2) {
throw new RuntimeException("...");
}
this.host = ipAndPort[0];
// parseInt()解析失败会抛出NumberFormatException运行时异常
this.port = Integer.parseInt(ipAndPort[1]);
}
①直接吞掉。具体的代码示例如下所示:
public void func1() throws Exception1 {
// ...
}
public void func2() {
//...
try {
func1();
} catch(Exception1 e) {
log.warn("...", e); //吐掉:try-catch打印日志
}
//...
}
②原封不动地 re-throw。具体的代码示例如下所示:
public void func1() throws Exception1 {
// ...
}
public void func2() throws Exception1 {//原封不动的re-throw Exception1
//...
func1();
//...
}
③包装成新的异常 re-throw。具体的代码示例如下所示:
public void func1() throws Exception1 {
// ...
}
public void func2() throws Exception2 {
//...
try {
func1();
} catch(Exception1 e) {
throw new Exception2("...", e); // wrap成新的Exception2然后re-throw
}
//...
}
public String generate() {
String substrOfHostName = getLastFieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
public String generate() throws IdGenerationFailureException {
String substrOfHostName = getLastFieldOfHostName();
if (substrOfHostName == null || substrOfHostName.isEmpty()) {
throw new IdGenerationFailureException("host name is empty.");
}
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastFieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplittedByDot(hostName);
} catch (UnknownHostException e) {
logger.warn("Failed to get the host name.", e);
}
return substrOfHostName;
}
private String getLastFieldOfHostName() throws UnknownHostException{
String substrOfHostName = null;
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplittedByDot(hostName);
return substrOfHostName;
}
public String generate() throws IdGenerationFailureException {
String substrOfHostName = null;
try {
substrOfHostName = getLastFieldOfHostName();
} catch (UnknownHostException e) {
throw new IdGenerationFailureException("host name is empty.");
}
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者空字符串,这个函数应该返回什么?
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName) {
String[] tokens = hostName.split("\\.");
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName) {
if (hostName == null || hostName.isEmpty()) {
throw IllegalArgumentException("..."); //运行时异常
}
String[] tokens = hostName.split("\\.");
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
private String getLastFieldOfHostName() throws UnknownHostException{
String substrOfHostName = null;
String hostName = InetAddress.getLocalHost().getHostName();
if (hostName == null || hostName.isEmpty()) { // 此处做判断
throw new UnknownHostException("...");
}
substrOfHostName = getLastSubstrSplittedByDot(hostName);
return substrOfHostName;
}
对于 generateRandomAlphameric(int length) 函数,如果 length < 0 或 length = 0,这个函数应该返回什么?
@VisibleForTesting
protected String generateRandomAlphameric(int length) {
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int maxAscii = 'z';
int randomAscii = random.nextInt(maxAscii);
boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
if (isDigit|| isUppercase || isLowercase) {
randomChars[count] = (char) (randomAscii);
++count;
}
}
return new String(randomChars);
}
}
public class RandomIdGenerator implements IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() throws IdGenerationFailureException {
String substrOfHostName = null;
try {
substrOfHostName = getLastFieldOfHostName();
} catch (UnknownHostException e) {
throw new IdGenerationFailureException("...", e);
}
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastFieldOfHostName() throws UnknownHostException{
String substrOfHostName = null;
String hostName = InetAddress.getLocalHost().getHostName();
if (hostName == null || hostName.isEmpty()) {
throw new UnknownHostException("...");
}
substrOfHostName = getLastSubstrSplittedByDot(hostName);
return substrOfHostName;
}
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName) {
if (hostName == null || hostName.isEmpty()) {
throw new IllegalArgumentException("...");
}
String[] tokens = hostName.split("\\.");
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
@VisibleForTesting
protected String generateRandomAlphameric(int length) {
if (length <= 0) {
throw new IllegalArgumentException("...");
}
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int maxAscii = 'z';
int randomAscii = random.nextInt(maxAscii);
boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
if (isDigit|| isUppercase || isLowercase) {
randomChars[count] = (char) (randomAscii);
++count;
}
}
return new String(randomChars);
}
}
五、参考链接
https://www.jianshu.com/nb/50853864