IT行业,千变万化,日新月异,身处其中的各位同仁必感同身受,特别是对从事技术开发的朋友们而言,或许能感觉到唯一不变的就是变化。对纷繁复杂的程序人生而言,其实有一个看不见的主旋律,那就是找工作,找房子,找人(另一半)。如果你是心怀梦想,浪迹于上广北深的千万大军中的一员的话,我想这一句话该是你生活的写照了。在三找中,找工作是其他两找的基础,本系列文章主要是就找工作展开而言。
对开发而言,面试的过程,很多时候就是解算法题,给你一块白板,一支笔,你在白板上写下面试者给你的算法题解。在此,我向大家推荐一个面试算法题网站leetcode oj(https://oj.leetcode.com/),该网站收集了个大著名软件公司的算法题,正在找工作的朋友不妨上去看看。算法面试大概分两部分,一是要做出来,二是要说清楚。很多失败的情况,主要是倒在第二条。说不清楚,除了表达能力外,很重要的一个原因就是算法的设计不不清晰,软件是一个逻辑复杂聚合体,如果做的不清楚,想讲清楚,不太可能,接下来,借用leetcode的一道算法题,讨论一下如何利用面向对象的分析和设计模式给出清晰设计方案。
Validate if a given string is numeric.
Some examples:
"0" => true
" 0.1 " => true
"abc" => false
"1 a" => false
"2e10" => true
Note: It is intended for the problem statement to be ambiguous. You should gather all requirements up front before implementing one.
(https://oj.leetcode.com/problems/valid-number/)
这道题目初看上去很简单,如果你拿到后直接动手,恐怕就危险了,如同它提示所说的,题目一个隐含目的就是模糊性,如果你不对题目仔细分析,并对面试官提出一些减少模糊性的问题就直接动手,那你很可能就已经被pass了。题目要求设计算法,判断字符串是否是数字,数字格式合法性的情况太多了,除了题目给定的例子外,还有很多情况需要搞清楚,例如“00123”是否合法, ".123", "123." 是否是合法的浮点数。而且我们不要以为只要判断给定字符串符合例子格式就可以了,同时还要考虑十六进制格式“0Xabc"是否也是合法的数字格式,罗马数字”I, II, IV"等是否也是合理格式。这道题不但语义模糊,而且具备极高的开放性,扩展性,据说在leetcode上,对该题的测试用例就有1400多个,因此题目看起来简单,但隐藏着很多的陷阱,所以它在leetcode里难度被标记为"Hard". 我们先看看leetcode里一位朋友给的c++解法:
这是一段简洁的应该是通过了测试的c++解决方案,这段代码是“对”的,但是可能会有一些问题,
一,它的表述性不强,除了作者外,其他人要容易的看懂,估计不容易(其他人也包括面试官)。
二,代码中有若干注释,大家如果读过Code Complete, Clean Code 等编程大牛写的书籍,则会意识到,好的代码是不需要注释的,当你需要注释才能解释代码的时候,很可能意味着你的设计不清楚,你的代码有code smell。
第三,将算法逻辑通过很多if else语句柔和起来,调试会复杂,修改一处逻辑很容易引起新的bug。
第四,很难扩展,该解法只处理了问题中给的几个例子,很显然,合理数字不止这三种,如果要添加新的判断功能的话,例如判断十六进制数,那将是困难重重,添加后接下来就是找bug了。如果要是这是面试给出的答案,恐怕要通过的可能性会不高。接下来,我们看看,能否通过面向对象的方法和设计模式给出一个清晰的,可容错,可扩展的解决方案。
根据经典设计模式书 《design patterns, elements of reusable oriented software》。它给出了几条面向对象的设计原则,我们先来看看其中三条:
1. Program to interface, no implementation.
2. Close for modification, open for extension.
3. Encapsulate what varies
根据这几条设计原则,我们一起探讨如何设计一个满足规则的方案,以下的代码基于java实现。首先是program to interface,因此我们先针对问题给出一个对象接口:
对实现了该接口的类,只要将字符串输入,它将返回输入字符串是否是合理的数字格式。根据规则3,封装可变的部分,那此处可变的是什么呢,那就是字符串的解析逻辑,整形有自己的解析逻辑,浮点数有自己的解析逻辑,科学计数有自己的解析逻辑,十六进制数有自己的解析逻辑,因此要将这些不同的解析算法封装起来,而不是全部柔和在一起,因此根据这个接口,可以分别派生出以下几个类:
IntegerValidate , HexValidate ,SienceFormatValidate ,FloatValidate
各种类型的解析算法分别实现在相应类里,这样就实现了原则3。这种做法有一个好处就是各司其职,逻辑清晰,同时由于封装导致局部化,某一处的修改不会影响到其他部位。假定上述的几个类都实现了,然后我们根据设计模式 Chain of responsibility, 即责任链模式,将他们用一个队列串起来,当要判断给定字符串时,只要将链表上的对象逐个取出,调用validate接口,只要有一个返回true, 那么字符串就是合法的,要不然字符串就非法。该设计模式又满足了第二条原则, 试想如果我们需要再添加对罗马数字的支持,我们只要再实现一个类, RoamNumberValidate, 然后将其加入链表即可,新功能的添加不会对系统造成负面影响。接下来我们看看相应代码实现.
NumberValidateTemplate 类是一个模板类,对应于书里称之为Template 模式,关于该模式我们以后详谈,该类的主要功能是对输入的字符串做预处理,我想,不需要多说,大家能看懂它的实现逻辑。接下来就是IntegerValidate 等算法实现类的代码:
这两个类的实现简单,无需太多精力可看懂,对于FloatValidate, 我们只要判断"."前后的内容能通过IntegerValidate即可,由此可见,通过面向对象的设计方法,容易实现代码的重用。
同理,对于SienceFormatValidate , 只用判断"e"的前半部分满足IntegerValidate, 或FloatValidate, 后半部分满足IntegerValidate即可,可见又是一次代码重用^_^:
接下来用一个链表将他们串联起来:
最后,我们看看它用起来多简单:
至此,整个方案就结束了,虽然代码量相比上个方案有所增加,但逻辑上更清晰,扩展性更强,要想增加对罗马数字的支持,再添加一个相应类的实现即可,这也就做到了Close for modification, Open for extension。 该方案很好的包容了题目所蕴含的模糊性,使用这个方案,pass的概率想必会大一些吧。
作者:陈屹
转载注明出处,谢谢
对开发而言,面试的过程,很多时候就是解算法题,给你一块白板,一支笔,你在白板上写下面试者给你的算法题解。在此,我向大家推荐一个面试算法题网站leetcode oj(https://oj.leetcode.com/),该网站收集了个大著名软件公司的算法题,正在找工作的朋友不妨上去看看。算法面试大概分两部分,一是要做出来,二是要说清楚。很多失败的情况,主要是倒在第二条。说不清楚,除了表达能力外,很重要的一个原因就是算法的设计不不清晰,软件是一个逻辑复杂聚合体,如果做的不清楚,想讲清楚,不太可能,接下来,借用leetcode的一道算法题,讨论一下如何利用面向对象的分析和设计模式给出清晰设计方案。
Validate if a given string is numeric.
Some examples:
"0" => true
" 0.1 " => true
"abc" => false
"1 a" => false
"2e10" => true
Note: It is intended for the problem statement to be ambiguous. You should gather all requirements up front before implementing one.
(https://oj.leetcode.com/problems/valid-number/)
这道题目初看上去很简单,如果你拿到后直接动手,恐怕就危险了,如同它提示所说的,题目一个隐含目的就是模糊性,如果你不对题目仔细分析,并对面试官提出一些减少模糊性的问题就直接动手,那你很可能就已经被pass了。题目要求设计算法,判断字符串是否是数字,数字格式合法性的情况太多了,除了题目给定的例子外,还有很多情况需要搞清楚,例如“00123”是否合法, ".123", "123." 是否是合法的浮点数。而且我们不要以为只要判断给定字符串符合例子格式就可以了,同时还要考虑十六进制格式“0Xabc"是否也是合法的数字格式,罗马数字”I, II, IV"等是否也是合理格式。这道题不但语义模糊,而且具备极高的开放性,扩展性,据说在leetcode上,对该题的测试用例就有1400多个,因此题目看起来简单,但隐藏着很多的陷阱,所以它在leetcode里难度被标记为"Hard". 我们先看看leetcode里一位朋友给的c++解法:
class Solution {
public:
// can be also solved by DFA
bool isNumber(const char *s) {
bool has_dot = false, has_e = false, has_num = false;
while (*s == ' ') s++; // filter prefix ' '
if (*s == '-' || *s == '+') s++; // filter operator '+' or '-'
while (*s && *s != ' ') {
if ((*s == 'e' || *s == 'E') && !has_e) { // filter 'e'
has_e = has_dot = true;
if (!has_num) return false; // there should be a number before 'e'
if (*(s + 1) == '-' || *(s + 1) == '+') s++;
if (!isdigit(*(s + 1))) return false;
} else if (*s == '.' && !has_dot) has_dot = true;
else if (isdigit(*s)) has_num = true;
else return false;
s++;
}
while (*s) if (*s++ != ' ') return false; // filter suffix ' '
return has_num;
}
};
这是一段简洁的应该是通过了测试的c++解决方案,这段代码是“对”的,但是可能会有一些问题,
一,它的表述性不强,除了作者外,其他人要容易的看懂,估计不容易(其他人也包括面试官)。
二,代码中有若干注释,大家如果读过Code Complete, Clean Code 等编程大牛写的书籍,则会意识到,好的代码是不需要注释的,当你需要注释才能解释代码的时候,很可能意味着你的设计不清楚,你的代码有code smell。
第三,将算法逻辑通过很多if else语句柔和起来,调试会复杂,修改一处逻辑很容易引起新的bug。
第四,很难扩展,该解法只处理了问题中给的几个例子,很显然,合理数字不止这三种,如果要添加新的判断功能的话,例如判断十六进制数,那将是困难重重,添加后接下来就是找bug了。如果要是这是面试给出的答案,恐怕要通过的可能性会不高。接下来,我们看看,能否通过面向对象的方法和设计模式给出一个清晰的,可容错,可扩展的解决方案。
根据经典设计模式书 《design patterns, elements of reusable oriented software》。它给出了几条面向对象的设计原则,我们先来看看其中三条:
1. Program to interface, no implementation.
2. Close for modification, open for extension.
3. Encapsulate what varies
根据这几条设计原则,我们一起探讨如何设计一个满足规则的方案,以下的代码基于java实现。首先是program to interface,因此我们先针对问题给出一个对象接口:
interface NumberValidate {
boolean validate(String s);
}
对实现了该接口的类,只要将字符串输入,它将返回输入字符串是否是合理的数字格式。根据规则3,封装可变的部分,那此处可变的是什么呢,那就是字符串的解析逻辑,整形有自己的解析逻辑,浮点数有自己的解析逻辑,科学计数有自己的解析逻辑,十六进制数有自己的解析逻辑,因此要将这些不同的解析算法封装起来,而不是全部柔和在一起,因此根据这个接口,可以分别派生出以下几个类:
IntegerValidate , HexValidate ,SienceFormatValidate ,FloatValidate
各种类型的解析算法分别实现在相应类里,这样就实现了原则3。这种做法有一个好处就是各司其职,逻辑清晰,同时由于封装导致局部化,某一处的修改不会影响到其他部位。假定上述的几个类都实现了,然后我们根据设计模式 Chain of responsibility, 即责任链模式,将他们用一个队列串起来,当要判断给定字符串时,只要将链表上的对象逐个取出,调用validate接口,只要有一个返回true, 那么字符串就是合法的,要不然字符串就非法。该设计模式又满足了第二条原则, 试想如果我们需要再添加对罗马数字的支持,我们只要再实现一个类, RoamNumberValidate, 然后将其加入链表即可,新功能的添加不会对系统造成负面影响。接下来我们看看相应代码实现.
abstract class NumberValidateTemplate implements NumberValidate{
public boolean validate(String s)
{
if (checkStringEmpty(s))
{
return false;
}
s = checkAndProcessHeader(s);
if (s.length() == 0)
{
return false;
}
return doValidate(s);
}
private boolean checkStringEmpty(String s)
{
if (s.equals(""))
{
return true;
}
return false;
}
private String checkAndProcessHeader(String value)
{
value = value.trim();
if (value.startsWith("+") || value.startsWith("-"))
{
value = value.substring(1);
}
return value;
}
protected abstract boolean doValidate(String s);
}
NumberValidateTemplate 类是一个模板类,对应于书里称之为Template 模式,关于该模式我们以后详谈,该类的主要功能是对输入的字符串做预处理,我想,不需要多说,大家能看懂它的实现逻辑。接下来就是IntegerValidate 等算法实现类的代码:
class IntegerValidate extends NumberValidateTemplate{
protected boolean doValidate(String integer)
{
for (int i = 0; i < integer.length(); i++)
{
if(Character.isDigit(integer.charAt(i)) == false)
{
return false;
}
}
return true;
}
}
class HexValidate extends NumberValidateTemplate{
private char[] valids = new char[] {'a', 'b', 'c', 'd', 'e', 'f'};
protected boolean doValidate(String hex)
{
hex = hex.toLowerCase();
if (hex.startsWith("0x"))
{
hex = hex.substring(2);
}
else
{
return false;
}
for (int i = 0; i < hex.length(); i++)
{
if (Character.isDigit(hex.charAt(i)) != true && isValidChar(hex.charAt(i)) != true)
{
return false;
}
}
return true;
}
private boolean isValidChar(char c)
{
for (int i = 0; i < valids.length; i++)
{
if (c == valids[i])
{
return true;
}
}
return false;
}
}
这两个类的实现简单,无需太多精力可看懂,对于FloatValidate, 我们只要判断"."前后的内容能通过IntegerValidate即可,由此可见,通过面向对象的设计方法,容易实现代码的重用。
class FloatValidate extends NumberValidateTemplate{
protected boolean doValidate(String floatVal)
{
int pos = floatVal.indexOf(".");
if (pos == -1)
{
return false;
}
if (floatVal.length() == 1)
{
return false;
}
String first = floatVal.substring(0, pos);
String second = floatVal.substring(pos + 1, floatVal.length());
if (checkFirstPart(first) == true && checkFirstPart(second) == true)
{
return true;
}
return false;
}
private boolean checkFirstPart(String first)
{
if (first.equals("") == false && checkPart(first) == false)
{
return false;
}
return true;
}
private boolean checkPart(String part)
{
if (Character.isDigit(part.charAt(0)) == false ||
Character.isDigit(part.charAt(part.length() - 1)) == false)
{
return false;
}
NumberValidate nv = new IntegerValidate();
if (nv.validate(part) == false)
{
return false;
}
return true;
}
}
同理,对于SienceFormatValidate , 只用判断"e"的前半部分满足IntegerValidate, 或FloatValidate, 后半部分满足IntegerValidate即可,可见又是一次代码重用^_^:
class SienceFormatValidate extends NumberValidateTemplate{
protected boolean doValidate(String s)
{
s = s.toLowerCase();
int pos = s.indexOf("e");
if (pos == -1)
{
return false;
}
if (s.length() == 1)
{
return false;
}
String first = s.substring(0, pos);
String second = s.substring(pos+1, s.length());
if (validatePartBeforeE(first) == false || validatePartAfterE(second) == false)
{
return false;
}
return true;
}
private boolean validatePartBeforeE(String first)
{
if (first.equals("") == true)
{
return false;
}
if (checkHeadAndEndForSpace(first) == false)
{
return false;
}
NumberValidate integerValidate = new IntegerValidate();
NumberValidate floatValidate = new FloatValidate();
if (integerValidate.validate(first) == false && floatValidate.validate(first) == false)
{
return false;
}
return true;
}
private boolean checkHeadAndEndForSpace(String part)
{
if (part.startsWith(" ") ||
part.endsWith(" "))
{
return false;
}
return true;
}
private boolean validatePartAfterE(String second)
{
if (second.equals("") == true)
{
return false;
}
if (checkHeadAndEndForSpace(second) == false)
{
return false;
}
NumberValidate integerValidate = new IntegerValidate();
if (integerValidate.validate(second) == false)
{
return false;
}
return true;
}
}
接下来用一个链表将他们串联起来:
class NumberValidator implements NumberValidate {
private ArrayList<NumberValidate> validators = new ArrayList<NumberValidate>();
public NumberValidator()
{
addValidators();
}
private void addValidators()
{
NumberValidate nv = new IntegerValidate();
validators.add(nv);
nv = new FloatValidate();
validators.add(nv);
nv = new HexValidate();
validators.add(nv);
nv = new SienceFormatValidate();
validators.add(nv);
}
@Override
public boolean validate(String s)
{
for (NumberValidate nv : validators)
{
if (nv.validate(s) == true)
{
return true;
}
}
return false;
}
}
最后,我们看看它用起来多简单:
public class Solution {
public boolean isNumber(String s) {
NumberValidate nv = new NumberValidator();
return nv.validate(s);
}
}
至此,整个方案就结束了,虽然代码量相比上个方案有所增加,但逻辑上更清晰,扩展性更强,要想增加对罗马数字的支持,再添加一个相应类的实现即可,这也就做到了Close for modification, Open for extension。 该方案很好的包容了题目所蕴含的模糊性,使用这个方案,pass的概率想必会大一些吧。
作者:陈屹
转载注明出处,谢谢