敏捷软件开发_实例2<四>

上一章中对薪水支付案例的用例和类做了详细的阐述,在本篇会介绍薪水支付案例包的划分和数据库,UI的设计。

包的划分

一个错误包的划分

为什么这个包是错误的:

  • 如果对classifications更改就要影响payrolldatabase更改,还会迫使transactions更改,tansactions重新发布和编译测试就是不负责的,transactions没有共享封闭性,每个类都有自己变化的敏感,所以发布的频率非常高,是不合理的。

调整一下:

将具体类和具体类打包,抽象类和抽象类打包,交互类单独打包。这已经是一个比较好打包设计了。

类的组件应该要符合共同重用原则,payrolldamain中的类没有形成最小的可重用单元,transaction类不必和组件中的其他类一起重用,可以把transaction迁移到transactionapplication类中

这样的划分太精细了,是否有这样的必要需要整体来看。

最终包的结构:

数据库的设计

emplogee是核心

完成这个设计需要进行重构:

  • 提取出payrolldatabase接口,

      public interface PayrollDatabase
    {
    void AddEmployee(Employee employee);
    Employee GetEmployee(int id);
    void DeleteEmployee(int id);
    void AddUnionMember(int id, Employee e);
    Employee GetUnionMember(int id);
    void RemoveUnionMember(int memberId);
    ArrayList GetAllEmployeeIds();
    IList GetAllEmployees();
    }
  • 内存表实例:

      public class InMemoryPayrollDatabase : PayrollDatabase
    {
    private static Hashtable employees = new Hashtable();
    private static Hashtable unionMembers = new Hashtable(); public void AddEmployee(Employee employee)
    {
    employees[employee.EmpId] = employee;
    } // etc...
    public Employee GetEmployee(int id)
    {
    return employees[id] as Employee;
    } public void DeleteEmployee(int id)
    {
    employees.Remove(id);
    } public void AddUnionMember(int id, Employee e)
    {
    unionMembers[id] = e;
    } public Employee GetUnionMember(int id)
    {
    return unionMembers[id] as Employee;
    } public void RemoveUnionMember(int memberId)
    {
    unionMembers.Remove(memberId);
    } public ArrayList GetAllEmployeeIds()
    {
    return new ArrayList(employees.Keys);
    } public IList GetAllEmployees()
    {
    return new ArrayList(employees.Values);
    } public void Clear()
    {
    employees.Clear();
    unionMembers.Clear();
    }
    }
  • 数据库

      public class SqlPayrollDatabase : PayrollDatabase
    {
    private SqlConnection connection; public SqlPayrollDatabase()
    {
    connection = new SqlConnection("Initial Catalog=Payroll;Data Source=localhost;user id=sa;password=abc");
    connection.Open();
    } ~SqlPayrollDatabase()
    {
    connection.Close();
    } public void AddEmployee(Employee employee)
    {
    //增加员工策略
    SaveEmployeeOperation operation = new SaveEmployeeOperation(employee, connection);
    operation.Execute();
    } public Employee GetEmployee(int id)
    {
    //数据库事务
    LoadEmployeeOperation loadOperation = new LoadEmployeeOperation(id, connection);
    loadOperation.Execute();
    return loadOperation.Employee;
    } public void DeleteEmployee(int id)
    {
    throw new NotImplementedException();
    } public void AddUnionMember(int id, Employee e)
    {
    throw new NotImplementedException();
    } public Employee GetUnionMember(int id)
    {
    throw new NotImplementedException();
    } public void RemoveUnionMember(int memberId)
    {
    throw new NotImplementedException();
    } public ArrayList GetAllEmployeeIds()
    {
    throw new NotImplementedException();
    } public IList GetAllEmployees()
    {
    throw new NotImplementedException();
    } }
  • 如果插入雇佣记录成功,但是支付记录失败,为了解决这个问题而使用事务的方式。

      public class SaveEmployeeOperation
    {
    private readonly Employee employee;
    private readonly SqlConnection connection; private string methodCode;
    private string classificationCode;
    private SqlCommand insertPaymentMethodCommand;
    private SqlCommand insertEmployeeCommand;
    private SqlCommand insertClassificationCommand; public SaveEmployeeOperation(Employee employee, SqlConnection connection)
    {
    this.employee = employee;
    this.connection = connection;
    } public void Execute()
    {
    PrepareToSavePaymentMethod(employee);
    PrepareToSaveClassification(employee);
    PrepareToSaveEmployee(employee); SqlTransaction transaction = connection.BeginTransaction("Save Employee");
    try
    {
    ExecuteCommand(insertEmployeeCommand, transaction);
    ExecuteCommand(insertPaymentMethodCommand, transaction);
    ExecuteCommand(insertClassificationCommand, transaction);
    transaction.Commit();
    }
    catch(Exception e)
    {
    transaction.Rollback();
    throw e;
    }
    } private void ExecuteCommand(SqlCommand command, SqlTransaction transaction)
    {
    if(command != null)
    {
    command.Connection = connection;
    command.Transaction = transaction;
    command.ExecuteNonQuery();
    }
    } private void PrepareToSaveEmployee(Employee employee)
    {
    string sql = "insert into Employee values (" +
    "@EmpId, @Name, @Address, @ScheduleType, " +
    "@PaymentMethodType, @PaymentClassificationType)";
    insertEmployeeCommand = new SqlCommand(sql); this.insertEmployeeCommand.Parameters.Add("@EmpId", employee.EmpId);
    this.insertEmployeeCommand.Parameters.Add("@Name", employee.Name);
    this.insertEmployeeCommand.Parameters.Add("@Address", employee.Address);
    this.insertEmployeeCommand.Parameters.Add("@ScheduleType", ScheduleCode(employee.Schedule));
    this.insertEmployeeCommand.Parameters.Add("@PaymentMethodType", methodCode);
    this.insertEmployeeCommand.Parameters.Add("@PaymentClassificationType", classificationCode);
    } private void PrepareToSavePaymentMethod(Employee employee)
    {
    PaymentMethod method = employee.Method;
    if(method is HoldMethod)
    methodCode = "hold";
    else if(method is DirectDepositMethod)
    {
    methodCode = "directdeposit";
    DirectDepositMethod ddMethod = method as DirectDepositMethod;
    insertPaymentMethodCommand = CreateInsertDirectDepositCommand(ddMethod, employee);
    }
    else if(method is MailMethod)
    {
    methodCode = "mail";
    MailMethod mailMethod = method as MailMethod;
    insertPaymentMethodCommand = CreateInsertMailMethodCommand(mailMethod, employee);
    }
    else
    methodCode = "unknown";
    } private SqlCommand CreateInsertDirectDepositCommand(DirectDepositMethod ddMethod, Employee employee)
    {
    string sql = "insert into DirectDepositAccount values (@Bank, @Account, @EmpId)";
    SqlCommand command = new SqlCommand(sql);
    command.Parameters.Add("@Bank", ddMethod.Bank);
    command.Parameters.Add("@Account", ddMethod.AccountNumber);
    command.Parameters.Add("@EmpId", employee.EmpId);
    return command;
    } private SqlCommand CreateInsertMailMethodCommand(MailMethod mailMethod, Employee employee)
    {
    string sql = "insert into PaycheckAddress values (@Address, @EmpId)";
    SqlCommand command = new SqlCommand(sql);
    command.Parameters.Add("@Address", mailMethod.Address);
    command.Parameters.Add("@EmpId", employee.EmpId);
    return command;
    } private void PrepareToSaveClassification(Employee employee)
    {
    PaymentClassification classification = employee.Classification;
    if(classification is HourlyClassification)
    {
    classificationCode = "hourly";
    HourlyClassification hourlyClassification = classification as HourlyClassification;
    insertClassificationCommand = CreateInsertHourlyClassificationCommand(hourlyClassification, employee);
    }
    else if(classification is SalariedClassification)
    {
    classificationCode = "salary";
    SalariedClassification salariedClassification = classification as SalariedClassification;
    insertClassificationCommand = CreateInsertSalariedClassificationCommand(salariedClassification, employee);
    }
    else if(classification is CommissionClassification)
    {
    classificationCode = "commission";
    CommissionClassification commissionClassification = classification as CommissionClassification;
    insertClassificationCommand = CreateInsertCommissionClassificationCommand(commissionClassification, employee);
    }
    else
    classificationCode = "unknown"; } private SqlCommand CreateInsertHourlyClassificationCommand(HourlyClassification classification, Employee employee)
    {
    string sql = "insert into HourlyClassification values (@HourlyRate, @EmpId)";
    SqlCommand command = new SqlCommand(sql);
    command.Parameters.Add("@HourlyRate", classification.HourlyRate);
    command.Parameters.Add("@EmpId", employee.EmpId);
    return command;
    } private SqlCommand CreateInsertSalariedClassificationCommand(SalariedClassification classification, Employee employee)
    {
    string sql = "insert into SalariedClassification values (@Salary, @EmpId)";
    SqlCommand command = new SqlCommand(sql);
    command.Parameters.Add("@Salary", classification.Salary);
    command.Parameters.Add("@EmpId", employee.EmpId);
    return command;
    } private SqlCommand CreateInsertCommissionClassificationCommand(CommissionClassification classification, Employee employee)
    {
    string sql = "insert into CommissionedClassification values (@Salary, @Commission, @EmpId)";
    SqlCommand command = new SqlCommand(sql);
    command.Parameters.Add("@Salary", classification.BaseRate);
    command.Parameters.Add("@Commission", classification.CommissionRate);
    command.Parameters.Add("@EmpId", employee.EmpId);
    return command;
    } private static string ScheduleCode(PaymentSchedule schedule)
    {
    if(schedule is MonthlySchedule)
    return "monthly";
    if(schedule is WeeklySchedule)
    return "weekly";
    if(schedule is BiWeeklySchedule)
    return "biweekly";
    else
    return "unknown";
    }
    }

界面设计

界面设计时最好使得业务行为和UI分离,这里使用model view presenter模式(MVP)

  • model:实体层和数据库交互
  • view:界面层
  • presenter:业务处理层

MVP的作用是解耦界面、业务和实体的关系

  • 在presenter中主动使用view,界面的形态都是由presenter去控制,就是在presenter中去注册view事件,当用户触发事件时,这个事件会通过view传递到presenter中,并通过presenter调用model数据方法,最后presenter 调用引用的view实例去改变界面的形态。

public class AddEmployeePresenter
{
private TransactionContainer transactionContainer;
private AddEmployeeView view;
private PayrollDatabase database; private int empId;
private string name;
private string address;
private bool isHourly;
private double hourlyRate;
private bool isSalary;
private double salary;
private bool isCommission;
private double commissionSalary;
private double commission; public AddEmployeePresenter(AddEmployeeView view,
TransactionContainer container,
PayrollDatabase database)
{
this.view = view;
this.transactionContainer = container;
this.database = database;
} public int EmpId
{
get { return empId; }
set
{
empId = value;
UpdateView();
}
} public string Name
{
get { return name; }
set
{
name = value;
UpdateView();
}
} public string Address
{
get { return address; }
set
{
address = value;
UpdateView();
}
} public bool IsHourly
{
get { return isHourly; }
set
{
isHourly = value;
UpdateView();
}
} public double HourlyRate
{
get { return hourlyRate; }
set
{
hourlyRate = value;
UpdateView();
}
} public bool IsSalary
{
get { return isSalary; }
set
{
isSalary = value;
UpdateView();
}
} public double Salary
{
get { return salary; }
set
{
salary = value;
UpdateView();
}
} public bool IsCommission
{
get { return isCommission; }
set
{
isCommission = value;
UpdateView();
}
} public double CommissionSalary
{
get { return commissionSalary; }
set
{
commissionSalary = value;
UpdateView();
}
} public double Commission
{
get { return commission; }
set
{
commission = value;
UpdateView();
}
} private void UpdateView()
{
if(AllInformationIsCollected())
view.SubmitEnabled = true;
else
view.SubmitEnabled = false;
} public bool AllInformationIsCollected()
{
bool result = true;
result &= empId > 0;
result &= name != null && name.Length > 0;
result &= address != null && address.Length > 0;
result &= isHourly || isSalary || isCommission;
if(isHourly)
result &= hourlyRate > 0;
else if(isSalary)
result &= salary > 0;
else if(isCommission)
{
result &= commission > 0;
result &= commissionSalary > 0;
}
return result;
} public TransactionContainer TransactionContainer
{
get { return transactionContainer; }
} public virtual void AddEmployee()
{
transactionContainer.Add(CreateTransaction());
} public Transaction CreateTransaction()
{
if(isHourly)
return new AddHourlyEmployee(
empId, name, address, hourlyRate, database);
else if(isSalary)
return new AddSalariedEmployee(
empId, name, address, salary, database);
else
return new AddCommissionedEmployee(
empId, name, address, commissionSalary,
commission, database);
}
} public interface ViewLoader
{
void LoadPayrollView();
void LoadAddEmployeeView(
TransactionContainer transactionContainer);
} public class WindowViewLoader : ViewLoader
{
private readonly PayrollDatabase database;
private Form lastLoadedView; public WindowViewLoader(PayrollDatabase database)
{
this.database = database;
} public void LoadPayrollView()
{
PayrollWindow view = new PayrollWindow();
PayrollPresenter presenter =
new PayrollPresenter(database, this); view.Presenter = presenter;
presenter.View = view; // 相互关联 LoadView(view);
} public void LoadAddEmployeeView(
TransactionContainer transactionContainer)
{
AddEmployeeWindow view = new AddEmployeeWindow();
AddEmployeePresenter presenter =
new AddEmployeePresenter(view,
transactionContainer, database); view.Presenter = presenter; LoadView(view);
} private void LoadView(Form view)
{
view.Show();
lastLoadedView = view;
}
/// <summary>
/// 最新的form
/// </summary>
public Form LastLoadedView
{
get { return lastLoadedView; }
}
} public class PayrollMain
{
public static void Main(string[] args)
{
PayrollDatabase database =
new InMemoryPayrollDatabase();
WindowViewLoader viewLoader =
new WindowViewLoader(database); viewLoader.LoadPayrollView();
Application.Run(viewLoader.LastLoadedView);
}
} public class PayrollPresenter
{
private PayrollView view;
private readonly PayrollDatabase database;
private readonly ViewLoader viewLoader;
private TransactionContainer transactionContainer; public PayrollPresenter(PayrollDatabase database,
ViewLoader viewLoader)
{
//this.view = view;
this.database = database;
this.viewLoader = viewLoader;
TransactionContainer.AddAction addAction =
new TransactionContainer.AddAction(TransactionAdded);
transactionContainer = new TransactionContainer(addAction);
} public PayrollView View
{
get { return view; }
set { view = value; }
} public TransactionContainer TransactionContainer
{
get { return transactionContainer; }
} public void TransactionAdded()
{
UpdateTransactionsTextBox();
} private void UpdateTransactionsTextBox()
{
StringBuilder builder = new StringBuilder();
foreach(Transaction transaction in
transactionContainer.Transactions)
{
builder.Append(transaction.ToString());
builder.Append(Environment.NewLine);
}
view.TransactionsText = builder.ToString();
} public PayrollDatabase Database
{
get { return database; }
} public virtual void AddEmployeeActionInvoked()
{
viewLoader.LoadAddEmployeeView(transactionContainer);
} public virtual void RunTransactions()
{
foreach(Transaction transaction in
transactionContainer.Transactions)
transaction.Execute(); transactionContainer.Clear();
UpdateTransactionsTextBox();
UpdateEmployeesTextBox();
} private void UpdateEmployeesTextBox()
{
StringBuilder builder = new StringBuilder();
foreach(Employee employee in database.GetAllEmployees())
{
builder.Append(employee.ToString());
builder.Append(Environment.NewLine);
}
view.EmployeesText = builder.ToString();
}
} public class TransactionContainer
{
public delegate void AddAction(); private IList transactions = new ArrayList();
private AddAction addAction; public TransactionContainer(AddAction action)
{
addAction = action;
} public IList Transactions
{
get { return transactions; }
} public void Add(Transaction transaction)
{
transactions.Add(transaction);
if(addAction != null)
addAction();
} public void Clear()
{
transactions.Clear();
}
} public class AddEmployeeWindow : Form, AddEmployeeView
{
public System.Windows.Forms.TextBox empIdTextBox;
private System.Windows.Forms.Label empIdLabel;
private System.Windows.Forms.Label nameLabel;
public System.Windows.Forms.TextBox nameTextBox;
private System.Windows.Forms.Label addressLabel;
public System.Windows.Forms.TextBox addressTextBox;
public System.Windows.Forms.RadioButton hourlyRadioButton;
public System.Windows.Forms.RadioButton salaryRadioButton;
public System.Windows.Forms.RadioButton commissionRadioButton;
private System.Windows.Forms.Label hourlyRateLabel;
public System.Windows.Forms.TextBox hourlyRateTextBox;
private System.Windows.Forms.Label salaryLabel;
public System.Windows.Forms.TextBox salaryTextBox;
private System.Windows.Forms.Label commissionSalaryLabel;
public System.Windows.Forms.TextBox commissionSalaryTextBox;
private System.Windows.Forms.Label commissionLabel;
public System.Windows.Forms.TextBox commissionTextBox;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Label label1;
private System.ComponentModel.Container components = null;
public System.Windows.Forms.Button submitButton;
private AddEmployeePresenter presenter; public AddEmployeeWindow()
{
InitializeComponent();
} protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
public AddEmployeePresenter Presenter
{
get { return presenter; }
set { presenter = value; }
} private void hourlyRadioButton_CheckedChanged(
object sender, System.EventArgs e)
{
hourlyRateTextBox.Enabled = hourlyRadioButton.Checked;
presenter.IsHourly = hourlyRadioButton.Checked;
} private void salaryRadioButton_CheckedChanged(
object sender, System.EventArgs e)
{
salaryTextBox.Enabled = salaryRadioButton.Checked;
presenter.IsSalary = salaryRadioButton.Checked;
} private void commissionRadioButton_CheckedChanged(
object sender, System.EventArgs e)
{
commissionSalaryTextBox.Enabled =
commissionRadioButton.Checked;
commissionTextBox.Enabled =
commissionRadioButton.Checked;
presenter.IsCommission =
commissionRadioButton.Checked;
} private void empIdTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.EmpId = AsInt(empIdTextBox.Text);
} private void nameTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.Name = nameTextBox.Text;
} private void addressTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.Address = addressTextBox.Text;
} private void hourlyRateTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.HourlyRate = AsDouble(hourlyRateTextBox.Text);
} private void salaryTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.Salary = AsDouble(salaryTextBox.Text);
} private void commissionSalaryTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.CommissionSalary =
AsDouble(commissionSalaryTextBox.Text);
} private void commissionTextBox_TextChanged(
object sender, System.EventArgs e)
{
presenter.Commission = AsDouble(commissionTextBox.Text);
} private void addEmployeeButton_Click(
object sender, System.EventArgs e)
{
presenter.AddEmployee();
this.Close();
} private double AsDouble(string text)
{
try
{
return Double.Parse(text);
}
catch (Exception)
{
return 0.0;
}
} private int AsInt(string text)
{
try
{
return Int32.Parse(text);
}
catch (Exception)
{
return 0;
}
} public bool SubmitEnabled
{
set { submitButton.Enabled = value; }
}
}

完毕

敏捷软件开发_实例2<四>的更多相关文章

  1. 敏捷软件开发_实例1<二>

    敏捷软件开发_实例1 这本书的实例非常好,给了我非常多的启发.主要讲了两个实例,咖啡机和薪水支付实例,咖啡机实例比较简单并没有用什么设计模式,薪水支付实例用了很多设计模式,包括后面的打包等. 咖啡机实 ...

  2. 敏捷软件开发_设计原则<三>

    敏捷软件开发_设计原则 单一职责原则(single responsibilities principle,SRP) 原理:一个类应该只有一个变化 分离职责:如果不耦合的职责那么很简单,如果两个职责耦合 ...

  3. 敏捷软件开发_UML<一>

    敏捷软件开发_UML  所看书籍是:敏捷软件开发_原则.模式与实践_C#版(美)马丁著,这本书写的非常棒,感谢作者.该归纳总结的过程按照我读的顺序写. UML  在建造桥梁,零件,自动化设备之前需要建 ...

  4. 敏捷软件开发vs传统软件开发

    摘要 本文介绍了传统软件开发(着重介绍了传统软件开发中常用的瀑布模型)和敏捷软件开发,以及敏捷开发和传统开发的对比. 一.传统软件开发 比较常用的几种传统软件开发方法:瀑布式开发.迭代式开发.螺旋开发 ...

  5. 敏捷软件开发:原则、模式与实践——第12章 ISP:接口隔离原则

    第12章 ISP:接口隔离原则 不应该强迫客户程序依赖并未使用的方法. 这个原则用来处理“胖”接口所存在的缺点.如果类的接口不是内敛的,就表示该类具有“胖”接口.换句话说,类的“胖”接口可以分解成多组 ...

  6. 敏捷软件开发:原则、模式与实践——第10章 LSP:Liskov替换原则

    第10章 LSP:Liskov替换原则    Liskov替换原则:子类型(subtype)必须能够替换掉它们的基类型(base type). 10.1 违反LSP的情形 10.1.1 简单例子 对L ...

  7. 敏捷软件开发 Agile software Development(转)

    原文链接: http://www.cnblogs.com/kkun/archive/2011/07/06/2099253.html 敏捷软件开发 Agile software Development ...

  8. 《敏捷软件开发-原则、方法与实践》-Robert C. Martin读书笔记(转)

    Review of Agile Software Development: Principles, Patterns, and Practices 本书主要包含4部分内容,这些内容对于今天的软件工程师 ...

  9. 敏捷软件开发:原则、模式与实践——第13章 写给C#程序员的UML概述

    第13章 写给C#程序员的UML概述 UML包含3类主要的图示.静态图(static diagram)描述了类.对象.数据结构以及它们之间的关系,藉此表现出了软件元素间那些不变的逻辑结构.动态图(dy ...

随机推荐

  1. MySQL插入数据时报错Cause: java.sql.SQLException: #HY000的解决方法

    数据库中有字段要求不能为空,但是insert插入的时候,改字段没有值

  2. PlayJava Day020

    1.异常Exception补充: ①错误(Error)指的是致命性错误,一般无法处理 ②异常以类的形式封装 程序可以处理的异常对应的类是java.lang.Exception及其子类 运行时异常对应的 ...

  3. HTML识别后台传输或者js变量中字符串里的 '\n' 并成功换行显示

    HTML识别 string 里的 '\n' 并成功换行显示 设置标签的的css属性 white-space: pre-line; <div style='white-space: pre-lin ...

  4. 7天教你精通变大神,学CAD关键还要掌握方法,纯干货新手要看

    接触CAD初期是“痛苦”的,“煎熬”的,也是充满“成就”的. 痛苦是初学者怎么都不懂,需要学习的东西很多,整个过程是有些痛苦的. 煎熬也是每个求学阶段都会遇到的状态,眼睛会了,手不会,这个状态很难受. ...

  5. Easy User Manager System writeup

    0x01 解题 思路 一个进程用自己的ip去申请拿到code然后进入verify页面,另外一个进程去申请8.8.8.8 步骤 1. 首先注册一个账号 然后用两个不同的浏览器进入Change页面.这里我 ...

  6. python小技巧 小知识

    python小技巧 小知识 python系统变量(修改调用shell命令路径)或用户空间说明 20150418 python调用系统命令,报找不到.怎么办? 类似执行shell的: [ -f /etc ...

  7. Java读写Excel文件,利用POI

    直接看工具类代码吧, package com.example.demo.util; import com.example.demo.entity.ExcelDataVO; import org.apa ...

  8. Mysql中事务ACID实现原理

    引言 照例,我们先来一个场景~ 面试官:"知道事务的四大特性么?"你:"懂,ACID嘛,原子性(Atomicity).一致性(Consistency).隔离性(Isola ...

  9. fastdfs使用总结

    参考:https://www.cnblogs.com/chiangchou/p/fastdfs.html   说明:这篇博客是为了记录我在安装和使用FastDFS分布式文件系统时遇到的问题和解决方法, ...

  10. Python画一棵漂亮的樱花树(不同种樱花+玫瑰+圣诞树喔)

    不少用Python(大多是turtle库)绘制的树图,感觉很漂亮,我整理了一下,挑了一些我觉得不错的代码分享给大家(这些我都测试过,确实可以生成喔~)one 樱花树 动态生成樱花效果图(这个是动态的) ...