在之前的文章中有介绍过SOLID原则,这篇博客来介绍一下其余的几种设计原则。
KISS原则和YANGI原则
KISS原则为尽量保持简单原则。
YANGI原则为You Ain’t Gonna Need It,核心思想是不要做过度设计,也是保持简单的意思。
感觉这个和延后实现的思想有点相似。(延后实现的思想在计算机中有很多应用,比如copy on write技术,进程创建函数fork就运用了这个技术,还有操作系统收到操作IO的请求时,不会立刻实现,而是先等一会观察是否会有类似的请求在之后到来,使得几个操作可以合并)
这两个原则其实说的比较抽象,因为简单这个概念是相对的,模糊的。
第一个原则是保持简单原则
那么,怎样的类才是简单的呢,可以说,如果一个类让你维护起来觉得困难了,那么这个类就是不简单的。也可以把自己设计的类给别人看,别人觉得看不懂,那么这个类也是复杂的。
当然,这个描述其实也必较模糊。因为这个可能要专业直觉。
第二个原则是不要做过度设计,核心目标也是保持简单,但主要说的是做不做的问题。
当然,这个原则也不是指完全不用去管以后的事,有些近期可能会用到的业务,我们有必要未其留下扩展点,但不要真正去实现。 对于一些长远的的业务,我们则不用去考虑,因为软件设计本来就是一件耗脑子的事情,做好当前的设计才是最重要的,没必要把精力留在太久远的将来,如果将来真的用到了某个我们没去考虑的功能,我们考虑重构即可。
DYR原则
DRY原则全文为Don’t Repeat Yourself.
意思是不要写重复的代码。
软件代码中的重复主要有实现逻辑重复,功能语义重复,代码执行重复。
实现逻辑重复:
假设有一个用户信息验证的类,它验证用户名是否有效和验证密码是否有效
public class UserAuthenticator {
public void authenticate(String username, String password) {
if (!isValidUsername(username)) {
// ...throw InvalidUsernameException...
}
if (!isValidPassword(password)) {
// ...throw InvalidPasswordException...
}
//... 省略其他代码...
}
private boolean isValidUsername(String username) {
// check not null, not empty
if (StringUtils.isBlank(username)) {
return false;
}
// check length: 4~64
int length = username.length();
if (length < 4 || length > 64) {
return false;
}
// contains only lowcase characters
if (!StringUtils.isAllLowerCase(username)) {
return false;
}
// contains only a~z,0~9,dot
for (int i = 0; i < length; ++i) {
char c = username.charAt(i);
if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
return false;
}
}
return true;
}
private boolean isValidPassword(String password) {
// check not null, not empty
if (StringUtils.isBlank(password)) {
return false;
}
// check length: 4~64
int length = password.length();
if (length < 4 || length > 64) {
return false;
}
// contains only lowcase characters
if(!StringUtils.isAllLowerCase(password)) {
return false;
}
// contains only a~z,0~9,dot
for (int i = 0; i < length; ++i) {
char c = password.charAt(i);
if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
return false;
}
}
return true;
}
}
在这段代码中,验证用户名和密码的实现逻辑发生了重复。
我们未来实现代码重用,可以将用户名和密码的验证变成一个方法isValidUserNameOrPassword()。
然而这样是有问题的,如果密码的检验流程发生了一些变化,那么这个函数就不能工作了。
所以是不能合并两个函数的。
我们实现代码复用可以将一些校验中通用的逻辑包装成一个函数或几个函数,通过调用函数来实现代码复用。
功能语义重复
假如你要验证IP地址是否有效,因此你写了一个isValidIp()方法,但是有一个项目同事没有使用你这个方法,而是重新写了一个 checkIfIpValid()方法,这样虽然两个代码的名字不同,实现逻辑也不同,但是功能都是一样的,所以违反了DRY原则,应该避免。
我们在平时工作中时,应该要了解和自己工作密切的同事的工作。特别是在接手一个同事的工作时,应该先了解清楚同事已经做了什么工作,避免做重复的事。
执行重复
public class UserService {
private UserRepo userRepo;// 通过依赖注入或者 IOC 框架注入
public User login(String email, String password) {
boolean existed = userRepo.checkIfUserExisted(email, password);
if (!existed) {
// ... throw AuthenticationFailureException...
}
User user = userRepo.getUserByEmail(email);
return user;
}
}
public class UserRepo {
public boolean checkIfUserExisted(String email, String password) {
if (!EmailValidation.validate(email)) {
// ... throw InvalidEmailException...
}
if (!PasswordValidation.validate(password)) {
// ... throw InvalidPasswordException...
}
//...query db to check if email&password exists...
}
public User getUserByEmail(String email) {
if (!EmailValidation.validate(email)) {
// ... throw InvalidEmailException...
}
//...query db to get user by email...
}
}
这段代码中,EmailValidation.validate(email)函数执行了两次,在checkIfUserExisted和getUserByEmail都执行了一次。这执行重复,而且,由于验证邮箱需要访问数据库,这个开销可不小,违反了DRY原则,我们应该避免。
最小知识原则
最小知识原则也叫迪米特原则,是一个用来实现高内聚,低耦合的指导方法。
我们由于相近的功能往往同时被修改,所以我们将相近的功能放到一个类中。
同时,我们要保持类与类之间的关系简单清晰。
先看它不让我们做什么:
没有依赖关系的类不应该依赖。
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
NetworkTransporter是一个底层网络传输类,它本不应该依赖HtmlRequest类,但它却依赖了。这违反了迪米特法则,我们应该将这种依赖关系改为交互关系。
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(String address, Byte[] data) {
//...
}
}
之后要调用send函数是,我们在htmlRequest中取出address和data传给send做参数就行,没必要将整个类传给send.
接下来是它要我们做什么。
有依赖关系的类之间,尽量只依赖必要的接口
public class Serialization {
public String serialize(Object object) {
String serializedResult = ...;
//...
return serializedResult;
}
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
若是某些类只用到了序列化的方法,有些类只用到了非序列化方法,这样按照迪米特法则,就需要将接口的粒度再细分。
但是实际上没有必要,因为序列化和反序列化是相似度很高的操作,将它们拆开反而降低了类的内聚性。
所以,软件设计其实是没有银弹的,没有什么一定好的原则。我们一定要清楚我们软件设计的目的,那就是保证高内聚,低耦合,设计原则和设计模式其实只是一些术,但是只要道不走偏,术的重要性就没那么高了。