薪水支付案例

一.需求

开发一个薪水支付系统,为公司每个雇员支付薪水,系统必须按照规定的方法准时地给雇员支付正确数目的薪水,同时,必须从雇员的薪水中减去各种扣款。

雇员的种类:

  • 钟点工,按小时付费,当工作时间超过8小时时,按超过的部分按1.5倍收费。雇员信息中会给出每小时的酬金,每周五支付。
  • 月薪雇员,在每个月的最后一天发放工资,雇员信息中会给出月薪的酬金。
  • 带薪雇员,固定的工资+销售提成,提成根据销售情况来算,雇员会提交销售凭条,里面包含了销售日期和数量,雇员信息中含有固定工资字段和提成酬金字段,每隔一周的周五支付。

支付方式:

  • 将支付支票邮寄到指定的邮政地址
  • 将支付支票存到税务人员那里随时支取
  • 将薪水直接打到银行账户中

扣款项:

  • 雇员加入协会,协会每周会收取费用,另外协会会不定时的收取其他服务费用,费用直接从薪水中扣除。

运行方式:

可以输入指定日期或直接取当天日期,并发放该日期的薪水,可以运行一次或多次。

二.用例

image.png
image.png
image.png

三.初步分析

首先,雇员的种类有三种,第一时间可能会想到通过Employee来派生三个子类,但是,用例中有更改雇员类型的操作,钟点工和带薪雇员是可以转换的,这意味着,派生子类的设计不能在这里使用。我们可以使用策略模式,将雇员类型抽象为策略类,每个策略类中包含表示该类型雇员的特有字段,雇员的支付方式同理。最后是处理协会这个扣费项,采用组合的方式,使每个Employee对象包含一个协会对象Affiliation,当然这是个基类,因为雇员可以选择加入协会,也可以不选择,这需要两个子类来表示,一个是包含服务酬金的UnionAffiliation,表示加入协会,另一个是NoAffiliation,表示没有加入任何协会。

四.初步设计

image.png

五.设计背后的思考

在上述UML图中,我们并没有看到雇员支付薪水的方法,它的设计将被推迟到PaymentClassifiction类中,该类保存了计算薪水所需要的数据。另外,每种雇员他们支付薪水的时间点是完全不同的,但是UML图中并没有考虑到这点,我们很容易的想到使用策略模式,即将每个具体支付时间点抽象为策略类,具体设计如下。

image.png

六.具体实现

  • 数据库对象

package cn.zzf.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author GaoFeng2017
 * @date 2018-06-06 14:48:36
 **/

public class EmpDB {

    private Map<Integer,Employee> employees = new HashMap<>(16);
    private Map<Integer,Employee> members = new HashMap<>(16);

    private static EmpDB db = new EmpDB();

    private EmpDB() {
    }

    public Employee getEmployee(Integer employeeId) {
        return employees.get(employeeId);
    }

    public List<Employee> getAll() {
        return new ArrayList<>(employees.values());
    }

    public boolean removeEmployee(Integer employeeId) {
        return employees.remove(employeeId) != null;
    }

    public void addEmployee(Employee employee) {
        employees.put(employee.getEmployeeId(),employee);
    }

    public void clear() {
        employees.clear();
    }

    public Employee getMember(Integer memberIds) {
        return members.get(memberIds);
    }

    public void addUnionAffiliation(Integer memberId,Employee employee) {
        members.put(memberId,employee);
    }

    public void removeUnionAffiliation(Integer memberId) {
        members.remove(memberId);
    }

    public static EmpDB getDB() {
        return db;
    }
}

  • 增加雇员

image.png

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-06 14:38:11
 **/

public abstract class AddEmployeeTransaction implements Transaction {

    private EmpDB db = EmpDB.getDB();

    /** 雇员姓名 */
    private String name;

    /** 雇员住址 */
    private String address;

    /** 雇员Id */
    private Integer EmpId;

    public AddEmployeeTransaction(String name, String address, Integer empId) {
        this.name = name;
        this.address = address;
        EmpId = empId;
    }

    @Override
    public void execute() {
        Employee e = new Employee(this.name,this.address,this.EmpId);
        PaymentMethod method = new HoldMethod();
        e.setPaymentClassification(getPaymentClassification());
        e.setPaymentSchedule(getPaymentSchedule());
        e.setPaymentMethod(new HoldMethod());
        e.setAffiliation(new NoAffiliation());
        db.addEmployee(e);
    }

    /**
     * 获取雇员支付策略
     * @author: GaoFeng2017
     * @date: 2018/6/6 15:54
     * @param:
     * @return:
     *
     */
    public abstract PaymentClassification getPaymentClassification();

    /**
     * 获取支付时间表
     * @author: GaoFeng2017
     * @date: 2018/6/7 11:33
     * @param:
     * @return:
     *
     */
    public abstract PaymentSchedule getPaymentSchedule();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Integer getEmpId() {
        return EmpId;
    }

    public void setEmpId(Integer empId) {
        EmpId = empId;
    }
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-06 16:05:17
 **/

public class AddCommissionedTransaction extends AddEmployeeTransaction {

    private double monthSalary;
    private double commissionRate;

    public AddCommissionedTransaction(String name, String address, Integer empId, double monthSalary, double commissionRate) {
        super(name, address, empId);
        this.monthSalary = monthSalary;
        this.commissionRate = commissionRate;
    }

    @Override
    public PaymentClassification getPaymentClassification() {
        return new CommissionedClassification(monthSalary,commissionRate);
    }

    @Override
    public PaymentSchedule getPaymentSchedule() {
        return new BiWeeklySchedule();
    }
}

AddEmployeeTransaction类使用了模板方法模式,每个addTransaction添加的步骤是一样的,只是雇员类型和支付薪水时间表不同而已,该操作被封装成了模板方法,子类只需实现getClassification,getSchedule两个抽象方法。这里只给出Commission雇员类型的实现,需要完整代码的可以在github上下载,下面的代码也将省略掉一些“重复“实现。

  • 删除雇员

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 11:42:42
 **/

public class DeleteEmployeeTransaction implements Transaction{

    private Integer empId;
    private EmpDB db = EmpDB.getDB();

    public DeleteEmployeeTransaction(Integer empId) {
        this.empId = empId;
    }

    @Override
    public void execute() {
        db.removeEmployee(empId);
    }
}

  • 时间卡,销售凭条以及服务费用

image.png
image.png
image.png
package cn.zzf.impl;

import java.util.Objects;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 11:55:10
 **/

public class TimeCardTransaction implements Transaction {
    private Integer empId;
    private TimeCard timeCard;
    private EmpDB db = EmpDB.getDB();

    public TimeCardTransaction() {

    }

    public TimeCardTransaction(Integer empId, TimeCard timeCard) {
        this.empId = empId;
        this.timeCard = timeCard;
    }

    @Override
    public void execute() {
        Employee employee = db.getEmployee(empId);
        PaymentClassification paymentClassification = employee.getPaymentClassification();

        if (!Objects.equals(paymentClassification.getClass(),HourlyClassification.class)) {
            throw new RuntimeException("关联失败,雇员不是钟点工");
        }
        HourlyClassification classification = (HourlyClassification) employee.getPaymentClassification();
        classification.getTimeCards().add(timeCard);
    }
}

package cn.zzf.impl;

import java.util.Date;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 10:37:51
 **/

public class TimeCard {
    private Date date;
    private int hours;

    public TimeCard() {
    }

    public TimeCard(Date date, int hours) {
        this.date = date;
        this.hours = hours;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public int getHours() {
        return hours;
    }

    public void setHours(int hours) {
        this.hours = hours;
    }
}

package cn.zzf.impl;

import java.util.Date;
import java.util.LinkedList;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 13:40:22
 **/

public class ServiceChargeTransaction implements Transaction {

    private Integer memberId;
    private Date date;
    private Double amount;
    private EmpDB db  = EmpDB.getDB();


    public ServiceChargeTransaction() {
    }

    public ServiceChargeTransaction(Integer memberId, Date date, Double amount) {
        this.memberId = memberId;
        this.date = date;
        this.amount = amount;
    }

    @Override
    public void execute() {
        Employee employee = db.getMember(memberId);
        UnionAffiliation unionAffiliation = (UnionAffiliation) employee.getAffiliation();
        unionAffiliation.getServiceCharges().add(new ServiceCharge(date,amount));
    }
}

package cn.zzf.impl;

import java.util.Date;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 11:16:15
 **/

public class ServiceCharge {

    private Date date;
    private Double amount;

    public ServiceCharge() {
    }

    public ServiceCharge(Date date, Double amount) {
        this.date = date;
        this.amount = amount;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Double getAmount() {
        return amount;
    }

    public void setAmount(Double amount) {
        this.amount = amount;
    }
}

  • 更改雇员属性

image.png
image.png
package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 15:37:12
 **/

public abstract class ChangeEmployeeTransaction implements Transaction{
    private Integer empId;
    private EmpDB db = EmpDB.getDB();

    public ChangeEmployeeTransaction(Integer empId) {
        this.empId = empId;
    }

    public ChangeEmployeeTransaction() {
    }

    @Override
    public void execute() {
        Employee e = db.getEmployee(empId);
        if (e != null) {
            change(e);
        }
    }

    /** 改变雇员属性 */
    public abstract void change(Employee employee);
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 15:44:39
 **/

public class ChangeNameTransaction extends ChangeEmployeeTransaction {

    private String name;

    public ChangeNameTransaction(Integer empId, String name) {
        super(empId);
        this.name = name;
    }

    @Override
    public void change(Employee employee) {
        employee.setName(name);
    }
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 15:49:35
 *
 **/

public abstract class ChangeClassificationTransaction extends ChangeEmployeeTransaction {

    public ChangeClassificationTransaction(Integer empId) {
        super(empId);
    }

    @Override
    public void change(Employee employee) {
        employee.setPaymentClassification(getPaymentClassification());
        employee.setPaymentSchedule(getPaymentSchedule());
    }

    /**
     * 获取雇员要更改的支付策略
     * @author: GaoFeng2017
     * @date: 2018/6/7 16:00
     * @param:
     * @return:
     *
     */
    public abstract PaymentClassification getPaymentClassification();

    /**
     * 获取雇员要更改的薪水支付时间表
     * @author: GaoFeng2017
     * @date: 2018/6/7 16:00
     * @param:
     * @return:
     *
     */
    public abstract PaymentSchedule getPaymentSchedule();
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 16:05:16
 **/

public class ChangeHourlyTransaction extends ChangeClassificationTransaction {

    private Double hourlyRate;

    public ChangeHourlyTransaction(Integer empId, Double hourlyRate) {
        super(empId);
        this.hourlyRate = hourlyRate;
    }

    @Override
    public PaymentClassification getPaymentClassification() {
        return new HourlyClassification(hourlyRate);
    }

    @Override
    public PaymentSchedule getPaymentSchedule() {
        return new WeeklySchedule();
    }
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 16:39:10
 **/

public abstract class ChangeAffiliationTransaction extends ChangeEmployeeTransaction {

    public ChangeAffiliationTransaction(Integer empId) {
        super(empId);
    }

    @Override
    public void change(Employee employee) {
        recordMembership(employee);
        employee.setAffiliation(getAffiliation());
    }

    /**
     * 获取雇员要修改的会员
     *
     * @author: GaoFeng2017
     * @date: 2018/6/7 16:42
     * @param:
     * @return:
     *
     */
    public abstract Affiliation getAffiliation();


    /**
     * 确定当前雇员关系
     * @author GaoFeng2017
     * @date 2018/6/7 18:11
     * @param employee 雇员对象
     * @return
     *
     */
    public abstract void recordMembership(Employee employee);
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 16:49:04
 **/

public class ChangeMemberTransaction extends ChangeAffiliationTransaction {

    private Double dus;
    private Integer memberId;
    private EmpDB db = EmpDB.getDB();

    public ChangeMemberTransaction(Integer empId, Double dus, Integer memberId) {
        super(empId);
        this.dus = dus;
        this.memberId = memberId;
    }

    @Override
    public Affiliation getAffiliation() {
        return new UnionAffiliation(memberId,dus);
    }

    @Override
    public void recordMembership(Employee employee) {
        db.addUnionAffiliation(memberId,employee);
    }

}

package cn.zzf.impl;

import java.util.Objects;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 16:52:27
 **/

public class ChangeNoMemberTransaction extends ChangeAffiliationTransaction {

    private EmpDB db = EmpDB.getDB();

    public ChangeNoMemberTransaction(Integer empId) {
        super(empId);
    }

//    private

    @Override
    public Affiliation getAffiliation() {
        return new NoAffiliation();
    }

    @Override
    public void recordMembership(Employee employee) {
        Affiliation affiliation = employee.getAffiliation();

        if (Objects.equals(affiliation.getClass(),NoAffiliation.class)) {
            return;
        }

        UnionAffiliation unionAffiliation = (UnionAffiliation) affiliation;

        db.removeUnionAffiliation(unionAffiliation.getMemberId());
    }


}

package cn.zzf.impl;

import java.util.Date;
import java.util.LinkedList;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 10:59:57
 **/

public class UnionAffiliation implements Affiliation {

    private Integer memberId;
    private double dues;
    private LinkedList<ServiceCharge> serviceCharges = new LinkedList<>();

    public UnionAffiliation() {

    }


    public UnionAffiliation(Integer memberId, double dues) {
        this.memberId = memberId;
        this.dues = dues;
    }

    public double getDues() {
        return dues;
    }

    public void setDues(double dues) {
        this.dues = dues;
    }

    public LinkedList<ServiceCharge> getServiceCharges() {
        return serviceCharges;
    }

    public Integer getMemberId() {
        return memberId;
    }

    public void setMemberId(Integer memberId) {
        this.memberId = memberId;
    }

    @Override
    public Double calculateDeductions(PayCheck payCheck) {
        Date aDate = payCheck.getPayStartTime();
        Date bDate = payCheck.getPayDate();
        int count = DateUtil.getFridayCount(aDate,bDate);
        Double sum = dues * count;
        for (ServiceCharge serviceCharge : serviceCharges) {
            if (DateUtil.isBetweenDate(serviceCharge.getDate(),aDate,bDate)) {
                sum += serviceCharge.getAmount();
            }
        }
        return sum;
    }
}

package cn.zzf.impl;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 19:19:56
 **/

public class NoAffiliation implements Affiliation {
    @Override
    public Double calculateDeductions(PayCheck payCheck) {
        return 0.0;
    }
}

值得注意的是,这里的ChangeAffiliationTransaction多了一个名为recordMembership的抽象方法,它将会检测雇员是否加入过协会,并且解除已经加入的协会关系。

七.支付薪水

package cn.zzf.impl;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

/**
 * @author GaoFeng2017
 * @date 2018-06-06 14:48:51
 *
 **/

public class Employee {

    private String name;
    private String address;
    private Integer employeeId;
    private PaymentClassification paymentClassification;
    private PaymentMethod paymentMethod;
    private PaymentSchedule paymentSchedule;
    private Affiliation affiliation;

    public Employee(String name, String address, Integer employeeId) {
        this.name = name;
        this.address = address;
        this.employeeId = employeeId;
    }

    public Affiliation getAffiliation() {
        return affiliation;
    }

    public void setAffiliation(Affiliation affiliation) {
        this.affiliation = affiliation;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Integer getEmployeeId() {
        return employeeId;
    }

    public void setEmployeeId(Integer employeeId) {
        this.employeeId = employeeId;
    }

    public PaymentClassification getPaymentClassification() {
        return paymentClassification;
    }

    public void setPaymentClassification(PaymentClassification paymentClassification) {
        this.paymentClassification = paymentClassification;
    }

    public PaymentMethod getPaymentMethod() {
        return paymentMethod;
    }

    public void setPaymentMethod(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public PaymentSchedule getPaymentSchedule() {
        return paymentSchedule;
    }

    public void setPaymentSchedule(PaymentSchedule paymentSchedule) {
        this.paymentSchedule = paymentSchedule;
    }

    public Date getStartPayDate(Date date) {
        return paymentSchedule.getStartPayDate(date);
    }

    public void PayDay(PayCheck payCheck) {
        Double grossPay = paymentClassification.calculatePay(payCheck);
        Double deductions = affiliation.calculateDeductions(payCheck);
        Double netPay = grossPay - deductions;
        payCheck.setDeductions(deductions);
        payCheck.setGrossPay(grossPay);
        payCheck.setNetPay(netPay);
        paymentMethod.pay(payCheck);
    }

}

package cn.zzf.impl;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author GaoFeng2017
 * @date 2018-06-07 20:31:48
 **/

public class PaydayTransaction implements Transaction {

    private Date date;
    private EmpDB db = EmpDB.getDB();
    public static  Map<Integer,PayCheck> map = new HashMap<>(16);

    public PaydayTransaction(Date date) {
        this.date = date;
    }

    @Override
    public void execute() {
        List<Employee> employees = db.getAll();
        for (Employee employee : employees) {
            System.out.println(employee.getPaymentSchedule().isPayDay(date));
            if (employee.getPaymentSchedule().isPayDay(date)) {
                PayCheck payCheck = new PayCheck(employee.getStartPayDate(date),date);
                employee.PayDay(payCheck);
                map.put(employee.getEmployeeId(),payCheck);
            }
        }
    }
}

测试程序写的比较混乱,就不贴上去了。
当execute被调用执行时,将会遍历每一个雇员对象,并且和给定的支付日期匹配,如果当前雇员的支付日期为给定日期,就发放薪水。不过,这里需要依靠payCheck对象来检测是否重复发放薪水。
完整代码 https://github.com/ZGAOF/oop-design/tree/master/salary-pay/src/main/java/cn/zzf/impl

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

推荐阅读更多精彩内容

  • 我们在项目开发中,设计模式和理念决定了你做事的效率,如果你想让你的大脑存储一些重要的设计模式,好在关键的时候拿来就...
    tery007阅读 4,779评论 7 47
  • NSArray NSArray实例可以保存一组指向其他对象的指针。 17.1创建数组 NSArry实例一旦被创建后...
    帽子和五朵玫瑰阅读 145评论 0 0
  • 梦凉青青,灯火碎影。 箫歌遥遥,不闻升平。 ...
    忆夕笑雪阅读 155评论 0 0
  • 人生最重要的理财是打理自己的习惯,人生最重要的投资是投资自己的思想。 李笑来专栏不是教我们如何赚钱,而是教我们如何...
    寓凡阅读 571评论 0 0
  • 前言: 蝴蝶如要在百花园里得到飞舞的欢乐,那首先得忍受与蛹决裂的痛苦 ----------------------...
    z_j_r阅读 290评论 0 0