java薪水支付案例

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

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

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

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

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

2.支付方式

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

3.扣除项:

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

4.运行周期:

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

开始设计:

员工类如何设计:

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


java薪水支付案例_第1张图片
三种员工类型

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


java薪水支付案例_第2张图片
用type字段来标识员工类型,我们需要做很多的if判断,这是孕育bug的地方

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


java薪水支付案例_第3张图片
用一个父类来抽象成员工接口

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

java薪水支付案例_第4张图片
三种支付类型

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

java薪水支付案例_第5张图片
让爸爸PayMethod来对接

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


java薪水支付案例_第6张图片
关联后的类图
关系说明:
 1.SalesSlip、TimeCard与SalesEmployee、HourEmployee为组合关系(同生共死);
 2.PayMethod类与Employee类的关系为聚合(局部可单独存在,即PayMethod离开Employee也可单独存在)

问题来了!

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

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

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

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


java薪水支付案例_第7张图片
抽象为支付类型,这才是真正变化的部分

谁负责计算薪水?

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

java薪水支付案例_第8张图片
让PayClassify负责薪酬计算

java薪水支付案例_第9张图片
PayClassify的孩子们开始干活了

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

抽象“变化的支付日”

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

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

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

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

java薪水支付案例_第11张图片
让PayDeail负责支付细节,携带日期信息,保证没搞错

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

java薪水支付案例_第12张图片
扣除服务费的抽象

到这里,我们基本上已经解决了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 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 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 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 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);
            }
        }
    }
}

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

你可能感兴趣的:(java薪水支付案例)