我们要写出好的代码,其前提是要知道“好”和“烂”定义的标准是什么,然后才能在写代码的时候,去设计一份好的代码。
如何定义“好”的代码?
好和坏是一个比较笼统的概率,代码质量高低是一个综合各种因素得到的结论,并不能通过单一的维度去评价一段代码写的好坏。对一段代码的质量评价,常常有很强的主观性。比如,怎么样的代码才算可读性好,每个人的评判标准都不大一样。
列举一些常见的代码评价标准:
定义 | 解释 |
---|---|
可维护性、可扩展性 | 能够在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。(对修改关闭,对扩展开放) |
可读性、简洁性 | 思从深而行从简,目的是让他人可以轻松读懂你的代码。列如命名是否规范、注释是否详尽、函数长度是否合适等 |
可复用性 | 构建可复用的代码,减少重复代码的产出 |
可测试性 | 代码的单测编写时较为方便,能从侧面上反应代码质量的好坏 |
灵活性、模块化、健壮性…… | …… |
如何让代码符合“好”的标准?
要写出高质量代码,需要掌握一些能落地、够细化的编程方法论,这其中就包含编码规范、重构技巧、面向对象设计思想、设计原则、设计模式等等内容,这是平常的工作和学习中,需要去积累的。
面向对象
特性
面向对象的四大特性:封装、继承、多态[、抽象]
封装(Encapsulation):
作用:类通过暴露有限的访问接口,授权外部仅能通过指定的方式来访问内部信息或者数据,实现隐藏信息、保护数据的目的。其实现的基础是访问权限的控制。
继承(Inheritance):
实现代码复用的目的。但子类和父类高度耦合,修改父类的代码,会直接影响到子类。
多态(Polymorphism):
提高代码的可扩展性和复用性。
子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态的实现,在 Java 中可以利用“extends + Override ”方式,或者可以利用 “implements + Override”方式,在Python 中 可以利用 duck-typing来实现多态。
eg:
class Logger:
def record(self):
print(“I write a log into file.”)
class DB:
def record(self):
print(“I insert data into db. ”)
def test(recorder):
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)
抽象(Abstraction):
隐藏方法的具体实现,让调用者只关心提供的功能,不需要知道实现细节。如 interface 与 abstract
待改进的面向过程设计思维
1. getter、setter方法需要按情况添加
public class ShoppingCart {
private int itemsCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemsCount() {
return this.itemsCount;
}
// public void setItemsCount(int itemsCount) {
// this.itemsCount = itemsCount;
// }
//
public double getTotalPrice() {
return this.totalPrice;
}
// public void setTotalPrice(double totalPrice) {
// this.totalPrice = totalPrice;
// }
public List<ShoppingCartItem> getUnmodifiableItems() {
return Collections.unmodifiableList(this.items);
}
public List<ShoppingCartItem> getItems() {
return this.items;
}
public void addItem(ShoppingCartItem item) {
items.add(item);
itemsCount++;
totalPrice += item.getPrice();
}
public void clear() {
items.clear();
itemsCount = 0;
totalPrice = 0.0;
}
// ...省略其他方法...
}
- itemsCount 和 totalPrice 不对外提供设值的方法,避免跟 items 属性的值不一致
- ShoppingCart 中提供 clear 方法,同步修改 itemsCount 和 totalPrice
2. Util 工具类,是否可以定义到父类中
设计原则
6大原则(SOLID)、DRY
1. S-SRP(Single Responsibility Principle)
一个类或者模块只负责完成一个职责(或者功能)。不写出大而全的类,这样不利于对后面进行扩展和阅读。
如何判断类的职责是否足够单一?
评价一个类的职责是否足够单一,并没有一个非常明确的、可以量化的标准,每个人都有自己的理解。所以,在设计类时,可以先写一个符合当前业务需求的类。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。
2. I-ISP(Interface Segregation Principle)
“接口隔离”有三种不同的理解:
-
如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
eg:当注销行为非用户可操作时,可将注销行为隔离到其他接口
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 { // ...省略实现代码... }
-
如果把“接口”理解为单个API接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
eg:打印日志时需要脱敏,将脱敏能力继续细化为一个函数
public class LogUtils { public static void info(Logger logger, Object... objs) { LogUtil.info(logger, ScanAndDesensUtil.scanAndDesensText()); } } public class ScanAndDesensUtil { public static StringBuilder scanAndDesensText(Object... text) { //...实现 } public static String scanAndDesensText(String text) { //...实现 } }
-
如果把“接口”理解为OOP中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
eg:定时更新配置 ScheduledUpdater 只依赖 Updater 接口,配置信息监控 SimpleHttpServer 只依赖 Viewer 接口
//配置热更新接口 public interface Updater { void update(); } //配置热更新任务 public class ScheduledUpdater { public ScheduleUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) { this.updater = updater; //... } public void run() { executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { updater.update(); } }, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS); } } //配置信息浏览接口 public interface Viewer { String outputInPlainText(); Map<String, String> output(); } //配置信息的添加与发送 public class SimpleHttpServer { private String host; private int port; private Map<String, List<Viewer>> viewers = new HashMap<>(); public SimpleHttpServer(String host, int port) {//...} public void addViewers(String urlDirectory, Viewer viewer) { if (!viewers.containsKey(urlDirectory)) { viewers.put(urlDirectory, new ArrayList<Viewer>()); } this.viewers.get(urlDirectory).add(viewer); } public void run() { //... } } public class RedisConfig implemets Updater, Viewer{ private ConfigSource configSource; //配置中心(比如zookeeper) private String address; //...省略其他配置 public RedisConfig(ConfigSource configSource) { this.configSource = configSource; } public String getAddress() { return this.address; } //...省略其他get()、init()方法... @Override public void update() { //从configSource加载配置到address/timeout/maxTotal... } } public class KafkaConfig implements Updater { //...省略... } public class MysqlConfig implements Viewer { //...省略... }
与单一职责原则相似,接口隔离原则更加细化,而单一职责原则,提出的是接口的设计思想。
3. O-OCP(Open Closed Principle)
对扩展开放,对修改关闭。添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
4. L-LSP(Liskov Substitution Principle)
派生的子类对象,可以替换任何基类出现的地方,并且保证子类和父类中的逻辑行为不变及正确性不被破坏。
eg:
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;
}
@Override
public Response sendRequest(Request request) {
//此处新增的逻辑,应当在Transporter或SecurityTransporter调用该方法时,具有一致的业务功能
if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
}
return super.sendRequest(request);
}
}
从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性、是一种语法、是一种代码实现的思路,而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的。
5. D-DIP(Dependency Inversion Principle)
IOC(Inversion Of Control)
通过框架,将程序中的流程“控制”权利,“反转”到框架(某个抽象类)中,实现通过框架统一代码流程。例如模板模式
DI(Dependency Injection)
不通过 new 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
Spring 的控制反转容器是通过 DI 来实现的。
DIP
简单点:可以理解为IOC的不同表达方式。
6. KISS(Keep it sample and stupid)
提倡用最简单的方法解决问题。如何保证代码符合 KISS 原则?
- 尽量使用低理解成本方式实现业务逻辑。如不使用复杂的正则表达式。
- 善于使用已有的工具类库,降低出 bug 的概率。
- 不过度优化,降低代码的可读性。
7. DRY(Don’t Repeat Yourself)
三种典型的代码重复情况:
-
实现逻辑重复:在保证 “SRP” 等原则的前提下,可以将重复逻辑进行重构。但如果函数的语义不同,但实际代码相同时,不能将其合并。
-
功能语义重复:函数名不同,但功能语义相同的函数,应当做合并重构。
-
代码执行重复:同一函数在流程中被不合理的多次执行。
eg:优化 login 函数。
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 usera;
}
}
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...
}
}
优化后:
public class UserService {
private UserRepo userRepo;//通过依赖注入或者IOC框架注入
public User login(String email, String password) {
if (!EmailValidation.validate(email)) {
// ... throw InvalidEmailException...
}
User user = userRepo.getUserByEmail(email);
if (user == null) {
// ... throw AuthenticationFailureException...
}
return usera;
}
}
public class UserRepo {
public User getUserByEmail(String email) {
//...query db to get user by email...
}
}
重构技巧
为什么要重构?
- 保证在业务不断迭代过程中的代码质量
重构什么?
重构大致分为大规模高层次的重构和小规模低层次的重构:
- 大规模主要针对代码结构、模块化、解耦、抽象复用组件等等;
- 小规模主要是针对类、函数级别的重构。
解耦
解耦将各个模块间的依赖简化,是控制代码复杂度的有效手段。
给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些设计原则,比如:单一职责原则、基于接口而非实现编程、依赖注入、少用继承(继承是一种强依赖关系)等
什么时候重构?
持续重构,将重构当做开发的一部分,避免因技术、业务、需求变动带来的代码质量持续下降问题,避免开发初期过度设计的问题。
如何重构?
- 控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候写一些兼容过渡代码。
- 保证每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。
重构的质量如何保证?
单测用例 + 集成测试用例
单测用例的作用:
- 写单元测试的过程就是Code Review和重构的过程,在单测中覆盖各种输入、异常、边界情况,能有效地发现代码中的bug和代码设计上的问题
- 单元测试是对集成测试的补充,是 TDD 可落地执行的改进方案
Q: 如何定义日常中的设计与过度设计?
编码规范
命名:
- 一致性:团队内,对于关键业务名称的命名需要统一,不同团队尽量使整个链路命名保持一致
- 贴合业务含义:保证命名有意义。例如:方法名需要明确方法的职责,可以比较长,但如果方法名太长,则表示方法中的内容较多,基于"职责单一"原则,考虑是否需要继续拆分
- 命名规范:
- 所有编程相关的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
- 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
- 类名使用UpperCamelCase风格,以下情形例外:DO / BO / DTO / VO / AO / UID等。
- 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格。
- 常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
- POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。
- 借鉴优秀的代码:通过阅读优秀的开源代码,尝试学习理解作者的命名思维、设计模式等。
合理使用 Optional:
- 优化判空能力
- orElse 与 orElseGet 的区别:
- orElse:每次都会执行其中的函数。
- orElseGet,只有Optional中的值为空时,它才会执行其中对应的函数。这样做的好处是可以避免提前计算结果的风险。
- 故orElse中常常指定一个固定的变量值返回,orElseGet中常用于指定一个函数来执行。
- 正确的换行:
- 方法调用的点符号与下文一起换行;
- 方法调用中的多个参数需要换行时,在逗号后进行。
合理使用 Lambda:
- 简化 for 循环
- 避免过长的匿名函数
- 隐式设置的局部 final 变量
- 抛出异常的影响:
- Lambda 中调用的方法如果抛出 CheckedException,那么必须捕获 exception,这会导致表达式特别累赘。所以,在业务场景允许的情况下,被 Lambda 调用的方法最好抛出 UncheckedException。
- 如果是外部接口申明了 CheckedException,可以考虑将外部接口封装一次,抛出 Runtime Exception。
- Exception 分类
讨论:
如何将代码方法论,更好的融入日常开发中?
设计模式
设计模式的本质就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。
我们需要掌握不同设计模式能解决的问题、应用场景。
创建型 - 工厂模式
定义:
将复杂的类创建和业务使用进行解耦。在需要使用时,动态地根据不同的类型,从 factory 中选取一个创建完成的类。
解决的问题:
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
- 代码复用:创建代码抽离到独立的工厂类之后可以复用。
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
- 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
工厂模式分为三种类型:简单工厂、工厂方法和抽象工厂。
简单工厂: 在 factory 中完成类的创建与选择
public class RuleConfigParserFactory {
private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
cachedParsers.put("properties", new PropertiesRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
return null;//返回null还是IllegalArgumentException全凭你自己说了算
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}
工厂方法: 相比于简单工厂,当类的创建、准备工作比较复杂时,可以考虑将创建的能力抽象到新的接口中。
/**
类创建接口
*/
public interface IRuleConfigParserFactory {
IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new JsonRuleConfigParser();
}
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new XmlRuleConfigParser();
}
}
/**因为工厂类只包含方法,不包含成员变量,完全可以复用,
*/
public class RuleConfigParserFactoryMap { //工厂的工厂
private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("json", new JsonRuleConfigParserFactory());
cachedFactories.put("xml", new XmlRuleConfigParserFactory());
cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
}
public static IRuleConfigParserFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}
//使用用例
IRuleConfigParserFactory parserFactory = ruleConfigParserFactoryMap.getParserFactory("xxx");
IRuleConfigParser parser = parserFactory.createParser();
抽象工厂:相比于工厂方法,抽象工厂让一个 factory 负责创建多个不同类型的对象
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
//此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new JsonRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new JsonSystemConfigParser();
}
}
创建型 - 建造者模式
定义
建造者模式是用来创建属性较多的复杂对象,通过设置不同的可选参数,“定制化”地创建对象。
与工厂模式相同,都是创建对象,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
解决的问题
如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合set()方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
- 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就会出现参数列表很长的问题,不易读,易出错。
- 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合set()方法的设计思路,那这些依赖关系或约束条件的校验逻辑对使用方来讲,是不方便的。
- 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露set()方法。构造函数配合set()方法来设置属性值的方式就不适用了。
用例:
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
//...省略getter方法,不能有setter方法...
//我们将Builder类设计成了ResourcePoolConfig的内部类。
//我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
public static class Builder {
//这些默认的值,放在Builder构建类中
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("...");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("...");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 使用用例:这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
结构型 - 代理模式
定义:
在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
解决的问题:
用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志、RPC、缓存。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。
eg:
**组合模式下的代理实现:**基于接口与实现类
public interface IUserController {
UserVo login(String telephone, String password);
}
/**
需要对系统中历史的 UserController 进行能力扩展
*/
public class UserController implements IUserController {
//...省略其他属性和方法...
@Override
public UserVo login(String telephone, String password) {
//...省略login逻辑...
//...返回UserVo数据...
}
}
public class UserControllerProxy implements IUserController {
private MetricsCollector metricsCollector;
private UserController userController;
public UserControllerProxy(UserController userController) {
this.userController = userController;
this.metricsCollector = new MetricsCollector();
}
@Override
public UserVo login(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
// 代理
UserVo userVo = userController.login(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
//UserControllerProxy使用举例
//因为原始类和代理类实现相同的接口,是基于接口而非实现编程,外部使用时只感知到接口层面,不必关心实现层
//将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController());
**继承方式下的代理:**继承与重写实现,适用于 UserController 无实现接口的场景
public class UserControllerProxy extends UserController {
private MetricsCollector metricsCollector;
public UserControllerProxy() {
this.metricsCollector = new MetricsCollector();
}
@Override
public UserVo login(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
UserVo userVo = super.login(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();
动态代理:
在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
行为型 - 观察者模式
有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
定义:
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,通过接口中的抽象方法,通知到所有依赖这个接口的对象。
通常,我们将观察者命名为Observer,被观察者命名为Observable
解决的问题:
将不同的行为逻辑,解耦到不同的类中去,让代码更加符合简单、单一职责原则。
eg:用户完成注册后,需要对该账户做一系列的额外操作:
扩展:
Google Guava EventBus 是观察者模式的一个通用框架,框架内通过 @Subscribe 标识 Observer 中的抽象能力,通过 EventBus#post(Object) 通知 Observer。
行为型 - 模板模式
定义:
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。它可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,可以理解为广义上的“业务逻辑”;算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
在模板模式经典的实现中,模板方法定义为final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
解决的问题:
1. 公共逻辑复用
模板模式把一个算法中不变的流程抽象到父类的模板方法(abstract method)中,将可变的部分留给子类实现。所有的子类都可以复用父类中模板方法定义的流程代码。eg:
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
//AbstractList类中,addAll()函数可以看作模板方法,add()是子类需要重写的方法,尽管没有声明为abstract的,但函数实现直接抛出了UnsupportedOperationException异常。
//意味着如果子类不重写add()方法,是不能使用的。
2. 方便扩展
指的是框架的扩展性,可以在不修改框架源码的情况下,定制化框架的功能。
与同步回调的对比:
从应用场景上来看,二者几乎一致,都可以实现代码复用和扩展;
从代码实现上来看,回调和模板模式完全不同。回调于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
基于组合优于继承的原则,在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点:
- 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们将不同模板需要的抽象方法放到定义的接口中,使用时只需要往用到的模板方法中注入接口的实现类即可。
行为型 - 策略模式
定义:
定义一族算法类,将每个算法分别封装起来,使算法的变化独立于使用它们的客户端。
解决的问题:
1. 将对策略(算法)的定义、创建、使用这三部分进行解耦。
- 定义(interface):包含一个策略接口和一组实现这个接口的策略类。
- 创建(factory):由工厂类来完成,封装策略创建的细节。
- 使用(service):包含一组策略可选,一般场景下,客户端在“运行时动态确定”选择使用哪个策略。
2. 控制代码的复杂度,让每个算法部分独立,且不至于过于复杂、代码量过多
eg:实现对一个文件进行排序的功能。文件中只包含整型数,并且,相邻的数字通过逗号来区隔。(重点在代码结构设计,不实现具体的算法)
/**
排序业务功能提供类
*/
public class SorterService {
private static final long GB = 1000 * 1000 * 1000;
private static final List<AlgRange> algs = new ArrayList<>();
static {
algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
}
//排序能力
public void sortFile(String filePath) {
// 省略校验逻辑
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg = null;
for (AlgRange algRange : algs) {
if (algRange.inRange(fileSize)) {
sortAlg = algRange.getAlg();
break;
}
}
sortAlg.sort(filePath);
}
//静态内部类,文件大小与对应排序算法的模型
private static class AlgRange {
private long start;
private long end;
private ISortAlg alg;
public AlgRange(long start, long end, ISortAlg alg) {
this.start = start;
this.end = end;
this.alg = alg;
}
public ISortAlg getAlg() {
return alg;
}
public boolean inRange(long size) {
return size >= start && size < end;
}
}
}
/**
工厂类,完成对象的创建与封装。
*/
public class SortAlgFactory {
private static final Map<String, ISortAlg> algs = new HashMap<>();
static {
algs.put("QuickSort", new QuickSort());
algs.put("ExternalSort", new ExternalSort());
algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
algs.put("MapReduceSort", new MapReduceSort());
}
public static ISortAlg getSortAlg(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return algs.get(type);
}
}
/**
排序能力抽象接口
*/
public interface ISortAlg {
void sort(String filePath);
}
public class QuickSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
行为型 - 职责链模式
定义:
多个处理器依次处理同一个请求。一个请求先经过A处理器处理,然后再把请求传递给B处理器,B处理器处理完后再传递给C处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
解决的问题
将多个同类型、不同功能的处理器实现拆分开,但通过数组或链表的方式将他们串在一起执行,保留了扩展能力,符合开闭原则,且让各个处理器中的代码变的更加简单。
链表实现方式:
//Handler及子类
public abstract class Handler {
protected Handler successor = null;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public final void handle() {
//当有nextHandle且返回为false时,
boolean handled = doHandle();
if (successor != null && !handled) {
successor.handle();
}
}
protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
//HandlerChain 构建
public class HandlerChain {
private Handler head = null;
private Handler tail = null;
public void addHandler(Handler handler) {
handler.setSuccessor(null);
if (head == null) {
head = handler;
tail = handler;
return;
}
tail.setSuccessor(handler);
tail = handler;
}
public void handle() {
if (head != null) {
head.handle();
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
数组实现方式:
public interface IHandler {
boolean handle();
}
public class HandlerA implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerChain {
private List<IHandler> handlers = new ArrayList<>();
public void addHandler(IHandler handler) {
this.handlers.add(handler);
}
public void handle() {
for (IHandler handler : handlers) {
boolean handled = handler.handle();
if (handled) {
//当处理器返回true时,链执行中断。
break;
}
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
参考文献:
《设计模式之美》
如何提高代码可读性
写好代码的建议