我们在项目开发中,设计模式和理念决定了你做事的效率,如果你想让你的大脑存储一些重要的设计模式,好在关键的时候拿来就用,那就仔细看看这个薪水支付案例吧。
案例来源:《Agile Software Dvelopment Principles》(敏捷软件开发)一书
思路启发者:码农翻身创始人刘欣老师
涉及到的基础知识:
1. 类之间的关系:实现、依赖、关联、聚合(has-a)、组合(contains-a);
2. 设计模式:组合模式、策略模式等;
3. 设计原则:OCP(单一职责)、SRP(开闭原则)等
需求提炼:
1.员工类型:
小时工:每天提交工作时间卡,记录了日、工作小时数,如果每天工作超过12小时,按2倍进行支付, 每周五支付;
固定薪资:每个月的最后一个工作日对他们进行支付,在员工中有个月薪字段;
销售员:带薪的员工,按照提成比例给佣金。提交销售凭条,记录日期和金额。在员工中有一个提成比例字段。 每隔一周的周五支付;
2.支付方式:
支票邮寄、保存在财务、银行转账;
3.扣除项:
在雇员记录中有一个每周应付款项字段,这些应付款需要从他们的薪水中扣除。
4.运行周期:
程序每个工作日运行一到多次, 对相应的员工进行支付,系统在支付前需要计算出每个员工的支付日期,这样就可以计算出从上次支付日期到支付日期间应付的薪资。
开始设计:
员工类如何设计:
思路一:将员工分为三种类型,用UML类图表示就是:
思路二:用一个类来表示,即在一个主类Employee中用type字段来标识员工类型,这种方式是我们平常最常见的方式,但在这个案例中满足不了需求,大家接着往下看。
用一个父类Employee,让三个子类类继承它(实现共同属性通用的目的):
支付方式,我们很容易做出三种类来代表:
但是这三种支付类型如何与Employee关联起来呢,我们应该进一步抽象,让它们的爸爸PayMethod去做对接:
此时再看这个类图,他们的关系应该是这样的:
关系说明:
1.SalesSlip、TimeCard与SalesEmployee、HourEmployee为组合关系(同生共死);
2.PayMethod类与Employee类的关系为聚合(局部可单独存在,即PayMethod离开Employee也可单独存在)
问题来了!
你是不是觉得事情没有那么简单,那恭喜你,说明你是个爱思考的小码。假如公司的小时工转岗为销售了,按照销售类来支付,怎么办?
其中一种解决思路就是,在Employee中增加type字段来表示员工类型,如果在数据库中有员工类型字段,那么将员工类型改掉就可以满足变化的需求了。但是如果这个员工是做了半个月固薪员工,半个月的销售员,那么他的薪资怎么算?
所以我们会发现在做抽象时,抽象的如果是不变的部分,那就搞错方向了,应该提取变化的部分做抽象。
好的思路是将员工的支付抽象为支付策略(策略模式--将不同的算法封装起来):
谁负责计算薪水?
好,我们继续,下一个要解决的问题是在哪里计算薪水?看起来让PayClassify负责最合适不过了。简单地想一下它是如何计算的:给定日期,if判断如果这一天是支付日,则进行薪水计算:
就在孩子们辛苦工作的时候,OCP老人家过来狠狠地敲了一下PayClassify老爹的头:你怎么这么糊涂,你干嘛让你的孩子又判断是否是付薪日,又做薪水计算!我这一辈子不断地告诉世人,一辈子只要做好一件事就可以了,这是我生命的意义,也是你们少走弯路的捷径啊!
抽象“变化的支付日”
支付日有三种:每周五支付,隔一周周五支付,月底支付。这三种支付日期抽象为三个类,并他们的父类PayDateUtil与Employee关联。
计算薪水的细节问题:
1.小时支付类型: sum (每个时间卡 x 每小时报酬) ,计算过去一周的时间卡
2.提成类型: 底薪 + sum ( 每个销售凭条的销售额 x 提成比例 ) ,过去两周的销售凭条
3.固薪类型: 固定的薪水
还有一个头疼的问题没有解决
谁来记录已经发薪的员工,保证系统重新运行时不会重发薪水?
我们需要一个类来单独负责运行检查:PayDetail,这个类主要的职责是跟着Employee对象,在计算薪水、扣除项时全部在场:
到这里,我们还剩下最后一个需求没有解决:扣除项。我们用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 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);
}
}
}
}
代码只放了一些关键的类代码,如果小伙伴们有兴趣,可以自己去实现一下。