没有杯子的世界:OOP设计思想的应用实践
最近看到一个有趣的问题:Person类具有Hand,Hand可以操作杯子Cup,但是在石器时代是没有杯子的,这个问题用编程怎么解决?
简单代码实现
我们先用简单代码实现原问题:
@Data
public class Person {
private final String name;
private Hand hand = new Hand();
private Mouth mouth = new Mouth();
private static class Hand {
// 为了简化问题,用字符串表示复杂的方法实现,这些方法极有可能具有副作用
String holdCup() {
return "hold a cup...";
}
String refillCup() {
return "refill the coffee cup...";
}
}
private static class Mouth {
String drinkCoffee() {
return "take a cup of coffee";
}
}
public String drinkCoffee() {
return String.join("\n",
hand.refillCup(),
hand.holdCup(),
mouth.drink()
);
}
// 略去其他方法,run(), work(), eat()...
public static void main(String[] args) {
Person eric = new Person("Eric");
System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
}
}
良好的代码设计经常面向接口编程,我们抽取出接口如下:
public interface Person {
String drinkCoffee();
// 略去其他方法,run(), work(), eat()...
interface Hand {
String holdCup();
String refillCup();
}
interface Mouth {
String drinkCoffee();
}
}
@Data
public class DefaultPerson implements Person {
private final String name;
private Hand hand = new DefaultHand();
private Mouth mouth = new DefaultMouth();
private static class DefaultHand implements Hand {
@Override
public String holdCup() {
return "hold a cup...";
}
@Override
public String refillCup() {
return "refill the coffee cup...";
}
}
private static class DefaultMouth implements Mouth {
@Override
public String drinkCoffee() {
return "take a cup of coffee";
}
}
@Override
public String drinkCoffee() {
return String.join("\n",
hand.refillCup(),
hand.holdCup(),
mouth.drinkCoffee()
);
}
public static void main(String[] args) {
Person eric = new DefaultPerson("eric");
System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
}
}
完事具备,现在我们来思考下这个问题: 问题的关键在于drinkCoffee方法,现在这个方法调用的结果是不对的,因为方法的调用依据了 DefaultPerson 之外的变量,即是否处于石器时代。 我们先看一个不好的实现:
@Value
public class BadPersonImpl implements Person {
String name;
boolean isInStoneEra;
@Override
public String drinkCoffee() {
if (isInStoneEra) {
return String.format("%s cannot drink, because there is no cup in the era.", getName());
}
return "refill the coffee cup..." + "hold a cup..." + "take a cup of coffee.";
}
public static void main(String[] args) {
Person eric = new BadPersonImpl("Eric", true);
System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
}
}
这段代码的问题是所有的内容都写死了,所有的代码都在一块,无法复用和拓展。
当然,如果说本来 Person 的实现就简单,新需求并不多,用这种方法也不是不可以。
问题分析&解决方法
不过,大部分情况下如果我们最开始这么写,把自己的路堵死了,当有新需求时,之后的修改极有可能发展成 if-else 套娃地狱,一个方法越写越多,越写越乱, 逻辑复杂到自己把自己都绕死了,最后实在受不了了,重写整个方法或类。
为什么我的代码中新加了 Mouth 这个类?
因为如果Person中有Hand这个类,通常说明 Hand类 有自己独立的实现,行为比较复杂,Person 实现的行为比较复杂, 加入了 Mouth 是为了说明 Person 类的复杂性,Person 是一个抽象工厂。
正确的做法应该考虑设计中的变量和不变量:
- 人所处的时代是变化的,时代影响人的行为
- 人的行为可以独立变化,即人具有hand、mouth等,其使用各个组件进行某些行为。
- 人的组件hand、mouth可以独立变化
不变:
- 时代一旦确定就不会更改(无需使用状态模式)
- Person的组件一旦确定就不会更改
- Person 和 Era 独立扩展
由此我们得出结论,Person 和 Era 要实现解耦。
interface EraEnvironment {
default boolean hasCup() {
return true;
}
}
class ModernEra implements EraEnvironment {
}
class StoneAge implements EraEnvironment {
@Override
public boolean hasCup() {
return false;
}
}
// 基于组合的实现
@Value
class PersonInEra implements Person {
Person person;
EraEnvironment era;
@Override
public String drinkCoffee() {
if (era.hasCup()) {
return person.drinkCoffee();
}
return String.format("%s cannot drink, because there is no cup in the era.", person.getName());
}
@Override
public String getName() {
return person.getName();
}
public static void main(String[] args) {
PersonInEra eric = new PersonInEra(new DefaultPerson("Eric"), new StoneAge());
System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
}
}
进一步优化成协调者模式,可以保证各个 Colleague 类(Person、EraEnvironment)独立扩展。
如果以后还有影响 Person 行为的变量,比如天气、心情等,可以引入新的协调者。
可以看出,随着需求的增多,协调者可能越来越多,此时我们就需要重新进行分析,哪些条件可以看做Person的固有属性,对Person进行重构。
// 优化抽取出抽象类
class PersonInEra extends AbstractPersonInEra {
public PersonInEra(Person person, EraEnvironment era) {
super(person, era);
}
@Override
public String drinkCoffee() {
if (getEra().hasCup()) {
return getPerson().drinkCoffee();
}
return String.format("%s cannot drink, because there is no cup in the era.", getName());
}
public static void main(String[] args) {
PersonInEra eric = new PersonInEra(new DefaultPerson("Eric"), new StoneAge());
System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
}
}
public abstract class AbstractPersonInEra implements Person {
private final Person person;
private final EraEnvironment era;
public AbstractPersonInEra(Person person, EraEnvironment era) {
this.person = person;
this.era = era;
}
@Override
public String getName() {
return person.getName();
}
protected Person getPerson() {
return person;
}
protected EraEnvironment getEra() {
return era;
}
@Override
public abstract String drinkCoffee();
}
面向对象原则分析
当然,根据对需求的不同理解和对未来需求的预期,我们可能选择不同的实现,这个问题还有可能用状态模式、策略模式等实现,不同的方法有优点也有缺点; 如果在面试中遇到这样的问题,一定要跟面试官明确背景和需求。
我们使用面向对象的基本原则分析下改动前后的代码:
1.单一职责原则(SRP):一个类/方法应该只有一个职责。
满足。以 PersonInEra::drinkCoffee 为例,其只负责根据环境,对调用方法进行选择。
2.开放封闭原则(OCP):软件实体应该对扩展开放,对修改关闭。
满足。对扩展开发不必多说,使用接口或抽象类都方便了拓展。
3.里氏替换原则(LSP):子类对象应该能够替换其父类对象并保持系统的行为正确性。
满足。我们使用时声明类型为接口 Person,使用的实例为其具体实现。
4.依赖倒置原则(DIP):高层模块不应该依赖于底层模块,而是应该通过抽象进行交互。
满足。client 使用了Person, Person的不同实现间的依赖都是接口或抽象类。 一个实体类抽象出接口是一个万金油式的好方法。
5.接口隔离原则(ISP):一个类对另一个类的依赖应该建立在最小的接口上。
满足。比如 AbstractPersonInEra 依赖的是 Person接口,这个接口并不包含其他不必要的方法。
6.合成/聚合复用原则(CARP):优先使用对象合成或聚合,而不是继承来实现代码复用。
满足。AbstractPersonInEra 使用的是组合实现。
7.迪米特法则(LoD):一个对象应该对其它对象保持最小的了解。 满足。这里还是看出了使用接口的好处,AbstractPersonInEra 只知道自己依赖了 Person 和 EraEnvironment, 对于依赖对象的实现一无所知。
策略模式
最后,你可以自己写个策略模式,和我写的策略模式比较一下,从面向对象设计的角度分析其优劣。
使用策略模式编写的代码如下:
// 策略模式,不改变原 DefaultPerson 的实现
@FunctionalInterface
public interface DrinkStrategy {
String drink();
}
public final class Persons {
private Persons(){}
@NotNull
private static DrinkStrategy stoneEraSupport(Person person, EraEnvironment era) {
return () -> {
if (era.hasCup()) {
return person.drinkCoffee();
}
return String.format("%s cannot drink, because there is no cup in the era.", person.getName());
};
}
// 工厂方法创建复杂对象
@NotNull
public static Person stoneAgeSupportWithNameAndEra(String name, EraEnvironment era) {
DefaultPerson oriPerson = new DefaultPerson(name);
return new StrategicPerson(oriPerson, stoneEraSupport(oriPerson, era));
}
}
@Value
public class StrategicPerson implements Person {
// 使用组合
Person person;
// 支持多种策略,拓展性好
DrinkStrategy drinkStrategy;
@Override
public String drinkCoffee() {
return drinkStrategy.drink();
}
// 除需要更改的方法外,其他实现委托给原 Person. 比较烦的是:需要委托的方法多的话,都要单独编写方法
@Override
public String getName() {
return person.getName();
}
public static void main(String[] args) {
Person eric = Persons.stoneAgeSupportWithNameAndEra("eric", new StoneAge());
System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
}
}
没有杯子的世界:OOP设计思想的应用实践的更多相关文章
- 基于 CSP 的设计思想和 OOP 设计思想的异同
LinkerLin Go语言推崇的CSP编程模型和设计思想,并没有引起很多Go开发者包括Go标准库作者的重视.标准库的很多设计保留了很浓的OOP的味道.本篇Blog想比较下从设计的角度看,CSP和OO ...
- javascript OOP编辑思想的一个实践参考
<html> <style type="text/css"> .current { background-color: red; } .dv { backg ...
- 从一般分布式设计看HDFS设计思想与架构
要想深入学习HDFS就要先了解其设计思想和架构,这样才能继续深入使用HDFS或者深入研究源代码.懂得了"所以然"才能在实际使用中灵活运用.快速解决遇到的问题.下面这篇博文我们就先 ...
- Web Magic设计思想
1.1 设计思想 1. 一个框架,一个领域 一个好的框架必然凝聚了领域知识.WebMagic的设计参考了业界最优秀的爬虫Scrapy,而实现则应用了HttpClient.Jsoup等Java世界最成熟 ...
- python 面向对象设计思想发展史
这篇主要说的是程序设计思想发展历史,分为概述和详细发展历史 一,概述 1940年以前:面向机器 最早的程序设计都是采用机器语言来编写的,直接使用二进制码来表示机器能够识别和执行的 指令和数 据.简单来 ...
- len(x) 击败 x.len(),从内置函数看 Python 的设计思想
内置函数是 Python 的一大特色,用极简的语法实现很多常用的操作. 它们预先定义在内置命名空间中,开箱即用,所见即所得.Python 被公认是一种新手友好型的语言,这种说法能够成立,内置函数在其中 ...
- MyBatis 强大之处 多环境 多数据源 ResultMap 的设计思想是 缓存算法 跨数据库 spring boot rest api mybaits limit 传参
总结: 1.mybaits配置工2方面: i行为配置,如数据源的实现是否利用池pool的概念(POOLED – This implementation of DataSource pools JDBC ...
- linux设备驱动的分层设计思想--input子系统及RTC
转自:linux设备驱动的分层设计思想 宋宝华 http://blog.csdn.net/21cnbao/article/details/5615493 1.1 设备驱动核心层和例化 在面向对象的程序 ...
- C++面向对象的设计思想——小结
1 对象的概念 面向对象(Object Oriented Analysis Design,OOAD)的思想把整个世界看成是由具有某种特征行为功能的基本单元——对象构成的.OOAD把一个对象的特征称为属 ...
- Spring5源码分析(1)设计思想与结构
1 源码地址(带有中文注解)git@github.com:yakax/spring-framework-5.0.2.RELEASE--.git Spring 的设计初衷其实就是为了简化我们的开发 基于 ...
随机推荐
- pycharm2019.3.1版本需要的JetBrains Runtime 11不支持windows 32位系统。
提示信息显示安装pycharm2019.3.1版本需要的JetBrains Runtime 11不支持windows 32位系统. 2.更换pycharm社区版的安装版本 百度找到解决办法,参考文章& ...
- C++常见报错信息和原因的对应关系
1. 无法找到 xxx.dll 没有把动态链接库和exe放在一个文件夹下 2. 不允许使用不完整的类型 指的是忘了加头文件 3. link err .无法解析的外部符号 指的是 lib 库没有配置对 ...
- 实践课:i至诚app案例分析---江洁兰
这个作业属于哪个课程 至诚软工实践F班 这个作业要求在哪里 作业要求 这个作业的目标 分析产品软件,找出其中的问题并进行分析,提高对产品软件bug方面的认识 学号 212106715 第一部分 找Bu ...
- Python之简单文件操作
文件操作,open() 1 # open(file_path, mode='r', encoding='utf-8') 2 # file_path 目标文件路径 3 # mode 文件模式,参数r-读 ...
- subplots函数使用说明
1.函数的功能 创建一个画布对象和一组子图对象. 2.函数的声明 fig, axs = subplots(nrows=1, ncols=1, sharex=False, sharey=False, s ...
- mysql数据库备份(windows环境)
备份:cmd输入指令,按照新数据库的字符集去备份,备份等待即可: 恢复:之前新建数据库,注意字符集问题,输入指令还原即可:
- modbus通讯协议详解
1.Modbus 协议简介 Modbus协议是一种已广泛应用于当今工业控制领域的通用通讯协议.通过此协议,控制器相互之间.或控制器经由网络(如以太网)可以和其它设备之间进行通信.Modbus协议使用 ...
- 499div2-E Border :裴蜀定理
这个定理就像类似学扩展欧几里得判断是否有解的条件,当时是ax+by = c:仅当c = k*gcd(a,b)时有解,其实也就是只要是公约数的倍数就行. 而裴蜀定理是多个未知量x1*a1+x2*a2+. ...
- springboot--配置格式文件
修改端口号的三种方法 1.server.port = 80 2.新建application.yml文件. 3.新建application.yaml文件. 配置文件加载顺序: 当三个文件都存在时prop ...
- 自己动手从零写桌面操作系统GrapeOS系列教程——20.汇编语言读硬盘实战
学习操作系统原理最好的方法是自己写一个简单的操作系统. 本讲我们设计一个简单的读硬盘实验.通过一定的方法使硬盘第二个扇区的前3个字节依次为1.2.3,最后3个字节依次为3.2.1,中间的506个字节全 ...