最近看到一个有趣的问题:Person类具有Hand,Hand可以操作杯子Cup,但是在石器时代是没有杯子的,这个问题用编程怎么解决?
简单代码实现
我们先用简单代码实现原问题:
@Datapublic 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(); }}@Datapublic 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之外的变量,即是否处于石器时代。我们先看一个不好的实现:
@Valuepublic 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; }}// 基于组合的实现@Valueclass 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 的实现@FunctionalInterfacepublic 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)); }}@Valuepublic 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()); }}