敏捷软件开发--薪水支付案例


薪水支付系统的初步规格说明

下面是和客户交谈时做的一些记录。
该系统由一个公司雇员数据库以及和雇员相关的数据(比如:工作时间卡)组成。该系统必须为每个雇员支付薪水。系统必须按照规定的方法准时地给雇员支付正确数目的薪水。同时,必须从雇员的薪水中减去各种扣款

  • 有些雇员是钟点工。会按照他们雇员记录中每小时报酬字段的值对他们进行支付。他们每天会提交工作时间卡,其中记录了日期以及工作小时数。如果每天工作超过8小时,那么超过的部分会按照正常报酬的1.5倍进行支付。每周五对他们进行支付

  • 有些雇员完全以月薪进行支付。每个月的最后一个工作日对他们进行支付。在他们的雇员记录中有一个月薪字段。

  • 同时,对于一些带薪雇员,会根据他们的销售情况,支付他们一定数量的酬金。他们会提交销售凭条,其中记录了销售日期和数量。在他们的雇员记录中有一个酬金字段,每隔一周的周五对他们进行支付

  • 雇员可以选择支付方式。可以选择把支付支票邮寄到他们指定的邮政地址;也可以把支票保存在出纳人员那里随时支取;或者要求将薪水直接存入他们指定的银行账户。

  • 一些雇员会加入协会。在他们的雇员记录中有一个每周应付款项目字段。这些应付款项目必须要从他们的新税种扣除。协会有时也会针对单个协会成员征收服务费用。协会每周会提交这些服务费用,服务费用必须用从相应雇员的下一个月的薪水总额中扣除。

  • 薪水支付程序每个工作日运行一次,并且当天为相应的雇员进行支付。系统会被告知雇员的支付日期,这样它会计算从雇员上次支付日期到现在规定的本次支付日期间应支付的数额。

根据需求中我们可以清楚地知道表和字段的样子。可以很容易地设计出一个可用的数据库模型。然后再写一些 DAO Service 。不过在使用这种方法产生的应用程序中,数据库成为了关注的中心。数据库是实现细节,我们先不考虑数据库的模型。先考虑系统中有哪些对象,这些类和类之间的关系。

员工类设计

根据前三点需求抽象出 Employee 为父类有三个子类,如下图的类结构:

Employee

UML类图:

  • 属性或方法前有 “-” 表示 private
  • 属性或方法前有 “+” 表示 public
  • 属性或方法前有 “#” 表示 protected
  • 空心箭头实线表示继承
  • 用实心菱形表示组合关系。意思是同生同死,只有两个类组合在一起才能使用,缺少一个另一个没有存在的意义。

支付方式

需求:雇员可以选择支付方式。可以选择把支付支票邮寄到他们指定的邮政地址;也可以把支票保存在出纳人员那里随时支取;或者要求将薪水直接存入他们指定的银行账户。

抽象 PaymentMethod 接口

  • PaymentMethod 接口有 pay(Double mony) 方法,有三个子类
  • MailMethod 邮寄支票方式
  • BankMethod 打到银行卡方式
  • HoldMethod 到财务自取方式

类图

PaymentMethod

空心箭头,虚线表示实现接口

需求变化

钟点工可以改成销售员工,可以把固定带薪员工改成销售员工

  • 看上面的类图,子类是不能相互转化的。
  • 在 Employee 中加入一个type字段,代表不同类型的员工,那时 TimeCard 类和 SalseReceipt 类怎么办?都组合到 Employee 类上?可是固定工资员工是没有这两个类的。小时工也没有销售凭条。这样不合理?

我们再仔细阅读需求,可以提取出下面这段话:

一些雇员按小时支付,一些雇员按月薪支付,一些雇员按一定数量的酬金支付。

每种类型的员工都需要被支付,只是支付的策略不同。所以重点是员工的支付策略,而不是员工类型。发现变化封装变化继续抽象

支付策略

抽象 PaymentClassification 接口

  • 把 Employee 的子类化去掉,在 Employee 类中注入一个 PaymentClassification 接口,有三个子类
  • HourtyClassification 按小时支付类型
  • SalaredClassification 固定薪资支付类型
  • CommissioneClassification 按销售凭条支付类型

类图

PaymentClassification

薪水计算

现在支付策略,支付方式设计完了,来看一下员工的薪水怎么计算?应该放在那里计算?
放在PaymentClassification的派生类里面是合理的,他们保存了计算薪水所需要的数据,并且每个派生类都知道自己薪水的计算具体方式。

但是我们再看一下需求:

  • 钟点工,根据工时卡,其中记录了日期以及工作小时数。如果每天工作超过8小时,那么超过的部分会按照正常报酬的1.5倍进行支付。每周五对他们进行支付
  • 月薪员工,根据月薪进行支付。每个月的最后一个工作日对他们进行支付
  • 销售员工,根据凭条,其中记录了销售日期和数量。每隔一周的周五对他们进行支付

那么根据上面的需求,写出的代码大概是这样子的

  • 固定月薪职工薪水计算
public class CommissionedClassification implements PaymentClassification {

    @Override
    public double calculatePay(Date date) {
        if(date 是否最后一个工作日){
            return 计算薪水
        }else{
            return 不计算薪水;
        }
    }
}
  • 小时工薪水计算
public class HourlyClassification implements PaymentClassification {

    @Override
    public double calculatePay(Date date) {
        if(date 是否是周五){
            return 计算薪水
        }else{
            return 不计算薪水;
        }
    }
}
  • 销售员工薪水计算
public class SalariedClassification implements PaymentClassification {

    @Override
    public double calculatePay(Date date) {
        if(date 是否为隔周周五){
            return 计算薪水
        }else{
            return 不计算薪水;
        }
    }
}

看这样写有没有什么问题?

难道不会在某天改变策略,支付日期发生变化。假如改为“每周四支付”,“隔三周支付”,"每个月,月末的前一天支付"。

  • 把支付薪水时间问题委托给PaymentClassification类,那么该类对于支付薪水时间方面的变化就不是封闭的。当支付时间发生变化时,必须要修改 if 里面的逻辑。这就违反了 OCP(开闭原则,对扩展开发对修改关闭)
  • 同时PaymentClassification负责是否为支付日期的判断和支付薪水的计算。这也违反了 SRP(单一职责)

所以怎么办? 关键问题是继续对支付时间进行抽象

支付时间抽象

抽象 PaymentSchedule 接口

  • 接口 PaymentSchedule 定义两个方法,否支付日期的判断,和上一次支付日期,有三个子类
  • WeeklySchedule 每周五支付
  • BiWeeklySchedule 隔周五支付
  • MothlySchedule 按月支付

类图

PaymentSchedule

抽象出来 PaymentSchedule 之后看一下现在薪水怎么计算:


image.png

分析上面类图

我们来看一下上面的类图:

  • 3个接口都是典型的策略模式
  • 3个接口职责单一,变化互不影响(系统正交化)
  • 满足开闭原则,方便扩展
  • 3个接口9个实现可以随意组合
  • 优先使用聚合而不是继承
  • 发现变化封装变化,主要是抽象

扣除项

一些雇员会计入协会。在他们的雇员记录中有一个每周应付款项目字段。这些应付款项目必须要从他们的新税种扣除。协会有时也会针对单个协会成员征收服务费用。协会每周会提交这些服务费用,服务费用必须用从相应雇员的下一个月的薪水总额中扣除。

抽象 Affiliation 类

  • Affiliation 定义服务费计算,一个员工可以加入多个协会。

类图

Affiliation

计算薪水细节问题

  • 钟点工:sum(每个时间卡 * 每小时报酬)
    • 对于钟点工,只计算过去一周的时间卡
  • 销售:基本薪资 + sum(每个销售凭条的销售额 * rate)
    • 对于销售,只计算过去两周的销售凭条
  • 普通员工:固定薪水
  • 薪水支付程序每天运行一次,甚至一天运行多次,一定不能产生同一个人多次发薪水的错误。

抽象支付细节

抽象 Paycheck 类

  • 需要把成功运行支付的记录保存下来,保证不会对同一个人重复支付
  • 需要有一个时间段(Period)的概念,能标识过去一周、过去两周
  • 收集参数

类图

Paycheck

主要代码

  • 计算薪水 PayrollApplication
List<Employee> emps = payrollDatabase.findAllEmp();
for (Employee emp : emps) {
    if (emp.isPayDay(date)) {
        Paycheck pc = new Paycheck(emp.getPayPeriodStartDate(date), date);
        emp.payDay(pc);
        payrollDatabase.savePaycheck(pc);
    }
}
  • Employee 类
public class Employee {
    private String id;
    private String name;
    private String address;
    private List<Affiliation> affiliations = new ArrayList<>();
    private PaymentClassification classification;
    private PaymentSchedule schedule;
    private PaymentMethod paymentMethod;

    public boolean isPayDay(LocalDate d) {
        return this.schedule.isPayDate(d);
    }

    public LocalDate getPayPeriodStartDate(LocalDate d) {
        return this.schedule.getPayPeriodStartDate(d);
    }

    public void payDay(Paycheck pc) {
        double grossPay = classification.calculatePay(pc);
        double deductions = calculateDeductions(pc);
        double netPay = grossPay - deductions;
        pc.setGrossPay(grossPay);
        pc.setDeductions(deductions);
        pc.setNetPay(netPay);
        pc.setEmpId(id);
        paymentMethod.pay(pc);
    }

    protected double calculateDeductions(Paycheck pc) {
        double deductions = 0.0;
        for (Affiliation affiliation : affiliations) {
            deductions += affiliation.calculateDeductions(pc);
        }
        return deductions;
    }
}

支付计划 PaymentSchedule

  • 实现类 MothlySchedule,每个月最后一个工作日支付
public class MothlySchedule implements PaymentSchedule{

    @Override
    public boolean isPayDate(LocalDate date) {      
        return DateUtil.isLastDayOfMonth(date);
    }

    @Override
    public LocalDate getPayPeriodStartDate(LocalDate payPeriodEndDate) {        
        return DateUtil.getFirstDay(payPeriodEndDate);
    }
}
  • 实现类 WeeklySchedule, 每周五支付
public class WeeklySchedule implements PaymentSchedule {

    @Override
    public boolean isPayDate(LocalDate date) {
        return DateUtil.isFriday(date);
    }

    @Override
    public LocalDate getPayPeriodStartDate(LocalDate payPeriodEndDate) {
        return DateUtil.add(payPeriodEndDate, -6);
    }
}
  • 实现类 BiweeklySchedule, 隔周周五支付
public class BiweeklySchedule implements PaymentSchedule {
    LocalDate firstPayableFriday = LocalDate.of(2017, 7, 6);

    @Override
    public boolean isPayDate(LocalDate date) {
        long interval = DateUtil.getDaysBetween(firstPayableFriday, date);
        return interval % 14 == 0;
    }

    @Override
    public LocalDate getPayPeriodStartDate(LocalDate payPeriodEndDate) {
        return DateUtil.add(payPeriodEndDate, -13);
    }
}

支付策略 PaymentClassification

  • 实现类 HourlyClassification, 小时工薪水计算
public class HourlyClassification implements PaymentClassification {
    private double hourlyRate;
    private List<TimeCard> timeCards = new ArrayList<>();

    public void addTimeCards(TimeCard timeCard) {
        this.timeCards.add(timeCard);
    }

    @Override
    public double calculatePay(Paycheck paycheck) {
        double totalPay = 0;
        for (TimeCard tc : timeCards) {
            if (DateUtil.between(tc.getDate(), 
                paycheck.getPayPeriodStart(), paycheck.getPayPeriodEnd())) {
                totalPay += calculatePayForTimeCard(tc);
            }
        }
        return totalPay;
    }

    private double calculatePayForTimeCard(TimeCard tc) {
        double hours = tc.getHours();
        if (hours > 8) {
            return 8 * hourlyRate + (hours - 8) * hourlyRate * 1.5;
        } else {
            return 8 * hourlyRate;
        }
    }
}
  • 实现类 SalariedClassification, 固定月薪员工薪水计算
public class SalariedClassification implements PaymentClassification {
    private double salary;

    public SalariedClassification(double salary) {
        this.salary = salary;
    }

    @Override
    public double calculatePay(Paycheck paycheck) {
        return salary;
    }
}
  • 实现类 CommissionedClassification, 固定月薪员工薪水计算
public class CommissionedClassification implements PaymentClassification {
    private double rate;
    private double salary;
    private List<SalesReceipt> salesReceipt = new ArrayList<>();

    @Override
    public double calculatePay(Paycheck paycheck) {
        double commission = 0.0;
        for (SalesReceipt sr : salesReceipt) {
            if (DateUtil.between(sr.getDate(), 
                paycheck.getPayPeriodStart(), paycheck.getPayPeriodEnd())) {
                commission += sr.getAmount() * rate;
            }
        }
        return salary + commission;
    }

    public void addSalesReceipt(SalesReceipt salesReceipt) {
        this.salesReceipt.add(salesReceipt);
    }
}

支付方式 PaymentMethod

  • 实现类 BankMethod, 直接打到银行卡上
public class BankMethod implements PaymentMethod {
    private String bank;
    private double account;

    public BankMethod(String bank, double account) {
        super();
        this.bank = bank;
        this.account = account;
    }

    @Override
    public void pay(Paycheck paycheck) {
        System.out.println("向银行卡 " + bank + " 支付" + account + "元");
    }
}
  • 实现类 HoldMethod, 财务自取
public class HoldMethod implements PaymentMethod {

    @Override
    public void pay(Paycheck paycheck) {
        System.out.println("到财务自取");
    }
}
  • 实现类 MailMethod, 支票邮寄到指定地址
public class MailMethod implements PaymentMethod {
    private String address;
    @Override
    public void pay(Paycheck paycheck) {
        System.out.println("向" + address + " 发送支票");
    }
}

会费计算 Affiliation

  • 实现类 UnionAffiliation
private String memberId;
    private double weeklyBue;
    private List<ServiceChange> serviceChanges = new ArrayList<>();

    // int fridays = 统计在 paycheck 开始时间和结束时间有多少周五
    // totalDue = fridays * weeklyBue;
    // totalChange = 计算 paycheck 开始时间和结束时间之间的 ServiceChange
    // deduction = totalDue + totalChange
    @Override
    public double calculateDeductions(Paycheck paycheck) {
        int fridays = DateUtil.betweenOnFriday(paycheck.getPayPeriodStart(), paycheck.getPayPeriodEnd());
        double totalDue = fridays * weeklyBue;
        double totalChange = 0D;
        for (ServiceChange sc : serviceChanges) {
            if (DateUtil.between(sc.getDate(), paycheck.getPayPeriodStart(), paycheck.getPayPeriodEnd())) {
                totalChange += sc.getAmout();
            }
        }
        return totalDue + totalChange;
    }
}

系统动态模型

  • PayrollApplication 动态模型


  • 动态模型“今天不是发薪日”


    今天不是发薪日
  • 动态模型“今天是发薪日”


    今天是发薪日

Github 代码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 我们在项目开发中,设计模式和理念决定了你做事的效率,如果你想让你的大脑存储一些重要的设计模式,好在关键的时候拿来就...
    tery007阅读 4,747评论 7 47
  • 一.需求 开发一个薪水支付系统,为公司每个雇员支付薪水,系统必须按照规定的方法准时地给雇员支付正确数目的薪水,同时...
    Mrs_Gao阅读 1,219评论 0 1
  • 使用QuartusII 9.1版本,简单包括以下流程:新建工程编译工程分配引脚下载配置 为保证设计的正确性,在编译...
    海青简书号阅读 4,917评论 0 52
  • 关注底层知识,快速入门 四月份开始健身,一开始的计划是跟着视频跳Focus T25。任何健身方面的知识都没有接触过...
    sharemy的简书阅读 285评论 2 0
  • 头像一直是展示我们个性的有效途径,十年前深谙此道理的我就凭借绝妙的头像赢得班花! 前提:一部越狱的苹果手机 ①....
    Limuby阅读 2,034评论 0 0