设计原则

  • 一.理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
    • 1. 如何理解单一职责原则(SRP)?
  • 二. 如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
  • 三. 里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
  • 四.接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
  • 五. 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
    • 1. 控制反转(IOC)
    • 2. 依赖注入
    • 3. 依赖注入框架
    • 4. 依赖反转原则(DIP)
  • 六. 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
    • 1. 如何理解“KISS 原则”?
    • 2. 代码行数越少就越“简单”吗?
    • 3. 代码逻辑复杂就违背 KISS 原则吗?
    • 4. 如何写出满足 KISS 原则的代码?
  • 七. 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
    • 1. DRY 原则
    • 2. 实现逻辑重复
    • 3. 功能语义重复
    • 4. 代码执行重复
    • 5. 代码复用性
    • 6. 怎么提高代码复用性
  • 八.理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
    • 1. 何为“高内聚、松耦合”?
    • 2. “迪米特法则”理论描述
    • 3.“迪米特法则”理论之前半部分---{不该有直接依赖关系的类之间,不要有依赖}
    • 4.“迪米特法则”理论之后半部分---{有依赖关系的类之间,尽量只依赖必要的接口}
    • 5. 辩证思考与灵活应用
    • 6. 总结
  • 九. 实战一(上):针对业务系统的开发,如何做需求分析和设计
    • 1. 需求分析
    • 2. 系统设计
  • 十. 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
    • 1. 业务开发包括哪些工作?
    • 2. 数据库设计
    • 3. 接口设计
    • 4.业务模型设计
    • 5. 为什么要分 MVC 三层开发
    • 6. BO、VO、Entity 存在的意义是什么?
  • 十一. 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
    • 1. 项目背景
    • 2. 需求分析
    • 3. 框架设计

一.理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?

1. 如何理解单一职责原则(SRP)?
  1. 单一职责原则

一个类或者模块只负责完成一个职责(或者功能)

  1. 一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

  2. 例子:

比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类

  1. 不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类

  2. 解决方法:实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构

  3. 下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

  1. 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分
  2. 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  3. 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  4. 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  5. 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来
  1. 如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

二. 如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?

  1. 如何理解“对扩展开放、修改关闭”?

开闭原则: 软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)

  1. 例子:
    (1). api接口监控功能

public class Alert {private AlertRule rule; //存储告警规则,可以自由设置//告警通知类,支持邮件、短信、微信、手机等多种通知渠道private Notification notification; public Alert(AlertRule rule, Notification notification) {this.rule = rule;this.notification = notification;}public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {long tps = requestCount / durationOfSeconds;if (tps > rule.getMatchedRule(api).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}}
}

(2). 现需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢?主要的改动有两处:第一处是修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数;第二处是在 check() 函数中添加新的告警逻辑。具体的代码改动如下所示:


public class Alert {// ...省略AlertRule/Notification属性和构造函数...// 改动一:添加参数timeoutCountpublic void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {long tps = requestCount / durationOfSeconds;if (tps > rule.getMatchedRule(api).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}// 改动二:添加接口超时处理逻辑long timeoutTps = timeoutCount / durationOfSeconds;if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}}
}

(3). 问题:一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 check() 函数,相应的单元测试都需要修

(4). 上面的代码改动是基于“修改”的方式来实现新功能的。如果我们遵循开闭原则,也就是“对扩展开放、对修改关闭”。那如何通过“扩展”的方式,来实现同样的功能呢?

  1. 第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类;
  2. 第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中

(5). 具体实现


public class Alert {private List<AlertHandler> alertHandlers = new ArrayList<>();public void addAlertHandler(AlertHandler alertHandler) {this.alertHandlers.add(alertHandler);}public void check(ApiStatInfo apiStatInfo) {for (AlertHandler handler : alertHandlers) {handler.check(apiStatInfo);}}
}public class ApiStatInfo {//省略constructor/getter/setter方法private String api;private long requestCount;private long errorCount;private long durationOfSeconds;
}public abstract class AlertHandler {protected AlertRule rule;protected Notification notification;public AlertHandler(AlertRule rule, Notification notification) {this.rule = rule;this.notification = notification;}public abstract void check(ApiStatInfo apiStatInfo);
}public class TpsAlertHandler extends AlertHandler {public TpsAlertHandler(AlertRule rule, Notification notification) {super(rule, notification);}@Overridepublic void check(ApiStatInfo apiStatInfo) {long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}}
}public class ErrorAlertHandler extends AlertHandler {public ErrorAlertHandler(AlertRule rule, Notification notification){super(rule, notification);}@Overridepublic void check(ApiStatInfo apiStatInfo) {if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}}
}

(6). 使用:


public class ApplicationContext {private AlertRule alertRule;private Notification notification;private Alert alert;public void initializeBeans() {alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码notification = new Notification(/*.省略参数.*/); //省略一些初始化代码alert = new Alert();alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));}public Alert getAlert() { return alert; }// 饿汉式单例private static final ApplicationContext instance = new ApplicationContext();private ApplicationContext() {instance.initializeBeans();}public static ApplicationContext getInstance() {return instance;}
}public class Demo {public static void main(String[] args) {ApiStatInfo apiStatInfo = new ApiStatInfo();// ...省略设置apiStatInfo数据值的代码ApplicationContext.getInstance().getAlert().check(apiStatInfo);}
}
  1. 如何做到“对扩展开放、修改关闭”?

实际上,开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。所以,问如何才能做到“对扩展开放、对修改关闭”,也就粗略地等同于在问,如何才能写出扩展性好的代码

在讲具体的方法论之前,我们先来看一些更加偏向顶层的指导思想。为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。

  1. 在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”

  2. 还有,在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改

  3. 如何在项目中灵活应用开闭原则?

  1. 写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点
  2. 最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求
  3. Alert 告警的例子中,如果告警规则并不是很多、也不复杂,那 check() 函数中的 if 语句就不会很多,代码逻辑也不复杂,代码行数也不多,那最初的第一种代码实现思路简单易读,就是比较合理的选择。相反,如果告警规则很多、很复杂,check() 函数的 if 语句、代码逻辑就会很多、很复杂,相应的代码行数也会很多,可读性、可维护性就会变差,那重构之后的第二种代码实现思路就是更加合理的选择了。总之,这里没有一个放之四海而皆准的参考标准,全凭实际的应用场景来决定
  1. 如何理解“对扩展开放、对修改关闭”?

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”

三. 里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?

  1. 概念:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
  2. 例子:

public class Transporter {private HttpClient httpClient;public Transporter(HttpClient httpClient) {this.httpClient = httpClient;}public Response sendRequest(Request request) {// ...use httpClient to send request}
}public class SecurityTransporter extends Transporter {private String appId;private String appToken;public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {super(httpClient);this.appId = appId;this.appToken = appToken;}@Overridepublic Response sendRequest(Request request) {if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {request.addPayload("app-id", appId);request.addPayload("app-token", appToken);}return super.sendRequest(request);}
}public class Demo {    public void demoFunction(Transporter transporter) {    Reuqest request = new Request();//...省略设置request中数据值的代码...Response response = transporter.sendRequest(request);//...省略其他逻辑...}
}// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););
  1. 区别:

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

  1. 子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。

  2. 违背LSP—子类违背父类声明要实现的功能

父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。

  1. 子类违背父类对输入、输出、异常的约定
  2. 子类违背父类注释中所罗列的任何特殊说明

子类违背父类注释中所罗列的任何特殊说明父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

四.接口隔离原则有哪三种应用?原则中的“接口”该如何理解?

  1. 接口隔离原则:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者
  2. 把“接口”理解为一组 API 接口集合

public interface UserService {boolean register(String cellphone, String password);boolean login(String cellphone, String password);UserInfo getUserInfoById(long id);UserInfo getUserInfoByCellphone(String cellphone);
}public class UserServiceImpl implements UserService {//...
}

照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用


public interface UserService {boolean register(String cellphone, String password);boolean login(String cellphone, String password);UserInfo getUserInfoById(long id);UserInfo getUserInfoByCellphone(String cellphone);
}public interface RestrictedUserService {boolean deleteUserByCellphone(String cellphone);boolean deleteUserById(long id);
}public class UserServiceImpl implements UserService, RestrictedUserService {// ...省略实现代码...
}

五. 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?

1. 控制反转(IOC)
  1. 控制反转:
  1. 框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。
  2. 这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架
2. 依赖注入
  1. 不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
  2. 依赖注入 && 非依赖注入区别例子

// 非依赖注入实现方式
public class Notification {private MessageSender messageSender;public Notification() {this.messageSender = new MessageSender(); //此处有点像hardcode}public void sendMessage(String cellphone, String message) {//...省略校验逻辑等...this.messageSender.send(cellphone, message);}
}public class MessageSender {public void send(String cellphone, String message) {//....}
}
// 使用Notification
Notification notification = new Notification();// 依赖注入的实现方式
public class Notification {private MessageSender messageSender;// 通过构造函数将messageSender传递进来public Notification(MessageSender messageSender) {this.messageSender = messageSender;}public void sendMessage(String cellphone, String message) {//...省略校验逻辑等...this.messageSender.send(cellphone, message);}
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);
3. 依赖注入框架
  1. 依赖注入:

public class Demo {public static final void main(String args[]) {MessageSender sender = new SmsSender(); //创建对象Notification notification = new Notification(sender);//依赖注入notification.sendMessage("13918942177", "短信验证码:2346");}
}
4. 依赖反转原则(DIP)
  1. 依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象

六. 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?

1. 如何理解“KISS 原则”?
  1. 尽量保持简单
2. 代码行数越少就越“简单”吗?
  1. 功能描述:检查输入的字符串 ipAddress 是否是合法的 IP 地址。
  2. 例子:

// 第一种实现方式: 使用正则表达式
public boolean isValidIpAddressV1(String ipAddress) {if (StringUtils.isBlank(ipAddress)) return false;String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";return ipAddress.matches(regex);
}// 第二种实现方式: 使用现成的工具类
public boolean isValidIpAddressV2(String ipAddress) {if (StringUtils.isBlank(ipAddress)) return false;String[] ipUnits = StringUtils.split(ipAddress, '.');if (ipUnits.length != 4) {return false;}for (int i = 0; i < 4; ++i) {int ipUnitIntValue;try {ipUnitIntValue = Integer.parseInt(ipUnits[i]);} catch (NumberFormatException e) {return false;}if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {return false;}if (i == 0 && ipUnitIntValue == 0) {return false;}}return true;
}// 第三种实现方式: 不使用任何工具类
public boolean isValidIpAddressV3(String ipAddress) {char[] ipChars = ipAddress.toCharArray();int length = ipChars.length;int ipUnitIntValue = -1;boolean isFirstUnit = true;int unitsCount = 0;for (int i = 0; i < length; ++i) {char c = ipChars[i];if (c == '.') {if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;if (isFirstUnit && ipUnitIntValue == 0) return false;if (isFirstUnit) isFirstUnit = false;ipUnitIntValue = -1;unitsCount++;continue;}if (c < '0' || c > '9') {return false;}if (ipUnitIntValue == -1) ipUnitIntValue = 0;ipUnitIntValue = ipUnitIntValue * 10 + (c - '0');}if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;if (unitsCount != 3) return false;return true;
}
  1. 尽管第三种实现方式性能更高些,但我还是更倾向于选择第二种实现方法。那是因为第三种实现方式实际上是一种过度优化。除非 isValidIpAddress() 函数是影响系统性能的瓶颈代码,否则,这样优化的投入产出比并不高,增加了代码实现的难度、牺牲了代码的可读性,性能上的提升却并不明显。
3. 代码逻辑复杂就违背 KISS 原则吗?
4. 如何写出满足 KISS 原则的代码?
  1. 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

七. 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?

1. DRY 原则
  1. 三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。这三种代码重复,有的看似违反 DRY,实际上并不违反;有的看似不违反,实际上却违反了
2. 实现逻辑重复
  1. 例如:校验用户姓名合法性方法isValidUsername(),校验密码合法性方法isValidPassword(),部分代码重复,合并isValidUserNameOrPassword()。
  2. 合并之后的 isValidUserNameOrPassword() 函数,负责两件事情:验证用户名和验证密码,违反了“单一职责原则”和“接口隔离原则”。实际上,即便将两个函数合并成 isValidUserNameOrPassword(),代码仍然存在问题
  3. 尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。比如将校验只包含 az、09、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。
3. 功能语义重复
  1. 例如:在同一个项目中会有两个功能相同的函数,那是因为这两个函数是由两个不同的同事开发的,其中一个同事在不知道已经有了 isValidIp() 的情况下,自己又定义并实现了同样用来校验 IP 地址是否合法的 checkIfIpValid() 函数。
4. 代码执行重复
  1. 例如:UserService 中 login() 函数用来校验用户登录是否成功。如果失败,就返回异常;如果成功,就返回用户信息
  2. 代码:

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...}
}

3.分析:就是在 login() 函数中,email 的校验逻辑被执行了两次。一次是在调用 checkIfUserExisted() 函数的时候,另一次是调用 getUserByEmail() 函数的时候。这个问题解决起来比较简单,我们只需要将校验逻辑从 UserRepo 中移除,统一放到 UserService 中就可以了。

5. 代码复用性
  1. 代码复用 && 代码复用性 && DRY原则
  2. 代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码。代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用。DRY 原则是一条原则:不要写重复的代码。
6. 怎么提高代码复用性
  1. 减少代码耦合

对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合

  1. 满足单一职责原则

如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。

  1. 模块化

这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统

  1. 业务与非业务逻辑分离
  2. 通用代码下沉

从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层

  1. 继承、多态、抽象、封装
  2. 应用模板等设计模式

八.理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?

1. 何为“高内聚、松耦合”?
  1. 高内聚、松耦合

在这个设计思想中,“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。不过,这两者并非完全独立不相干。高内聚有助于松耦合,松耦合又需要高内聚的支持

  1. 高内聚

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护

  1. 松耦合

所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。实际上,我们前面讲的依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合

2. “迪米特法则”理论描述
  1. 迪米特法则理论翻译:

不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。

3.“迪米特法则”理论之前半部分—{不该有直接依赖关系的类之间,不要有依赖}
  1. 例如:NetworkTransporter 类负责底层网络通信,根据请求获取数据;HtmlDownloader 类用来通过 URL 获取网页;Document 表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象。
  2. 例子代码:
// ===> 负责底层网络通信,根据请求获取数据
public class NetworkTransporter {// 省略属性和其他方法...public Byte[] send(HtmlRequest htmlRequest) {//...}
}// ===> 通过 URL 获取网页
public class HtmlDownloader {private NetworkTransporter transporter;//通过构造函数或IOC注入public Html downloadHtml(String url) {Byte[] rawHtml = transporter.send(new HtmlRequest(url));return new Html(rawHtml);}
}// ===> 网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象
public class Document {private Html html;private String url;public Document(String url) {this.url = url;HtmlDownloader downloader = new HtmlDownloader();this.html = downloader.downloadHtml(url);}//...
}
  1. NetworkTransporter 类分析:

作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象 HtmlRequest。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest 类。
我们应该把 address 和 content 交给 NetworkTransporter,而非是直接把 HtmlRequest 交给NetworkTransporter。

  1. NetworkTransporter 类重构代码

public class NetworkTransporter {// 省略属性和其他方法...public Byte[] send(String address, Byte[] data) {//...}
}
  1. HtmlDownloader 类分析:

这个类的设计没有问题。不过,我们修改了 NetworkTransporter 的 send() 函数的定义,而这个类用到了 send() 函数,所以我们需要对它做相应的修改,修改后的代码如下所示


public class HtmlDownloader {private NetworkTransporter transporter;//通过构造函数或IOC注入// HtmlDownloader这里也要有相应的修改public Html downloadHtml(String url) {HtmlRequest htmlRequest = new HtmlRequest(url);Byte[] rawHtml = transporter.send(htmlRequest.getAddress(), htmlRequest.getContent().getBytes());return new Html(rawHtml);}
}
  1. Document 类分析

(1). 构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。
(2). HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性
(3). 从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则

  1. Document类重构

public class Document {private Html html;private String url;public Document(String url, Html html) {this.html = html;this.url = url;}//...
}// 通过一个工厂方法来创建Document
public class DocumentFactory {private HtmlDownloader downloader;public DocumentFactory(HtmlDownloader downloader) {this.downloader = downloader;}public Document createDocument(String url) {Html html = downloader.downloadHtml(url);return new Document(url, html);}
}
4.“迪米特法则”理论之后半部分—{有依赖关系的类之间,尽量只依赖必要的接口}
  1. 例子:Serialization 类序列化和反序列化

public class Serialization {public String serialize(Object object) {String serializedResult = ...;//...return serializedResult;}public Object deserialize(String str) {Object deserializedResult = ...;//...return deserializedResult;}
}
  1. 分析

单看这个类的设计,没有一点问题。不过,如果我们把它放到一定的应用场景里,那就还有继续优化的空间。假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口

  1. 拆分类:一个只负责序列化(Serializer 类),一个只负责反序列化(Deserializer 类)

public class Serializer {public String serialize(Object object) {String serializedResult = ...;...return serializedResult;}
}public class Deserializer {public Object deserialize(String str) {Object deserializedResult = ...;...return deserializedResult;}
}
  1. 拆分后分析

尽管拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设计思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至于过于分散。对于刚刚这个例子来说,如果我们修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要一并修改。在未拆分的情况下,我们只需要修改一个类即可。在拆分之后,我们需要修改两个类。显然,这种设计思路的代码改动范围变大了。

  1. 改进

如果我们既不想违背高内聚的设计思想,也不想违背迪米特法则,那我们该如何解决这个问题呢?实际上,通过引入两个接口就能轻松解决这个问题

  1. 改进实现

public interface Serializable {String serialize(Object object);
}public interface Deserializable {Object deserialize(String text);
}public class Serialization implements Serializable, Deserializable {@Overridepublic String serialize(Object object) {String serializedResult = ...;...return serializedResult;}@Overridepublic Object deserialize(String str) {Object deserializedResult = ...;...return deserializedResult;}
}public class DemoClass_1 {private Serializable serializer;public Demo(Serializable serializer) {this.serializer = serializer;}//...
}public class DemoClass_2 {private Deserializable deserializer;public Demo(Deserializable deserializer) {this.deserializer = deserializer;}//...
}
  1. 总结

实际上,上面的的代码实现思路,也体现了“基于接口而非实现编程”的设计原则,结合迪米特法则,我们可以总结出一条新的设计原则,那就是“基于最小接口而非最大实现编程”

5. 辩证思考与灵活应用
  1. 整个类只包含序列化和反序列化两个操作,只用到序列化操作的使用者,即便能够感知到仅有的一个反序列化函数,问题也不大。那为了满足迪米特法则,我们将一个非常简单的类,拆分出两个接口,是否有点过度设计的意思呢?

  2. 设计原则本身没有对错,只有能否用对之说。不要为了应用设计原则而应用设计原则,我们在应用设计原则的时候,一定要具体问题具体分析。

  3. 对于刚刚这个 Serialization 类来说,只包含两个操作,确实没有太大必要拆分成两个接口。但是,如果我们对 Serialization 类添加更多的功能,实现更多更好用的序列化、反序列化函数,我们来重新考虑一下这个问题。修改之后的具体的代码如下:


public class Serializer { // 参看JSON的接口定义public String serialize(Object object) { //... }public String serializeMap(Map map) { //... }public String serializeList(List list) { //... }public Object deserialize(String objectString) { //... }public Map deserializeMap(String mapString) { //... }public List deserializeList(String listString) { //... }
}
  1. 改进分析

在这种场景下,第二种设计思路要更好些。因为基于之前的应用场景来说,大部分代码只需要用到序列化的功能。对于这部分使用者,没必要了解反序列化的“知识”,而修改之后的 Serialization 类,反序列化的“知识”,从一个函数变成了三个。一旦任一反序列化操作有代码改动,我们都需要检查、测试所有依赖 Serialization 类的代码是否还能正常工作。为了减少耦合和测试工作量,我们应该按照迪米特法则,将反序列化和序列化的功能隔离开来。

6. 总结
  1. 单一职责原则

适用对象:模块,类,接口
侧重点:高内聚,低耦合
思考角度:自身

  1. 接口隔离原则

适用对象:接口,函数
侧重点:低耦合
思考角度:调用者

  1. 基于接口而非实现编程

适用对象:接口,抽象类
侧重点:低耦合
思考角度:调用者

  1. 迪米特法则

适用对象:模块,类
侧重点:低耦合
思考角度:类关系

九. 实战一(上):针对业务系统的开发,如何做需求分析和设计

1. 需求分析
  1. 积分系统概述

积分系统无外乎就两个大的功能点,一个是赚取积分,另一个是消费积分。赚取积分功能包括积分赚取渠道,比如下订单、每日签到、评论等;还包括积分兑换规则,比如订单金额与积分的兑换比例,每日签到赠送多少积分等。消费积分功能包括积分消费渠道,比如抵扣订单金额、兑换优惠券、积分换购、参与活动扣积分等;还包括积分兑换规则,比如多少积分可以换算成抵扣订单的多少金额,一张优惠券需要多少积分来兑换等等

  1. 积分赚取和兑换规则

积分的赚取渠道包括:下订单、每日签到、评论等。积分兑换规则可以是比较通用的。比如,签到送 10 积分。再比如,按照订单总金额的 10% 兑换成积分,也就是 100 块钱的订单可以积累 10 积分。除此之外,积分兑换规则也可以是比较细化的。比如,不同的店铺、不同的商品,可以设置不同的积分兑换比例

  1. 积分消费和兑换规则

积分的消费渠道包括:抵扣订单金额、兑换优惠券、积分换购、参与活动扣积分等。我们可以根据不同的消费渠道,设置不同的积分兑换规则。比如,积分换算成消费抵扣金额的比例是 10%,也就是 10 积分可以抵扣 1 块钱;100 积分可以兑换 15 块钱的优惠券等。

  1. 积分及其明细查询

查询用户的总积分,以及赚取积分和消费积分的历史记录。

2. 系统设计
  1. 合理地将功能划分到不同模块---------原则:高内聚、低耦合

(1). 划分方式一

积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护(增删改查),不划分到积分系统中,而是放到更上层的----营销系统----中。这样积分系统就会变得非常简单,只需要负责增加积分、减少积分、查询积分、查询积分明细等这几个工作

(2). 划分方式二

积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护,分散在各个相关业务系统中,比如订单系统、评论系统、签到系统、换购商城、优惠券系统等。还是刚刚那个下订单赚取积分的例子,在这种情况下,用户下订单成功之后,订单系统根据商品对应的积分兑换比例,计算所能兑换的积分数量,然后直接调用积分系统给用户增加积分。

(3). 划分方式三

所有的功能都划分到积分系统中,包括积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护。还是同样的例子,用户下订单成功之后,订单系统直接告知积分系统订单交易成功,积分系统根据订单信息查询积分兑换规则,给用户增加积分

  1. 比较

(1). 我们可以反过来通过看它是否符合高内聚、低耦合特性来判断。如果一个功能的修改或添加,经常要跨团队、跨项目、跨系统才能完成,那说明模块划分的不够合理,职责不够清晰,耦合过于严重。

(2). 为了避免业务知识的耦合,让下层系统更加通用,一般来讲,我们不希望下层系统(也就是被调用的系统)包含太多上层系统(也就是调用系统)的业务信息,但是,可以接受上层系统包含下层系统的业务信息。比如,订单系统、优惠券系统、换购商城等作为调用积分系统的上层系统,可以包含一些积分相关的业务信息。但是,反过来,积分系统中最好不要包含太多跟订单、优惠券、换购等相关的信息。

不管选择这两种中的哪一种,积分系统所负责的工作是一样的,只包含积分的增、减、查询,以及积分明细的记录和查询。

  1. 设计模块与模块之间的交互关系

(1). 常见系统之间的交互方式:一种是同步接口调用,另一种是利用消息中间件异步调用。第一种方式简单直接,第二种方式的解耦效果更好

  1. 设计模块的接口、数据库、业务模型

十. 实战一(下):如何实现一个遵从设计原则的积分兑换系统?

1. 业务开发包括哪些工作?
  1. 业务系统的设计与开发

无外乎有这样三方面的工作要做:接口设计、数据库设计和业务模型设计(也就是业务逻辑)

  1. 重要性

数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。改动数据库表结构,需要涉及数据的迁移和适配;改动接口,需要推动接口的使用者作相应的代码修改。这两种情况,即便是微小的改动,执行起来都会非常麻烦。因此,我们在设计接口和数据库的时候,一定要多花点心思和时间,切不可过于随意。相反,业务逻辑代码侧重内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,所以对改动的容忍性更大。

2. 数据库设计
  1. 积分明细表
3. 接口设计
  1. 设计积分系统的接口

接口设计要符合单一职责原则,粒度越小通用性就越好。但是,接口粒度太小也会带来一些问题。比如,一个功能的实现要调用多个小接口,一方面如果接口调用走网络(特别是公网),多次远程接口调用会影响性能;另一方面,本该在一个接口中完成的原子操作,现在分拆成多个小接口来完成,就可能会涉及分布式事务的数据一致性问题(一个接口执行成功了,但另一个接口执行失败了)。所以,为了兼顾易用性和性能,我们可以借鉴 facade(外观)设计模式,在职责单一的细粒度接口之上,再封装一层粗粒度的接口给外部使用

  1. 接口例子
4.业务模型设计
  1. DDD && OOP

前面我们还提到两种开发模式,基于贫血模型的传统开发模式和基于充血模型的 DDD 开发模式。前者是一种面向过程的编程风格,后者是一种面向对象的编程风格。不管是 DDD 还是 OOP,高级开发模式的存在一般都是为了应对复杂系统,应对系统的复杂性。对于我们要开发的积分系统来说,因为业务相对比较简单,所以,选择简单的基于贫血模型的传统开发模式就足够了

5. 为什么要分 MVC 三层开发
  1. 分层能起到代码复用的作用
  2. 分层能起到隔离变化的作用
  3. 分层能起到隔离关注点的作用

三层之间的关注点不同,分层之后,职责分明,更加符合单一职责原则,代码的内聚性更好。

  1. 分层能提高代码的可测试性
  2. 分层能应对系统的复杂性
6. BO、VO、Entity 存在的意义是什么?
  1. 含义

针对 Controller、Service、Repository 三层,每层都会定义相应的数据对象,它们分别是 VO(View Object)、BO(Business Object)、Entity,例如 UserVo、UserBo、UserEntity。在实际的开发中,VO、BO、Entity 可能存在大量的重复字段,甚至三者包含的字段完全一样。在开发的过程中,我们经常需要重复定义三个几乎一样的类,显然是一种重复劳动

  1. 相对于每层定义各自的数据对象来说,是不是定义一个公共的数据对象更好些呢?

(1). 推荐每层都定义各自的数据对象这种设计思路

(2).原因一.

VO、BO、Entity 并非完全一样。比如,我们可以在 UserEntity、UserBo 中定义 Password 字段,但显然不能在 UserVo 中定义 Password 字段,否则就会将用户的密码暴露出去

(3).原因二.

VO、BO、Entity 三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样的。所以,也并不能算违背 DRY 原则。在前面讲到 DRY 原则的时候,针对这种情况,如果合并为同一个类,那也会存在后期因为需求的变化而需要再拆分的问题。

(4).原因三

为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。数据从下一层传递到上一层的时候,将下一层的数据对象转化成上一层的数据对象,再继续处理。虽然这样的设计稍微有些繁琐,每层都需要定义各自的数据对象,需要做数据对象之间的转化,但是分层清晰。对于非常大的项目来说,结构清晰是第一位的

  1. 既然 VO、BO、Entity 不能合并,那如何解决代码重复的问题呢?

(1). 从设计的角度来说,VO、BO、Entity 的设计思路并不违反 DRY 原则,为了分层清晰、减少耦合,多维护几个类的成本也并不是不能接受的

(2).使用继承,将公共的字段定义到父类中

(3).多用组合,少用继承”设计思想,使用组合抽取公共的类

  1. 代码重复问题解决了,那不同分层之间的数据对象该如何互相转化呢?

(1). 现象

Service 层从 Repository 层获取的 Entity 之后,将其转化成 BO,再继续业务逻辑的处理。所以,整个开发的过程会涉及“Entity 到 BO”和“BO 到 VO”这两种转化。

(2). 解决方法

最简单的转化方式是手动复制。自己写代码在两个对象之间,一个字段一个字段的赋值。但这样的做法显然是没有技术含量的低级劳动。Java 中提供了多种数据对象转化工具,比如 BeanUtils、Dozer 等,可以大大简化繁琐的对象转化工作。

十一. 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?

1. 项目背景

我们希望设计开发一个小的框架,能够获取接口调用的各种统计信息,比如,响应时间的最大值(max)、最小值(min)、平均值(avg)、百分位值(percentile)、接口调用次数(count)、频率(tps) 等,并且支持将统计结果以各种显示格式(比如:JSON 格式、网页格式、自定义显示格式等)输出到各种终端(Console 命令行、HTTP 网页、Email、日志文件、自定义输出终端等),以方便查看。

2. 需求分析
  1. 功能性需求分析

(1). 接口统计信息:包括接口响应时间的统计信息,以及接口调用次数的统计信息等。

(2). 统计信息的类型:max、min、avg、percentile、count、tps 等

(3). 统计信息显示格式:Json、Html、自定义显示格式。

(4). 统计信息显示终端:Console、Email、HTTP 网页、日志、自定义显示终端

(5). 统计触发方式:包括主动和被动两种。主动表示以一定的频率定时统计数据,并主动推送到显示终端,比如邮件推送。被动表示用户触发统计,比如用户在网页中选择要统计的时间区间,触发统计,并将结果显示给用户

(6). 统计时间区间:框架需要支持自定义统计时间区间,比如统计最近 10 分钟的某接口的 tps、访问次数,或者统计 12 月 11 日 00 点到 12 月 12 日 00 点之间某接口响应时间的最大值、最小值、平均值等

(7). 统计时间间隔:对于主动触发统计,我们还要支持指定统计时间间隔,也就是多久触发一次统计显示。比如,每间隔 10s 统计一次接口信息并显示到命令行中,每间隔 24 小时发送一封统计信息邮件

3. 框架设计
  1. 针对需求做框架设计

对于稍微复杂系统的开发,很多人觉得不知从何开始。我个人喜欢借鉴 TDD(测试驱动开发)和 Prototype(最小原型)的思想,先聚焦于一个简单的应用场景,基于此设计实现一个简单的原型

004.设计原则与思想:设计原则相关推荐

  1. 设计原则与思想:设计原则

    这里写目录标题 理论一:对于单一职责原则,如何判断某个类的职责是否够单一 理论二:如何做到"对扩展开放,修改关闭?扩展和修改各指什么?" 理论三:里式替换(LSP)跟多态有何区别? ...

  2. 《终极海报——23位创意大咖的设计评论与思想》目录—导读

    致 终极海报--23位创意大咖的设计评论与思想 致我的侄子卡伊 他帮助我以如此多新奇的视角 去观察这个世界 序 终极海报--23位创意大咖的设计评论与思想 设计师们都在想什么?呃,我们永远也不会知道. ...

  3. 【软件架构】软件架构设计常用概念、原则与思想

    导读 本文一文总结软件架构设计常用概念.原则与思想,包括面向对象六大原则,DID原则,ACID.CAP.BASE理论,中间层思想,缓存思想等. 软件架构设计常用概念.原则与思想 面向对象设计六大原则 ...

  4. 设计原则与思想:设计原则12讲

    文章目录 设计原则与思想:设计原则(12讲) 理论一:对于单一职责原则,如何判定某个类的职责是否够"单一"? 如何理解单一职责原则(SRP)? 如何判断类的职责是否足够单一? 类的 ...

  5. 【设计模式之美 设计原则与思想:设计原则】22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?

    今天,我们讲最后一个设计原则:迪米特法则.尽管它不像 SOLID.KISS.DRY 原则那样,人尽皆知,但它却非常实用.利用这个原则,能够帮我们实现代码的"高内聚.松耦合".今天, ...

  6. 【设计模式之美 设计原则与思想:设计原则】23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?

    对于一个工程师来说,如果要追求长远发展,你就不能一直只把自己放在执行者的角色,不能只是一个代码实现者,你还要有独立负责一个系统的能力,能端到端(end to end)开发一个完整的系统.这其中的工作就 ...

  7. 设计原则与思想:规范和重构(11讲)

    文章目录 设计原则与思想:规范和重构(11讲) 理论一:什么情况下要重构?到底重构什么?又该如何重构? 重构的目的:为什么要重构(why)? 重构的对象:到底重构什么(what)? 重构的时机:什么时 ...

  8. 设计原则与思想【面向对象、设计原则、编程规范、重构技巧】

    一.高质量代码的评判标准: 可维护性:在不破化原有代码设计.不引入新的bug的情况下,能够快速的修改或者添加代码 可读性:我们需要看代码是否符合编码规范.命名是否达意.注释是否详尽.函数是否长短合适. ...

  9. 面向对象思想设计原则及常见设计模式

    面向对象思想设计原则 •在实际的开发中,我们要想更深入的了解面向对象思想,就必须熟悉前人总结过的面向对象的思想的设计原则 •单一职责原则 •开闭原则 •里氏替换原则 •依赖注入原则 •接口分离原则 • ...

最新文章

  1. python输入学号返回成绩_Python模拟登陆正方教务系统并抓取成绩单
  2. ssm三大框架工作原理_蒸发器最常见的三大工作原理
  3. 大话数据结构:最短路径算法
  4. 小白浅论JAVA数组中“for加强版”
  5. java 分词词频_利用word分词来对文本进行词频统计
  6. Codeforces Beta Round #71 C【KMP+DP】
  7. 设计模式之二-Proxy模式
  8. 阅读邮件回复邮件计算机操作题,《电子邮件》阅读练习题附答案
  9. 英特尔发布CPU新架构,突破性采用3D堆栈法
  10. 在面向服务的设计时有四个原则:
  11. 会话推荐和 序列推荐
  12. android studio 工程rebuild没反应,Andriod Studio Clear Project或Rebuild Project出错
  13. 他们都来全球边缘计算大会了,就差你了!
  14. c语言写的贪吃蛇在哪能运行,有没有可以帮我写c语言贪吃蛇的代码 cmd运行的
  15. vant area地区选择组件使用方法
  16. java计算机毕业设计智能外包管理平台源码+数据库+系统+lw文档
  17. ISO三体系,招投标企业认证最多的资质
  18. Redis实现点赞功能
  19. 2021年scrapy一分钟内爬取笔趣阁整本小说存为txt|解决章节排序问题
  20. 使用R语言绘制心形图

热门文章

  1. python excel 格式刷_巧用格式刷解决合并单元格无法统计问题
  2. Android 源码 图形系统概述
  3. 成功解决RuntimeWarning: All-NaN slice encountered vmin = np.nanmin(calc_data)
  4. 对类似于word目录编号(1,1.1,1.1.1)的排序方法
  5. 淘系技术发布业界首个基于神经渲染的3D建模产品Object Drawer,推理速度均提升10000倍...
  6. Win7蓝屏代码0x00000ed怎么解决
  7. 【常用算法】散列(hash)
  8. element日期插件datePick限制日期限制简单案例
  9. 计算机英语阅卷图片,标准『曝光』!英语阅卷组是这样给分的!
  10. qt判断用户是否正在使用当前应用程序