java薪水支付案例

作者: tery007 | 来源:发表于2017-07-23 23:22 被阅读2666次

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

    案例来源:《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);
                }
            }
        }
    }
    

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

    相关文章

      网友评论

        本文标题:java薪水支付案例

        本文链接:https://www.haomeiwen.com/subject/togmkxtx.html