java薪水支付案例

我们在项目开发中,设计模式和理念决定了你做事的效率,如果你想让你的大脑存储一些重要的设计模式,好在关键的时候拿来就用,那就仔细看看这个薪水支付案例吧。

案例来源:《Agile Software Dvelopment Principles》(敏捷软件开发)一书
思路启发者:码农翻身创始人刘欣老师

涉及到的基础知识:
1. 类之间的关系:实现、依赖、关联、聚合(has-a)、组合(contains-a);
2. 设计模式:组合模式、策略模式等;
3. 设计原则:OCP(单一职责)、SRP(开闭原则)等
需求提炼:
1.员工类型
  • 小时工:每天提交工作时间卡,记录了日、工作小时数,如果每天工作超过12小时,按2倍进行支付, 每周五支付;

  • 固定薪资:每个月的最后一个工作日对他们进行支付,在员工中有个月薪字段;

  • 销售员:带薪的员工,按照提成比例给佣金。提交销售凭条,记录日期和金额。在员工中有一个提成比例字段。 每隔一周的周五支付;

2.支付方式

支票邮寄、保存在财务、银行转账;

3.扣除项:

在雇员记录中有一个每周应付款项字段,这些应付款需要从他们的薪水中扣除。

4.运行周期:

程序每个工作日运行一到多次, 对相应的员工进行支付,系统在支付前需要计算出每个员工的支付日期,这样就可以计算出从上次支付日期到支付日期间应付的薪资。

开始设计:

员工类如何设计:

思路一:将员工分为三种类型,用UML类图表示就是:


三种员工类型

思路二:用一个类来表示,即在一个主类Employee中用type字段来标识员工类型,这种方式是我们平常最常见的方式,但在这个案例中满足不了需求,大家接着往下看。


用type字段来标识员工类型,我们需要做很多的if判断,这是孕育bug的地方

用一个父类Employee,让三个子类类继承它(实现共同属性通用的目的):


用一个父类来抽象成员工接口

支付方式,我们很容易做出三种类来代表:

三种支付类型

但是这三种支付类型如何与Employee关联起来呢,我们应该进一步抽象,让它们的爸爸PayMethod去做对接:

让爸爸PayMethod来对接

此时再看这个类图,他们的关系应该是这样的:


关联后的类图
关系说明:
 1.SalesSlip、TimeCard与SalesEmployee、HourEmployee为组合关系(同生共死);
 2.PayMethod类与Employee类的关系为聚合(局部可单独存在,即PayMethod离开Employee也可单独存在)

问题来了!

你是不是觉得事情没有那么简单,那恭喜你,说明你是个爱思考的小码。假如公司的小时工转岗为销售了,按照销售类来支付,怎么办?

其中一种解决思路就是,在Employee中增加type字段来表示员工类型,如果在数据库中有员工类型字段,那么将员工类型改掉就可以满足变化的需求了。但是如果这个员工是做了半个月固薪员工,半个月的销售员,那么他的薪资怎么算?

所以我们会发现在做抽象时,抽象的如果是不变的部分,那就搞错方向了,应该提取变化的部分做抽象。

好的思路是将员工的支付抽象为支付策略(策略模式--将不同的算法封装起来):


抽象为支付类型,这才是真正变化的部分

谁负责计算薪水?

好,我们继续,下一个要解决的问题是在哪里计算薪水?看起来让PayClassify负责最合适不过了。简单地想一下它是如何计算的:给定日期,if判断如果这一天是支付日,则进行薪水计算:

让PayClassify负责薪酬计算

PayClassify的孩子们开始干活了

就在孩子们辛苦工作的时候,OCP老人家过来狠狠地敲了一下PayClassify老爹的头:你怎么这么糊涂,你干嘛让你的孩子又判断是否是付薪日,又做薪水计算!我这一辈子不断地告诉世人,一辈子只要做好一件事就可以了,这是我生命的意义,也是你们少走弯路的捷径啊!

抽象“变化的支付日”

支付日有三种:每周五支付,隔一周周五支付,月底支付。这三种支付日期抽象为三个类,并他们的父类PayDateUtil与Employee关联。

PayDateUtil负责日期判断和薪水支付计算
计算薪水的细节问题:
 1.小时支付类型: sum (每个时间卡 x 每小时报酬) ,计算过去一周的时间卡
 2.提成类型: 底薪 + sum ( 每个销售凭条的销售额 x 提成比例 ) ,过去两周的销售凭条
 3.固薪类型: 固定的薪水

还有一个头疼的问题没有解决

谁来记录已经发薪的员工,保证系统重新运行时不会重发薪水?
我们需要一个类来单独负责运行检查:PayDetail,这个类主要的职责是跟着Employee对象,在计算薪水、扣除项时全部在场:

让PayDeail负责支付细节,携带日期信息,保证没搞错

到这里,我们还剩下最后一个需求没有解决:扣除项。我们用Reduce类代表服务费用:

扣除服务费的抽象

到这里,我们基本上已经解决了90%的业务需求,下面我们就来看看代码层面是怎么做的吧。

1.Employee类:

public class Employee {
    private String id;
    private String name;
    private Integer age;
    private Integer sex;
    private PayClassify classify;//支付策略类型
    private PayDateUtil payDateUtil;//支付时间抽象类
    private PaymentMethod paymentMethod;//支付方式
    private Reduce reduce;//扣除项

    public Employee(String id, String name){
        this.id = id;
        this.name = name;
    }
    public boolean isPayDay(Date d) {
        return this.payDateUtil.isPayDate(d);
    }
    public Date getStartDate(Date d) {
        return this.payDateUtil.getPayPeriodStartDate(d);
    }
    public void payDay(PayDetail detail){
         double grossPay = classify.calculatePay(detail);
         double deductions = reduce.calculateDeductions(detail);
         double netPay = grossPay - deductions;
         detail.setGrossPay(grossPay);
         detail.setDeductions(deductions);
         detail.setNetPay(netPay);
         paymentMethod.pay(detail);
    }
}

2.支付:

周五支付:
public class WeeklyUtil implements PayDateUtil {
    @Override
    public boolean isPayDate(Date date) {       
        return DateUtil.isFriday(date);
    }
    @Override
    public Date getPayPeriodStartDate(Date payPeriodEndDate) {      
        return DateUtil.add(payPeriodEndDate, -6);
    }
}
隔周支付:
public class OverWeekUtil implements PayDateUtil {
    Date firstPayableFriday = DateUtil.parseDate("2017-6-2");
    @Override
    public boolean isPayDate(Date date) {
        long interval = DateUtil.getDaysBetween(firstPayableFriday, date);
        return interval % 14 == 0;
    }
    @Override
    public Date getPayPeriodStartDate(Date payPeriodEndDate) {
        return DateUtil.add(payPeriodEndDate, -13);
    }
}
月底支付:
public class MonthEndUtil implements PayDateUtil {
    @Override
    public boolean isPayDate(Date date) {       
        return DateUtil.isLastDayOfMonth(date);
    }
    @Override
    public Date getPayPeriodStartDate(Date payPeriodEndDate) {      
        return DateUtil.getFirstDay(payPeriodEndDate);
    }
}

3.三种支付策略:

销售类支付策略:
public class SalesPayClassify implements PayClassify {
    double salary;
    double rate;
    public SalesPayClassify(double salary , double rate){
        this.salary = salary;
        this.rate = rate;
    }
    Map<Date, SalesReceipt> receipts;
    @Override
    public double calculatePay(PayDetail detail) {
        double commission = 0.0;
        for(SalesReceipt sr : receipts.values()){
            if(DateUtil.between(sr.getSaleDate(), detail.getPayPeriodStartDate(), 
                    detail.getPayPeriodEndDate())){
                commission += sr.getAmount() * rate;
            }
        }
        return salary + commission;
    }
}
按小时支付策略:
public class HourlPayClassify implements PayClassify {
    private double rate;
    private Map<Date, TimeCard> timeCards;
        public HourlPayClassify(double hourlyRate) {
        this.rate = hourlyRate;
    }
    public void addTimeCard(TimeCard tc){
        timeCards.put(tc.getDate(), tc);
    }
    @Override
    public double calculatePay(PayDetail detail) {
        double totalPay = 0;
        for(TimeCard tc : timeCards.values()){
            if(DateUtil.between(tc.getDate(), detail.getPayPeriodStartDate(), 
                    detail.getPayPeriodEndDate())){
                totalPay += calculatePayForTimeCard(tc);
            }
        }       
        return totalPay;
    }
    private double calculatePayForTimeCard(TimeCard  tc) {
        int hours = tc.getHours();
           if(hours > 12){
            return 12*rate + (hours-12) * rate * 2;
        } else{
            return 12*rate;
        }
    }
}
固定薪资:
public class BasePayClassify implements PayClassify {
    private double salary;
    public BasePayClassify(double salary){
        this.salary = salary;
    }
    @Override
    public double calculatePay(PayDetail pc) {      
        return salary;
    }
}

4.支付细节:

public class PayDetail {
    private Date start;
    private Date end;
    private double grossPay;//应付
    private double netPay;//实发
    private double deductions;//扣除
    private Map<String, String> itsFields;
    public PayDetail(Date start, Date end){
        this.start = start;
        this.end = end;
    }
    public void setGrossPay(double grossPay) {
        this.grossPay = grossPay;
    }
    public void setDeductions(double deductions) {
        this.deductions  = deductions;      
    }
    public void setNetPay(double netPay){
        this.netPay = netPay;
    }
    public Date getPayPeriodEndDate() {
        return this.end;
    }
    public Date getPayPeriodStartDate() {
        return this.start;
    }
}

5.DateUtil类:

public class DateUtil {
    public static long getDaysBetween(Date d1, Date d2){        
        return (d2.getTime() - d1.getTime())/(24*60*60*1000);       
    }   
    public static Date parseDate(String txtDate){
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");        
        try {
            return  sdf.parse(txtDate);
        } catch (ParseException e) {
            e.printStackTrace();
            return null;
        }       
    }
    public static boolean isFriday(Date d){
         Calendar   calendar   =   Calendar.getInstance();    
         return calendar.get(Calendar.DAY_OF_WEEK) == 5;    
    }   
    public static Date add(Date d, int days){
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(d);
        calendar.add(Calendar.DATE, days);
        return calendar.getTime();
    }   
    public static boolean isLastDayOfMonth(Date d){
        Calendar calendar=Calendar.getInstance();
        calendar.setTime(d);
        return calendar.get(Calendar.DATE)==calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
    }
    public static Date getFirstDay(Date d){
        Calendar calendar=Calendar.getInstance();
        calendar.setTime(d);
        int day = calendar.get(Calendar.DATE);
        calendar.add(Calendar.DATE, -(day-1));
        return calendar.getTime();
    }
    

6.遍历Employee集合,实现薪资发放

public class PaydayTest {
    private Date date;
    private PayService payService;
        public void execute(){
        List<Employee> employees = payService.getAllEmployees();
        for(Employee e : employees){
            if(e.isPayDay(date)){
                PayDetail detail = new PayDetail(e.getStartDate(date),date);
                e.payDay(detail);
                payService.savePaycheck(detail);
            }
        }
    }
}

代码只放了一些关键的类代码,如果小伙伴们有兴趣,可以自己去实现一下。

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

推荐阅读更多精彩内容