• 没有杯子的世界: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 是一个抽象工厂。

    正确的做法应该考虑设计中的变量和不变量:

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

    不变:

    1. 时代一旦确定就不会更改(无需使用状态模式)
    2. Person的组件一旦确定就不会更改
    3. 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());
        }
    }
    
  • 相关阅读:
    (免费分享)基于springboot医院管理系统
    关于主机2访问虚拟机1的web应用
    PTA 7-88 组合成最小的三位数
    uni.getLocation() 微信小程序 线上获取失败
    《最新出炉》系列入门篇-Python+Playwright自动化测试-8-上下文(Context)
    java-net-php-python-15体育用品网上销售系统计算机毕业设计程序
    [附源码]计算机毕业设计JAVAjsp社区养老服务管理系统
    Ajax加强
    jeecgBoot 路由配置和自定义路由配置
    win10系统x64安装java环境以及搭建自动化测试环境
  • 原文地址:https://www.cnblogs.com/dahua-dijkstra/p/17338189.html