没有杯子的世界:OOP设计思想的应用实践

打印 上一主题 下一主题

主题 890|帖子 890|积分 2670

最近看到一个有趣的问题:Person类具有Hand,Hand可以操作杯子Cup,但是在石器时代是没有杯子的,这个问题用编程怎么解决?
简单代码实现

我们先用简单代码实现原问题:
  1. @Data
  2. public class Person {
  3.     private final String name;
  4.     private Hand hand = new Hand();
  5.     private Mouth mouth = new Mouth();
  6.     private static class Hand {
  7.         // 为了简化问题,用字符串表示复杂的方法实现,这些方法极有可能具有副作用
  8.         String holdCup() {
  9.             return "hold a cup...";
  10.         }
  11.         String refillCup() {
  12.             return "refill the coffee cup...";
  13.         }
  14.     }
  15.     private static class Mouth {
  16.         String drinkCoffee() {
  17.             return "take a cup of coffee";
  18.         }
  19.     }
  20.     public String drinkCoffee() {
  21.         return String.join("\n",
  22.                 hand.refillCup(),
  23.                 hand.holdCup(),
  24.                 mouth.drink()
  25.         );
  26.     }
  27.     // 略去其他方法,run(), work(), eat()...
  28.     public static void main(String[] args) {
  29.         Person eric = new Person("Eric");
  30.         System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
  31.     }
  32. }
复制代码
良好的代码设计经常面向接口编程,我们抽取出接口如下:
  1. public interface Person {
  2.     String drinkCoffee();
  3.     // 略去其他方法,run(), work(), eat()...
  4.     interface Hand {
  5.         String holdCup();
  6.         String refillCup();
  7.     }
  8.     interface Mouth {
  9.         String drinkCoffee();
  10.     }
  11. }
  12. @Data
  13. public class DefaultPerson implements Person {
  14.     private final String name;
  15.     private Hand hand = new DefaultHand();
  16.     private Mouth mouth = new DefaultMouth();
  17.     private static class DefaultHand implements Hand {
  18.         @Override
  19.         public String holdCup() {
  20.             return "hold a cup...";
  21.         }
  22.         @Override
  23.         public String refillCup() {
  24.             return "refill the coffee cup...";
  25.         }
  26.     }
  27.     private static class DefaultMouth implements Mouth {
  28.         @Override
  29.         public String drinkCoffee() {
  30.             return "take a cup of coffee";
  31.         }
  32.     }
  33.     @Override
  34.     public String drinkCoffee() {
  35.         return String.join("\n",
  36.                 hand.refillCup(),
  37.                 hand.holdCup(),
  38.                 mouth.drinkCoffee()
  39.         );
  40.     }
  41.     public static void main(String[] args) {
  42.         Person eric = new DefaultPerson("eric");
  43.         System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
  44.     }
  45. }
复制代码
完事具备,现在我们来思考下这个问题: 问题的关键在于drinkCoffee方法,现在这个方法调用的结果是不对的,因为方法的调用依据了 DefaultPerson 之外的变量,即是否处于石器时代。 我们先看一个不好的实现:
  1. @Value
  2. public class BadPersonImpl implements Person {
  3.     String name;
  4.     boolean isInStoneEra;
  5.     @Override
  6.     public String drinkCoffee() {
  7.         if (isInStoneEra) {
  8.             return String.format("%s cannot drink, because there is no cup in the era.", getName());
  9.         }
  10.         return "refill the coffee cup..." + "hold a cup..." + "take a cup of coffee.";
  11.     }
  12.     public static void main(String[] args) {
  13.         Person eric = new BadPersonImpl("Eric", true);
  14.         System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
  15.     }
  16. }
复制代码
这段代码的问题是所有的内容都写死了,所有的代码都在一块,无法复用和拓展。
当然,如果说本来 Person 的实现就简单,新需求并不多,用这种方法也不是不可以。
问题分析&解决方法

不过,大部分情况下如果我们最开始这么写,把自己的路堵死了,当有新需求时,之后的修改极有可能发展成 if-else 套娃地狱,一个方法越写越多,越写越乱, 逻辑复杂到自己把自己都绕死了,最后实在受不了了,重写整个方法或类。
为什么我的代码中新加了 Mouth 这个类?
因为如果Person中有Hand这个类,通常说明 Hand类 有自己独立的实现,行为比较复杂,Person 实现的行为比较复杂, 加入了 Mouth 是为了说明 Person 类的复杂性,Person 是一个抽象工厂。
正确的做法应该考虑设计中的变量和不变量:

  • 人所处的时代是变化的,时代影响人的行为
  • 人的行为可以独立变化,即人具有hand、mouth等,其使用各个组件进行某些行为。
  • 人的组件hand、mouth可以独立变化
不变:

  • 时代一旦确定就不会更改(无需使用状态模式)
  • Person的组件一旦确定就不会更改
  • Person 和 Era 独立扩展
由此我们得出结论,Person 和 Era 要实现解耦。
  1. interface EraEnvironment {
  2.     default boolean hasCup() {
  3.         return true;
  4.     }
  5. }
  6. class ModernEra implements EraEnvironment {
  7. }
  8. class StoneAge implements EraEnvironment {
  9.     @Override
  10.     public boolean hasCup() {
  11.         return false;
  12.     }
  13. }
  14. // 基于组合的实现
  15. @Value
  16. class PersonInEra implements Person {
  17.     Person person;
  18.     EraEnvironment era;
  19.     @Override
  20.     public String drinkCoffee() {
  21.         if (era.hasCup()) {
  22.             return person.drinkCoffee();
  23.         }
  24.         return String.format("%s cannot drink, because there is no cup in the era.", person.getName());
  25.     }
  26.     @Override
  27.     public String getName() {
  28.         return person.getName();
  29.     }
  30.     public static void main(String[] args) {
  31.         PersonInEra eric = new PersonInEra(new DefaultPerson("Eric"), new StoneAge());
  32.         System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
  33.     }
  34. }
复制代码
进一步优化成协调者模式,可以保证各个 Colleague 类(Person、EraEnvironment)独立扩展。
如果以后还有影响 Person 行为的变量,比如天气、心情等,可以引入新的协调者。
可以看出,随着需求的增多,协调者可能越来越多,此时我们就需要重新进行分析,哪些条件可以看做Person的固有属性,对Person进行重构。
  1. // 优化抽取出抽象类
  2. class PersonInEra extends AbstractPersonInEra {
  3.     public PersonInEra(Person person, EraEnvironment era) {
  4.         super(person, era);
  5.     }
  6.     @Override
  7.     public String drinkCoffee() {
  8.         if (getEra().hasCup()) {
  9.             return getPerson().drinkCoffee();
  10.         }
  11.         return String.format("%s cannot drink, because there is no cup in the era.", getName());
  12.     }
  13.     public static void main(String[] args) {
  14.         PersonInEra eric = new PersonInEra(new DefaultPerson("Eric"), new StoneAge());
  15.         System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
  16.     }
  17. }
  18. public abstract class AbstractPersonInEra implements Person {
  19.     private final Person person;
  20.     private final EraEnvironment era;
  21.     public AbstractPersonInEra(Person person, EraEnvironment era) {
  22.         this.person = person;
  23.         this.era = era;
  24.     }
  25.     @Override
  26.     public String getName() {
  27.         return person.getName();
  28.     }
  29.     protected Person getPerson() {
  30.         return person;
  31.     }
  32.     protected EraEnvironment getEra() {
  33.         return era;
  34.     }
  35.     @Override
  36.     public abstract String drinkCoffee();
  37. }
复制代码
面向对象原则分析

当然,根据对需求的不同理解和对未来需求的预期,我们可能选择不同的实现,这个问题还有可能用状态模式、策略模式等实现,不同的方法有优点也有缺点; 如果在面试中遇到这样的问题,一定要跟面试官明确背景和需求。
我们使用面向对象的基本原则分析下改动前后的代码:
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, 对于依赖对象的实现一无所知。
策略模式

最后,你可以自己写个策略模式,和我写的策略模式比较一下,从面向对象设计的角度分析其优劣。
使用策略模式编写的代码如下:
  1. // 策略模式,不改变原 DefaultPerson 的实现
  2. @FunctionalInterface
  3. public interface DrinkStrategy {
  4.     String drink();
  5. }
  6. public final class Persons {
  7.     private Persons(){}
  8.     @NotNull
  9.     private static DrinkStrategy stoneEraSupport(Person person, EraEnvironment era) {
  10.         return () -> {
  11.             if (era.hasCup()) {
  12.                 return person.drinkCoffee();
  13.             }
  14.             return String.format("%s cannot drink, because there is no cup in the era.", person.getName());
  15.         };
  16.     }
  17.     // 工厂方法创建复杂对象
  18.     @NotNull
  19.     public static Person stoneAgeSupportWithNameAndEra(String name, EraEnvironment era) {
  20.         DefaultPerson oriPerson = new DefaultPerson(name);
  21.         return new StrategicPerson(oriPerson, stoneEraSupport(oriPerson, era));
  22.     }
  23. }
  24. @Value
  25. public class StrategicPerson implements Person {
  26.     // 使用组合
  27.     Person person;
  28.     // 支持多种策略,拓展性好
  29.     DrinkStrategy drinkStrategy;
  30.     @Override
  31.     public String drinkCoffee() {
  32.         return drinkStrategy.drink();
  33.     }
  34.     // 除需要更改的方法外,其他实现委托给原 Person. 比较烦的是:需要委托的方法多的话,都要单独编写方法
  35.     @Override
  36.     public String getName() {
  37.         return person.getName();
  38.     }
  39.     public static void main(String[] args) {
  40.         Person eric = Persons.stoneAgeSupportWithNameAndEra("eric", new StoneAge());
  41.         System.out.println("eric.drinkCoffee() = " + eric.drinkCoffee());
  42.     }
  43. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

何小豆儿在此

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表