设计模式与生活⑱:工资还没焐热,账单已经凑成一摞

每个月最开心的,是发薪日。

上午正写着代码,手机在桌角一震:工资到账。嘴角刚翘起来,手机就开始陆续地震。

房贷扣款 5800,老家那套房。信用卡自动还款 3000,上个月的零零碎碎。房租 4500,这间租屋,转给房东。水电代扣 230。车贷 2500。话费 100。

楼下贴着取暖费通知:中旬开始供暖,两千四,管一季。地铁每天来回十块,一个月二百二。

晚上到家瘫在沙发上,把今天的短信翻出来,一条条看。工资在卡里待了不到一个白天,大头就各就各位了。

一数,八笔。这事儿跟代码里的一种结构,是一回事。

短信一条条来,钱一笔笔走

今天聊的组合模式(Composite):一张账单能算钱,一摞账单也能算钱,问法不用换。系列第十八篇。

01 挨个加起来

最直接的写法,一项一个变量,最后一把加:

java 复制代码
int mortgage = 5800;    // 房贷
int rent = 4500;        // 房租
int utility = 230;      // 水电
int heating = 2400;     // 取暖费
int carLoan = 2500;     // 车贷
int metro = 220;        // 地铁
int creditCard = 3000;  // 信用卡
int phone = 100;        // 话费

int total = mortgage + rent + utility + heating
        + carLoan + metro + creditCard + phone;

八项八个变量,一把加完

能跑,总数也对。但问一句"住这一块,一个月多少"------房贷、房租、水电、取暖费,得从那条加法里把这四项挑出来,再写一条新的。下个月添了物业费,总数公式要改,住的小计公式也要改。漏改哪条,没人提醒你。

02 先让每张账单长一个样

把"一张账单"抽出来:有名字,有金额。

java 复制代码
interface BillItem {
    String name();
    int amount();
}

class SingleBill implements BillItem {
    private final String name;
    private final int amount;

    SingleBill(String name, int amount) {
        this.name = name;
        this.amount = amount;
    }

    public String name() {
        return name;
    }

    public int amount() {
        return amount;
    }
}

单张账单:一个名字,一个数

装进一个 List,遍历求和,添项不用再动求和的代码:

java 复制代码
List<BillItem> bills = List.of(
        new SingleBill("房贷", 5800),
        new SingleBill("房租", 4500),
        // ......还有六张
        new SingleBill("话费", 100)
);

int total = 0;
for (BillItem bill : bills) {
    total += bill.amount();
}

清单平了,但"住一共多少"还是答不上来

漏加的问题解决了,清单却是平的。"住这一块多少",还得按名字一张张往回挑。可账单天生是分摞的:住一摞,行一摞,剩下的散着。一摞也是账单,也该能直接问"多少"。

03 让一摞账单也长这个样

组合模式就一招:让"一摞"也实现同一个接口。

java 复制代码
class BillGroup implements BillItem {
    private final String name;
    private final List<BillItem> items = new ArrayList<>();

    BillGroup(String name) {
        this.name = name;
    }

    void add(BillItem item) {
        items.add(item);
    }

    public String name() {
        return name;
    }

    public int amount() {
        int total = 0;
        for (BillItem item : items) {
            total += item.amount();
        }
        return total;
    }
}

一组账单:里面装的还是 BillItem

注意 amount() 里那句 item.amount():组不关心手里这张是单是摞------是单张,自己报数;是摞,就继续往下问,问到单张为止。树就是这么长出来的。

拼一棵这个月的:

java 复制代码
BillGroup living = new BillGroup("住");
living.add(new SingleBill("房贷", 5800));
living.add(new SingleBill("房租", 4500));
living.add(new SingleBill("水电", 230));
living.add(new SingleBill("取暖费", 2400));

BillGroup commute = new BillGroup("行");
commute.add(new SingleBill("车贷", 2500));
commute.add(new SingleBill("地铁", 220));

BillGroup november = new BillGroup("11月账单");
november.add(living);
november.add(commute);
november.add(new SingleBill("信用卡", 3000));
november.add(new SingleBill("话费", 100));

int salary = 20000;
System.out.println("住 小计:" + living.amount());
System.out.println("行 小计:" + commute.amount());
System.out.println(november.name() + " 总计:" + november.amount());
System.out.println("到账 " + salary + ",还剩 " + (salary - november.amount()));

住里装四张,行里装两张,剩下的直接挂月账单上

跑一遍:

yaml 复制代码
住 小计:12930
行 小计:2720
11月账单 总计:18750
到账 20000,还剩 1250

代码实际运行输出

单张账单和一整月账单,对上的是同一个 amount()。调用的人不用问"你是一张还是一摞"------这就是组合模式说的:把对象组成树,表示"部分-整体";让单个对象和组合对象,用起来一个样。

04 树管算账,不管日子

先把失真点挑明。

现实里扣款有先后:房贷划走,卡里少了 5800,才有后面的事;余额不足,还会扣失败。组合不管这个------它管的是结构和总数,不管顺序。真要排队分先后,那是责任链(Day⑪)的路子;要一张张过一遍,那是迭代器的活。

还有,代码里这棵树是当月现拼的。现实的账单月月变:取暖费只这几个月有,信用卡时多时少。树跟着月份变,正常;别想着把它固化成一套用一辈子的配置。

最要紧的一条:现实里忘了缴取暖费,顶多是催缴单贴在门上;代码里漏 add 一项,编译不报错,运行不报警,总数悄无声息地就错了。树不会替你对账,对账还是得靠人。

JDK 里的 Swing 是这个结构:Container 里 add 的可以是按钮,也可以是面板,面板里还能再装面板;布局一声令下,整棵树重新排。文件系统也是:文件和文件夹都能问"多大",文件夹的大小,是里面所有东西一层层报上来的。

05 什么时候别用

账单就是一张流水清单,没有分组小计的需求------一个 List 遍历就够了,别硬造树。

叶子和容器的行为差得太远,也别硬凑:文件能"打开",文件夹的"打开"是另一个意思,硬统一成一个接口,接口里就会长出对一半成员没意义的方法。

还有,你要的如果是"挨个处理每一件",而不是"把一整组当一件用",那要找的可能是迭代器,不是组合。

06 还剩一千二百五

晚上的沙发上,总数对上了:18750。剩 1250。

一张能算,一摞也能算,问法都是同一个 amount()

跟前面十七篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把请求做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做,责任链管请求由哪一环接住,原型管拿哪个现成对象复制,桥接管抽象和实现怎么独立扩展,享元管把能共享的部分只留一份,备忘录管把某个时刻的状态存下来、再原样恢复,生成器管把复杂对象的装配拆成一步一步、最后一次性成型,访问者管对一批稳定的元素做什么操作由递进来的单子说了算。本篇组合,管单个和一组长一个样,整棵树当一件东西用。

带走一句话:组合 = 部分和整体长一个样;一张账单能算钱,一摞账单也能算钱,问法不用换。

账单是一张张来的,

日子是一月月过的。

树还是那棵树,下个月换几片叶子。


参考资料

相关推荐
百思不得小李13 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
爱你真是太好了16 小时前
上下文组成以及如何压缩
设计模式
freejackman20 小时前
【工程化】从“单兵作战”到“多引擎架构”:基于门面模式的重构实战
大数据·数据库·hive·sql·测试工具·设计模式·架构
Java成神之路-1 天前
从 if-else 到优雅解耦:策略 / 适配器 / 组合模式全流程实战
设计模式
是2的10次方啊1 天前
设计模式与生活⑰:空腹体检,单子领着人走
设计模式
是2的10次方啊2 天前
设计模式与生活⑯:天冷了,出门这身得一层层配
设计模式
心若自由何处是束缚3 天前
适配器模式:从原理到实战的完整指南
设计模式·适配器模式
是2的10次方啊3 天前
设计模式与生活⑮:看到哪儿了,得有个地方替你记着
设计模式
2601_967979124 天前
设计模式 - 访问者模式
设计模式·访问者模式·应用场景·对象结构·操作分离