一次設(shè)計(jì)演進(jìn)之旅
一、需求背景
我們需要實(shí)現(xiàn)對存儲在HDFS中的Parquet文件執(zhí)行數(shù)據(jù)查詢,并通過REST API暴露給前端以供調(diào)用。由于查詢的結(jié)果可能數(shù)量較大,要求API接口能夠提供分頁查詢。在第一階段,需要支持的報(bào)表有5張,需要查詢的數(shù)據(jù)表與字段存在一定差異,查詢條件也有一定差異。
每個(gè)報(bào)表的查詢都牽涉到多張表的Join。每張表都被創(chuàng)建為數(shù)據(jù)集,對應(yīng)為一個(gè)Parquet文件。Parquet文件夾名就是數(shù)據(jù)集名,名稱是系統(tǒng)自動(dòng)生成的,所以我們需要建立業(yè)務(wù)數(shù)據(jù)表名、Join別名以及自動(dòng)生成的數(shù)據(jù)集名的映射關(guān)系。數(shù)據(jù)集對應(yīng)的各個(gè)字段信息都存儲在Field元數(shù)據(jù)表中,其中我們需要的三個(gè)主要屬性為:
- CodeName:創(chuàng)建數(shù)據(jù)集時(shí),由系統(tǒng)自動(dòng)生成
 - FieldName:為客戶數(shù)據(jù)源對應(yīng)數(shù)據(jù)表的字段名
 - DisplayName:為報(bào)表顯示的列名
 
說明:為了便于理解,我將要實(shí)現(xiàn)的五個(gè)報(bào)表分別按照序號命名。
二、解決方案
1. 前置條件
本需求是圍繞著我們已有的BI產(chǎn)品做定制開發(fā)。現(xiàn)有產(chǎn)品已經(jīng)提供了如下功能:
- 通過Spark SQL讀取指定Parquet文件,但不支持同時(shí)讀取多個(gè)Parquet文件,并對獲得的DataFrame進(jìn)行Join
 - 獲取存儲在MySQL中的DataSet與Field元數(shù)據(jù)信息
 - 基于AKKA Actor的異步查詢
 
2. 項(xiàng)目目標(biāo)
交付日期非常緊急,尤其需要盡快提供最緊急的第一張報(bào)表:定期賬戶掛失后辦理支取。后續(xù)的報(bào)表也需要盡快交付,同時(shí)也應(yīng)盡可能考慮到代碼的重用,因?yàn)閳?bào)表查詢業(yè)務(wù)的相似度較高。
3. 整體方案
基于各個(gè)報(bào)表的具體需求,解析并生成查詢Parquet(事實(shí)上是讀取多個(gè))的Spark SQL語句。生成的SQL語句會(huì)交給Actor,并由Actor請求Spark的SQLContext執(zhí)行SQL語句,獲得DataFrame。利用take()結(jié)合zipWithIndex實(shí)現(xiàn)對DataFrame的分頁,轉(zhuǎn)換為前端需要的數(shù)據(jù)。
根據(jù)目前對報(bào)表的分析,生成的SQL語句包含join、where與order by子句。報(bào)表需要查詢的數(shù)據(jù)表是在系統(tǒng)中硬編碼的,然后通過數(shù)據(jù)表名到DataSet中查詢元數(shù)據(jù)信息,獲得真實(shí)的由系統(tǒng)生成的數(shù)據(jù)集名。查詢的字段名同樣通過硬編碼方式,并根據(jù)對應(yīng)數(shù)據(jù)集的ID與字段名獲得Field的元數(shù)據(jù)信息。
三、設(shè)計(jì)演進(jìn)
1. 引入模板方法模式
考慮到SQL語句具有一定的通用性(如select的字段、表名與join表名、on關(guān)鍵字、where條件、排序等),差異在于不同報(bào)表需要的表名、字段以及查詢條件。通過共性與可變性分析,我把相同的實(shí)現(xiàn)邏輯放在一個(gè)模板方法中,而將差異的內(nèi)容(也即各個(gè)報(bào)表特定的部分)交給子類去實(shí)現(xiàn)。這是一個(gè)典型的模板方法模式:
- trait ReportTypeParser extends DataSetFetcher with ParcConfiguration {
 - def sqlFor(criteria: Option[List[Condition]]): String
 - def criteriaFields: Array[Field]
 - private[parc] def predefinedTables: List[TableName]
 - private[parc] def predefinedFields: List[TableField]
 - def generateHeaders: Array[Field] = {
 - predefinedFields.map(tf => tf.fieldName.field(tf.table.originalName)).toArray
 - }}
 - class FirstReportTypeParser extends ReportTypeParser {
 - override def sqlFor(criteria: Option[List[Condition]]): String = {
 - s"""
 - select ${generateSelectFields}
 - from ${AccountDetailTable} a
 - left join ${AccountDebtDetailTable} b
 - left join ${AoucherJournalTable} c
 - on a.${AccountDetailTableSchema.Account.toString.codeName(AccountDetailTable)} = b.${AccountDebtDetailTableSchema.Account.toString.codeName(AccountDebtDetailTable)}
 - and a.${AccountDetailTableSchema.CustomerNo.toString.codeName(AccountDetailTable)} = c.${AoucherJournalTableSchema.CustomerNo.toString.codeName(AoucherJournalTable)}
 - where ${generateWhereClause}$
 - ${generateOrderBy}
 - """
 - }
 - override private[parc] def predefinedTables: List[TableName] = ...
 - override private[parc] def predefinedFields: List[TableField] = ...
 - private[parc] def generateSelectFields: String = {
 - if (predefinedFields.isEmpty) "*" else predefinedFields.map(field => field.fullName).mkString(",")
 - }
 - private[parc] def generateWhereCluase(conditionsOpt: Option[List[Condition]]): String = {
 - def evaluate(condition: Condition): String = {
 - val aliasName = aliasNameFor(condition.originalTableName)
 - val codeName = fetchField(condition.fieldId)
 - .map(_.codeName)
 - .getOrElse(throw ResourceNotExistException(s"can't find the field with id ${condition.fieldId}"))
 - val values = condition.operator.toLowerCase() match {
 - case "between" => {
 - require(condition.values.size == 2, "the values of condition don't match between operator")
 - s"BETWEEN ${condition.values.head} AND ${condition.values.tail.head}"
 - }
 - case _ => throw BadRequestException(s"can't support operator ${condition.operator}")
 - }
 - s"${aliasName}.${codeName} ${values}"
 - }
 - conditionsOpt match {
 - case Some(conditions) if !conditions.isEmpty => s"where ${conditions.map(c => evaluate(c)).mkString(" and ")}"
 - case _ => ""
 - }
 - }}
 
在ReportTypeParser中,我實(shí)現(xiàn)了部分可以重用的邏輯,例如generateHeaders()等方法。但是,還有部分實(shí)現(xiàn)邏輯放在了具體的實(shí)現(xiàn)類FirtReportTypeParser中,例如最主要的sqlFor方法,以及該方法調(diào)用的諸多方法,如generateSelectFields、generateWhereCluase等。
在這其中,TableName提供了表名與數(shù)據(jù)集名、別名之間的映射關(guān)系,而TableField則提供了TableName與Field之間的映射關(guān)系:
- case class TableName(originalName: String,
 - metaName: String,
 - aliasName: String,
 - generatedName: String = "")
 - case class TableField(table: TableName,
 - fieldName: String,
 - orderType: Option[OrderType] = None)
 
仔細(xì)觀察sqlFor方法的實(shí)現(xiàn),發(fā)現(xiàn)生成select的字段、生成Join的部分以及生成條件子句、排序子句都是有規(guī)律可循的。這個(gè)過程是在我不斷重構(gòu)的過程中慢慢浮現(xiàn)出來的。我不斷找到了這些相似的方法,例如generateSelectFields、generateWhereClause這些方法。它們之間的差異只在于一些與具體報(bào)表有關(guān)的元數(shù)據(jù)上,例如表名、字段名、字段名與表名的映射、表名與別名的映射。
我首先通過pull member up重構(gòu),將這兩個(gè)方法提升到ReportTypeParser中:
- trait ReportTypeParser extends ... {
 - private[parc] def generateSelectFields: String = ...
 - private[parc] def generateWhereCluase(conditionsOpt: Option[List[Condition]]): String
 
此外,還包括我尋找到共同規(guī)律的join部分:
- trait ReportTypeParser extends ... {
 - private[parc] def generateJoinKeys: String = {
 - def joinKey(tableField: TableField): String =
 - s"${aliasNameFor(tableField.tableName)}.${tableField.fieldName.codeName(mapping.tableName)}"
 - predefinedJoinKeys.map{
 - case (leftTable, rightTable) => s"${joinKey(leftTable)} = ${joinKey(rightTable)}"
 - }.mkString(" and ")
 - }}
 
現(xiàn)在sqlFor()方法就變成一個(gè)所有報(bào)表都通用的方法了,因此我也將它提升到ReportTypeParser中。
2. 元數(shù)據(jù)概念的浮現(xiàn)
我在最初定義諸如predefinedTables與predefinedFields等方法時(shí),還沒有清晰地認(rèn)識到所謂元數(shù)據(jù)(Metadata)的概念,然而這一系列重構(gòu)后,我發(fā)現(xiàn)定義在FirstReportParser子類中的方法,其核心職責(zé)就是提供SQL解析所需要的元數(shù)據(jù)內(nèi)容:
- class FirstReportTypeParser extends ReportTypeParser {
 - private[parc] def predefinedJoinKeys: List[(TableField, TableField)] = ...
 - override private[parc] def predefinedAliasNames: Map[TableName, AliasName] = ...
 - override private[parc] def predefinedCriteriaFields: List[TableField] = ...
 - override private[parc] def predefinedOrderByFields: List[TableField] = ...
 - override private[parc] def predefinedTables: List[TableName] = ...
 - override private[parc] def predefinedFields: List[TableFieldMapping] = ...
 - }
 
3. 以委派取代繼承
元數(shù)據(jù)的概念給了我啟發(fā)。針對報(bào)表的SQL語句解析,邏輯是完全相同的,不同之處僅在于解析的元數(shù)據(jù)而已。這就浮現(xiàn)出兩個(gè)不同的職責(zé):
- 提供元數(shù)據(jù)
 - 元數(shù)據(jù)解析
 
在變化方向上,引起這兩個(gè)職責(zé)發(fā)生變化的原因是完全不同的。不同的報(bào)表需要提供的元數(shù)據(jù)是不同的,而對于元數(shù)據(jù)的解析,則取決于Spark SQL的訪問方式(在后面我們會(huì)看到這種變化)。根據(jù)單一職責(zé)原則,我們需要將這兩個(gè)具有不同變化方向的職責(zé)分離,因此它們之間正確的依賴關(guān)系不應(yīng)該是繼承,而應(yīng)該是委派。
我首先引入了ReportMetadata,并將原來的FirstReportTypeParser更名為FirstReportMetadata,在實(shí)現(xiàn)了ReportMetadata的同時(shí),對相關(guān)元數(shù)據(jù)的方法進(jìn)行了重命名:
- trait ReportMetadata extends ParcConfiguration {
 - def joinKeys: List[(TableField, TableField)]
 - def tables: List[TableName]
 - def fields: List[TableField]
 - def criteriaFields: List[TableField]
 - def orderByFields: List[TableField]}trait FirstReportMetadata extends ReportMetadata
 
至于原有的ReportTypeParser則被更名為ReportMetadataParser。
4. 引入Cake Pattern
如果仍然沿用之前的繼承關(guān)系,我們可以根據(jù)reportType分別創(chuàng)建不同報(bào)表的Parser實(shí)例。但是現(xiàn)在,我們需要將具體的ReportMetadata實(shí)例傳給ReportMetadataParser。至于具體傳遞什么樣的ReportMetadata實(shí)例,則取決于reportType。
這事實(shí)上是一種依賴注入。在Scala中,實(shí)現(xiàn)依賴注入通常是通過self type實(shí)現(xiàn)所謂Cake Pattern:
- class ReportMetadataParser extends DataSetFetcher with ParcConfiguration {
 - self: ReportMetadata =>
 - def evaluateSql(criteria: Option[List[Condition]]): String = {
 - s"""
 - select ${evaluateSelectFields}
 - from ${evaluateJoinTables}
 - where ${evaluateJoinKeys}
 - ${evaluateCriteria(criteria)}
 - ${evaluateOrderBy}
 - """
 - }}
 
為了更清晰地表達(dá)解析的含義,我將相關(guān)方法都更名為以evaluate為前綴。通過self type,ReportMetadataParser可以訪問ReportMetadata的方法,至于具體是什么樣的實(shí)現(xiàn),則取決于創(chuàng)建ReportMetadataParser對象時(shí)傳遞的具體類型。
通過將Metadata從Parser中分離出來,實(shí)際上是差異化編程的體現(xiàn)。這是我們在建立繼承體系時(shí)需要注意的。我們要學(xué)會(huì)觀察差異的部分,然后僅僅將差異的部分剝離出來,然后為其進(jìn)行更通用的抽象,由此再針對實(shí)現(xiàn)上的差異去建立繼承體系,如分離出來的ReportMetadata。當(dāng)我們要實(shí)現(xiàn)其他報(bào)表時(shí),其實(shí)只需要定義ReportMetadata的實(shí)現(xiàn)類,提供不同的元數(shù)據(jù),就可以滿足要求。這就使得我們能夠有效地避免代碼的重復(fù),職責(zé)也更清晰。
5. 建立測試樁
引入Cake Pattern實(shí)現(xiàn)依賴注入還有利于我們編寫單元測試。例如在前面的實(shí)現(xiàn)中,我們通過Cake Pattern實(shí)際上注入了實(shí)現(xiàn)了DataSetFetcher的ReportMetadata類型。之所以需要實(shí)現(xiàn)DataSetFetcher,是因?yàn)槲蚁胪ㄟ^它訪問數(shù)據(jù)庫中的數(shù)據(jù)集相關(guān)元數(shù)據(jù)。但是,在測試時(shí)我只想驗(yàn)證sql解析的邏輯是否正確,并不希望真正去訪問數(shù)據(jù)庫。這時(shí),我們可以建立一個(gè)DataSetFetcher的測試樁。
- trait StubDataSetFetcher extends DataSetFetcher {
 - override def fetchField(dataSetId: ID, fieldName: String): Option[Field] = ...
 - override def fetchDataSetByName(dataSetName: String): Option[DataSetFetched] = ...
 - override def fetchDataSet(dataSetId: ID): Option[DataSetFetched] = ...
 - }
 
StubDataSetFetcher通過繼承DataSetFetcher重寫了三個(gè)本來要訪問數(shù)據(jù)庫的方法,直接返回了需要的對象。然后,我再將這個(gè)trait定義在測試類中,并將其注入到ReportMetadataParser中:
- class ReportMetadataParserSpec extends FlatSpec with ShouldMatchers {
 - it should "evaluate to sql for first report" in {
 - val parser = new ReportMetadataParser() with FirstReportMetadata with StubDataSetFetcher
 - val sql = parser.evaluateSql(None)
 - sql should be(expectedSql)
 - }
 - }
 
6. 引入表達(dá)式樹
針對第一個(gè)報(bào)表,我們還有一個(gè)問題沒有解決,就是能夠支持相對復(fù)雜的where子句。例如條件:
- extractDate(a.TransactionDate) < extractDate(b.DueDate) and b.LoanFlag = 'D'
 
不同的報(bào)表,可能會(huì)有不同的where子句。其中,extractDate函數(shù)是我自己定義的UDF。
前面提到的元數(shù)據(jù),主要都牽涉到表名、字段名,而這里的元數(shù)據(jù)是復(fù)雜的表達(dá)式。所以,我借鑒表達(dá)式樹的概念,建立了如下的表達(dá)式元數(shù)據(jù)結(jié)構(gòu):
- object ExpressionMetadata {
 - trait Expression {
 - def accept(parser: ExpressionParser): String = parser.evaluateExpression(this)
 - }
 - case class ConditionField(tableName:String, fieldName: String, funName: Option[String] = None) extends Expression
 - case class IntValue(value: Int) extends Expression
 - abstract class SingleExpression(expr: Expression) extends Expression {
 - override def accept(evaluate: Expression => String): String =
 - s"(${expr.accept(evaluate)} ${operator})"
 - def operator: String
 - }
 - case class IsNotNull(expr: Expression) extends SingleExpression(expr) {
 - override def operator: String = "is not null"
 - }
 - abstract class BinaryExpression(left: Expression, right: Expression) extends Expression {
 - override def accept(parser: ExpressionParser): String =
 - s"${left.accept(parser)} ${operator} ${right.accept(parser)}"
 - def operator: String
 - }
 - case class Equal(left: Expression, right: Expression) extends BinaryExpression(left, right) {
 - override def operator: String = "="
 - }
 - }
 
7. 利用模式匹配實(shí)現(xiàn)訪問者模式
一開始,我為各個(gè)Expression對象定義的其實(shí)是evaluate方法,而非現(xiàn)在的accept方法。我認(rèn)為各個(gè)Expression對象都是自我完備的對象,它所擁有的知識(數(shù)據(jù)或?qū)傩?使得它能夠自我實(shí)現(xiàn)解析,并利用類似合成模式的方式實(shí)現(xiàn)遞歸的解析。
然而在實(shí)現(xiàn)時(shí)我遇到了一個(gè)問題:在解析字段名時(shí),我們不能直接用字段名來組成where子句,因?yàn)樵谖覀儺a(chǎn)品的Parquet數(shù)據(jù)集中,字段的名字其實(shí)是系統(tǒng)自動(dòng)生成的。我們需要獲得:
- 該字段對應(yīng)的表的別名
 - 該字段名在數(shù)據(jù)集中真正存儲的名稱,即code_name,例如C01。
 
換言之,真正要生成的條件子句應(yīng)該形如:
- extractDate(a.c1) < extractDate(b.c1) and b.c2 = 'D'
 
然而,關(guān)于表名與別名的映射則是配置在ReportMetadata中,獲得別名與codeName的方法則被定義在ReportMetadataParser的內(nèi)部。如果將解析的實(shí)現(xiàn)邏輯放在Expression中,就需要依賴ReportMetadata與ReportMetadataParser。與之相比,我更傾向于將Expression傳給它們,讓它們完成對Expression的解析。換言之,Expression樹結(jié)構(gòu)只提供數(shù)據(jù),真正的解析職責(zé)則被委派給另外的對象,我將其定義為ExpressionParser:
- trait ExpressionParser {
 - def evaluateExpression(expression: Expression): String}
 
這種雙重委派與樹結(jié)構(gòu)的場景不正是訪問者模式最適宜的嗎?至于ExpressionParser的實(shí)現(xiàn),則可以交給ReportMetadataParser:
- class ReportMetadataParser extends DataSetFetcher with ParcConfiguration with ExpressionParser {override def evaluateExpression(expression: Expression): String = {
 - expression match {
 - case ConditionField(tableName, fieldName, funName) =>
 - val fullName = s"${table.aliasName}.${fieldName.codeName(table.originalName)}${orderType.getOrElse("")}"
 - funName match {
 - case Some(fun) => s"${funName}(${fullName})"
 - case None => fullName
 - case IntValue(v) => s"${v}"
 - case StringValue(v) => s"'${v}'"
 - }
 - }
 - def evaluateWhereClause: String = {
 - if (whereClause.isEmpty) return ""
 - val clause = whereClause.map(c => c.accept(this)).mkString(" and ")
 - s"where ${clause}"
 - }}
 
這里的evaluateExpression方法相當(dāng)于Visitor模式的visit方法。與傳統(tǒng)的Visitor模式不同,我不需要定義多個(gè)visit方法的重載,而是直接運(yùn)用Scala的模式匹配。
evaluateWhereClause方法會(huì)對Expression的元數(shù)據(jù)whereClause進(jìn)行解析,真正的實(shí)現(xiàn)是對每個(gè)Expression對象,執(zhí)行accept(this)方法,在其內(nèi)部又委派給this即ReportMetadataParser的evaluateExpression方法。
代碼中的whereClause是新增加的Metadata,具體的實(shí)現(xiàn)放到了FirstReportMetadata中:
- override def whereClause: List[Expression] = {
 - List(
 - LessThan(
 - ConditionField(AccountDetailTable, AccountDetailTableSchema.TransactionDate.toString, Some("extractDate")),
 - ConditionField(AoucherJournalTable, AoucherJournalTableSchema.DueDate.toString, Some("extractDate"))
 - ),
 - Equal(
 - ConditionField(AccountDetailTable, AccountDetailTableSchema.LoanFlag.toString),
 - StringValue("D")
 - )
 - )
 - }
 
8. 用函數(shù)取代trait定義
在Scala中,我們完全可以用函數(shù)來替代trait:
- trait Expression {
 - def accept(evaluate: Expression => String): String = evaluate(this)
 - }
 - class ReportMetadataParser extends DataSetFetcher with ParcConfiguration {
 - self: ReportMetadata with DataSetFetcher =>
 - def evaluateExpr(expression: Expression): String = {
 - expression match {
 - case ConditionField(tableName, fieldName) =>
 - s"${aliasNameFor(tableName)}.${fieldName.codeName(tableName)}"
 - case IntValue(v) => s"${v}"
 - case StringValue(v) => s"'${v}'"
 - }
 - }
 - def evaluateWhereClause: String = {
 - if (whereClause.isEmpty) return " true "
 - whereClause.map(c => c.accept(evaluateExpr)).mkString(" and ")
 - }}
 
9. 演進(jìn)過程的提交記錄
這個(gè)設(shè)計(jì)的過程并非事先明確進(jìn)行針對性的設(shè)計(jì),而是隨著功能的逐步實(shí)現(xiàn),伴隨著對代碼的重構(gòu)而逐漸浮現(xiàn)出來的。
整個(gè)過程的提交記錄如下圖所示(從上至下由最近到最遠(yuǎn)):
四、當(dāng)變化發(fā)生
通過前面一系列的設(shè)計(jì)演進(jìn),代碼結(jié)構(gòu)與質(zhì)量已經(jīng)得到了相當(dāng)程度的改進(jìn)與提高。關(guān)鍵是這樣的設(shè)計(jì)演進(jìn)是有價(jià)值回報(bào)的。在走出分離元數(shù)據(jù)關(guān)鍵步驟之后,設(shè)計(jì)就向著好的方向在發(fā)展。
在實(shí)現(xiàn)了第一張報(bào)表之后,后面四張報(bào)表的開發(fā)就變得非常容易了,只需要為這四張報(bào)表提供必需的元數(shù)據(jù)信息即可。
令人欣慰的是,這個(gè)設(shè)計(jì)還經(jīng)受了解決方案變化與需求變化的考驗(yàn)。
1. 解決方案變化
在前面的實(shí)現(xiàn)中,我采用了Spark SQL的SQL方式執(zhí)行查詢。查詢時(shí)通過join關(guān)聯(lián)了多張表。在生產(chǎn)環(huán)境上部署后,發(fā)現(xiàn)查詢數(shù)據(jù)集的性能不盡如人意,必須改進(jìn)性能(關(guān)于性能的調(diào)優(yōu),則是另一個(gè)故事了,我會(huì)在另外的文章中講解)。由于join的表有大小表的區(qū)別,改進(jìn)性能的方式是引入broadcast。雖然可以通過設(shè)置spark.sql.autoBroadcastJoinThreshold來告知Spark滿足條件時(shí)啟用broadcast,但更容易控制的方法是調(diào)用DataFrame提供的API。
于是,實(shí)現(xiàn)方案就需要進(jìn)行調(diào)整:解析SQL的過程 ---> 組裝DataFrame API的過程
從代碼看,從原來的:
- def evaluateSql(criteria: Option[List[Condition]]): String = {
 - logging {
 - s"""
 - select ${evaluateSelectFields}
 - from ${evaluateJoinTables}
 - on ${evaluateJoinKeys}
 - where ${evaluateWhereClause}${evaluateCriteria(criteria)}
 - ${evaluateOrderBy}
 - """
 - }
 - }
 
變?yōu)榻馕龈鱾€(gè)API的參數(shù),然后在加載DataFrame的地方調(diào)用API:
- val dataFrames = tableNames.map { table =>
 - load(table.generatedName).as(table.aliasName)
 - }
 - sqlContext.udf.register("extractDate", new ExtractDate)
 - val (joinedDF, _) = dataFrames.zipWithIndex.reduce {
 - (dfToIndex, accumulatorToIndex) =>
 - val (df, index) = dfToIndex
 - val (acc, _) = accumulatorToIndex
 - (df.join(broadcast(acc), keyColumnPairs(index)._1 === keyColumnPairs(index)._2), index)
 - }
 - joinedDF.where(queryConditions)
 - .orderBy(orderColumns: _*)
 - .select(selectColumns: _*)
 
解析方式雖然有變化,但需要的元數(shù)據(jù)還是基本相似,差別在于需要將之前我自己定義的字段類型轉(zhuǎn)換為Column類型。我們僅僅只需要修改 ReportMetadataParser類,在原有基礎(chǔ)上,增加部分獨(dú)有的元數(shù)據(jù)解析功能:
- class ReportMetadataParser extends ParcConfiguration with MortLogger {
 - def evaluateKeyPairs: List[(Column, Column)] = {
 - joinKeys.map {
 - case (leftKey, rightKey) => (leftKey.toColumn, rightKey.toColumn)
 - }
 - }
 - def evaluateSelectColumns: List[Column] = {
 - fields.map(tf => tf.toColumn)
 - }
 - def evaluateOrderColumns: List[Column] = {
 - orderByFields.map(f => f.toColumn)
 - }
 - }
 
2. 需求變化
我們的另一個(gè)客戶同樣需要類似的需求,區(qū)別在于他們的數(shù)據(jù)治理更好,我們只需要對已經(jīng)治理好的視圖數(shù)據(jù)執(zhí)行查詢即可,而無需跨表Join。在對現(xiàn)有代碼的包結(jié)構(gòu)做出調(diào)整,并定義了更為通用的Spark SQL查詢方法后,要做的工作其實(shí)就是定義對應(yīng)報(bào)表的元數(shù)據(jù)罷了。
僅僅花費(fèi)了1天半的時(shí)間,新客戶新項(xiàng)目的報(bào)表后端開發(fā)工作就完成了。要知道在如此短的開發(fā)周期內(nèi),大部分時(shí)間其實(shí)還是消耗在重構(gòu)工作上,包括重新調(diào)整現(xiàn)有代碼的包結(jié)構(gòu),提取重用代碼。現(xiàn)在,我可以悠閑一點(diǎn),喝喝茶,看看閑書,然后再重裝待發(fā),迎接下一個(gè)完全不同的新項(xiàng)目。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者】
















 
 
 







 
 
 
 