文中代码出现home、know等名词都是化名
在不同的页面中,有不同的数据需要导出Excel。首先不得不感概一下,目前公司项目代码封装的好,书写流畅,关于导出Excel这一块已经封装到实现很简单了
下面是截取之前已有的一段导出Excel的代码,方便文章的阅读我加上相应的注释
public IActionResult OldExcelExportPag1(long id)
{
//为excel取一个名字
var fileName = DateTime.Now.ToString("yyyyMMddhhmmss") + ".success.xlsx";
//_homeRepo是个接口,通过id去获取数据源
//数据源通过已封装的ToExcel去转换数据并映射为Excel的列
var result = _homeRepo.ExportSuccess(id).ToExcel(cfg => cfg
.Map(x => x.PostedOn, "Created on")
.Map(x => x.Phone, "Phone")
.Map(x => x.Street, "Street Address")
.Map(x => x.City, "City")
.Map(x => x.CountryCode, "Country"));
//调用File,输入Excel的数据源、ContentType、文件名导出
return File(result, "application/vnd.ms-excel", fileName);
}
我的第一版与上面相同,此时的想法是先以实现为主。和之前的代码相比,只有5处优点
public IActionResult MyExport(long homeId)
{
var fileName = $"homeNbr{homeId}Home{DateTime.Now:yyyyMMddhhmmss}.xlsx";
var source = _homeRepo
.ExportFamilyHomes(homeId)
.EachDo(x => x.know = "is not ready", x => x.Should);
var format = source.ToExcel(cfg => cfg
.Map(x => x.RefNum, "RefNumber")
.Map(x => x.Num1, "Num1#")
.Map(x => x.Num2, "Num2 #")
.Map(x => x.Num2Original, "Original Num2 #"));
return File(format, "application/vnd.ms-excel", fileName);
}
在第一版实现的过程中或是实现后,我思考的是下面的问题
首先给我的感觉是,太顺溜了,太过程化了。这个方法中的语句是否是同一个抽象层级,我分析每个步骤的目的如下
1和4是对Excel相关设置信息的细节描述,2和3是对数据源的细节描述,5是最终的目的,是导出,而我这个方法就是要导出,因此5层级最高,不care数据做了什么细节和怎么设置的excel,如果是下面的逻辑会不会更好
第二,在每一个导出的方法里最后都会写ContentType的这个字符串”application/vnd.ms-excel”,很明显抽出来容易修改、扩展、不易错……
我的第二版
exporter是在File才用到,为什么在source前创建?
public IActionResult MyExport(long homeId)
{
var exporter = new Excels.Home();
var source = _homeRepo
.ExportFamilyHomes(homeId)
.EachDo(x => x.know = "is not ready", x => x.Should);
return File(exporter.GetFormat(source), ExcelContentType.ContentType, exporter.GetFileName(homeId));
}
为了实现第二版新加以下接口
为了直接返回一个名字或者通过传入一个参数来生成一个名字
public interface IExcelFileName
{
string GetFileName();
string GetFileName<T>(T param);
}
为了获取数据格式(事实上这里的命名并不是完全准确,因为底层的方法ToExcel的方法做了两件事,将IEnumerable的数据源转换为Excel的数据源,并映射到相应的列名。而这个GetFormat返回的结果是Excel的数据源,但是其中调用ToExcel方法是看不到转换的过程,反而是看到每一个列的映射过程,因此取名为GetFormat)
interface IExcelFormat<T>
{
byte[] GetFormat(IEnumerable<T> source);
}
复用一个固定的字符串来当作Excel生成的ContentType
public class ExcelContentType
{
internal static string ContentType = "application/vnd.ms-excel";
}
我最终的目的是需要一个导出的工具,里面做好了导出需要做的任何细节,所以是一个总的接口继承多个小接口,接口隔离原则,保持每个接口尽可能的小
interface IExcelExporter<T> : IExcelFileName, IExcelFormat<T>
{
}
实现这个页面需要导出的具体细节,有点策略模式的味道。每个导出的地方,都需要FileName,但FileName不同;都需要Format,但Format不同。使用的时候通过interface创建各自需要的implement
GetFileName方法在这里并没有用到,因此实现throw NotImplementedException,如果误使用,直接的抛出错误提醒,远比返回string.Empty好,谙含“使用异常而非错误码”
public class Home : IExcelExporter<Family.Home>
{
public string GetFileName()
{
throw new NotImplementedException();
}
public string GetFileName<T>(T param)
=> $"homeNbr{param}Home{DateTime.Now:yyyyMMddhhmmss}.xlsx";
public byte[] GetFormat(IEnumerable<Family.Home> source)
=> source.ToExcel(cfg => cfg
.Map(x => x.RefNum, "RefNumber")
.Map(x => x.Num1, "Num1#")
.Map(x => x.Num2, "Num2 #")
.Map(x => x.Num2Original, "Original Num2 #"));
}
我的第三版
public IActionResult MyExport(long homeId)
{
var source = _homeRepo.ExportFamilyHomes(homeId);
var exporter = new Excels.Home {Source = source};
return File(exporter.GetFormat(), ExcelContentType.ContentType, exporter.GetFileName(homeId));
}
新增和变动接口如下
新增一个Generics接口用于贮存数据,在内部处理细节
interface IExcelSource<T>
{
IEnumerable<T> Source { set; }
}
数据已然贮存在内部,一气呵成的处理,去掉该接口方法中的参数,不需要方法注入了
interface IExcelFormat
{
byte[] GetFormat();
}
interface IExcelExporter<T> : IExcelSource<T>, IExcelFileName, IExcelFormat
{
}
数据在传入后,不再允许被获取,对外不提供get访问
public class Home : IExcelExporter<Family.Home>
{
public IEnumerable<Family.Home> Source { private get; set; }
public string GetFileName()
{
throw new NotImplementedException();
}
public string GetFileName<T>(T param)
=> $"homeNbr{param}Home{DateTime.Now:yyyyMMddhhmmss}.xlsx";
public IEnumerable<Family.Home> Process()
=> Source.EachDo(x => x.know = "is not ready", x => x.Should);
public byte[] GetFormat()
=> Process()
.ToExcel(cfg => cfg
.Map(x => x.RefNum, "RefNumber")
.Map(x => x.Num1, "Num1#")
.Map(x => x.Num2, "Num2 #")
.Map(x => x.Num2Original, "Original Num2 #"));
}
怎么解决下面两个问题?
感受:
We blather about stupid managers and intolerant customers and useless markerting types and telephone sanitizers. But the fault, dear Dillbert, is not in our stars, but in ourselves. We are unprofessional.
代码变得糟糕把问题归咎于愚蠢的经理、苛求的用户……是我们太不专业了。难度不关进度的事?难度不关那些愚蠢的经理和没用的营销手段的事?难道他们就不该负点责吗?不。
我对这段话抱着“你真幸运“的态度,潜在台词是”你说的真好听”
为什么要选公司,以上家公司来看,每周迭代不完的需求,不注重代码的整洁在那时那刻确实是开发最快的选择,成片的copy,UserController里每多一个页面涉及到注册会加一个Action,then复制吧……。你会说现在整洁以后会快,maybe you’re right。那我想说当你以工龄三年底层码农的身份拿着这份钱做这份事,连CEO都明确开会掷地有声的说不求好,能用,求快……,加上管理不当导致经常加无意义的班以及人员更迭频繁
我想Uncle Bob在年轻时候的工作一定不是在这样一家公司,在这样的环境下,还有时间去优化?还有时间去思考、写书?呵呵哒,洗衣服做家务照顾猫咪和wife的时间都需要挤出来的。以我在他的Twitter和书中表达出来的语气来看,他必定会很赞同我这个观点,选择一家好的公司至关重要
In the end of this blog我还想说为什么选择一家好的公司很重要。目前的公司开发不管从前到后很顺畅;我有时间去思考怎么可以做的更好;我可以接触一些居然百度搜索不到的pattern去学习然后提交到GitHub,这些都是多么幸运和合理的事情
I’m proud of I did something at afternoon. I’m glad I work at a good company. This kind of company won’t destory your work life balance at the most of time. It doesn’t get you into a never-ending spiral. It allows you to see a new landscape. That’s my definition of good. —Eden