寫在前面
本篇文章主要是講 SKU 商品規格組合的 問題、解決思路及算法優化。 最后 將提供一個SKU算法的通配方案 - SKUDataFilter
本篇文章分析較為詳細,針對于對SKU問題不甚了解的童鞋。
不想聽我瞎BB的,這邊是干貨地址
文章最后是 使用說明
更新日志
2019.04.12
-
加入是否默認選中第一組SKU的控制
此處是選中第一組SKU, 并不一定包含第一個屬性// needDefaultValue _filter.needDefaultValue = YES; [self.collectionView reloadData]; //更新UI顯示 [self action_complete:nil]; //更新結果查詢
2018.07.11 ~ cocoapods version 1.0.1
-
支持cocopods 導入
pod 'SKUDataFilter' -
升級數據防崩潰過濾,即使sku-condition完全對不上號,也不會閃退了。(針對某些極端測試人員)
2018.06.21 -
- 最近收到很多因為部分sku信息不完整,導致崩潰的反饋。所以新增了sku-condition的檢測,過濾并提示了不完整的condition。已更新
2017.12.16 -
- 由于之前的疏忽,在更新算法的時候,漏了一個點,導致一個非常嚴重的bug,感謝簡書網友@畢小強指出,已更新
關于SKU
維基百科:
最小庫存管理單元(Stock Keeping Unit, SKU)是一個會計學名詞,定義為庫存管理中的最小可用單元。
最小庫存管理單元就是“單品” 最小庫存單元是指包含特定的自然屬性與社會屬性的商品種類,在零售連鎖門店管理中通常稱為“單品”。對于一種商品而言,當他的品牌、型號、配置、花色、容量、生產日期、保質期、用途、價格、產地等屬性與其他商品存在不同時,就是一個不同的最小存貨單元。
通俗來講,一個SKU 就是商品在規格上的一種組合,比如說,一件衣服 有紅色 M號的 也有藍色 L號的 ,不同的組合就是不同的SKU
下圖DEMO演示效果圖

問題與思路
我們所說的SKU 組合算法,就是對商品規格組合的一種篩選和過濾。即 根據已選中的一個或多個屬性過濾出 剩余屬性的 可選性,以及選完所有屬性之后對應的 結果(庫存、價格等)
這里的問題就有兩個
- 根據已選中的一個或多個屬性過濾出 剩余屬性的 可選性
- 根據選中的所有屬性查詢對應的結果(庫存、價格等)
第二個問題較簡單,只需要遍歷一遍SKU,找到對應的結果即可,重點在第一個
簡單舉個例子
**商品規格 :
款式 : F M
顏色 : R G B
尺寸 : L X S
SKU:
M,G,X - 66元,10件
F,G,S - 88元,12件
F,R,X - 99元,15件
我們把 一組滿足條件的屬性 叫做條件式 ,那么這里就有三個條件式
用個圖來表示他們之間的關系 (紅線為F-G-S)

這里的屬性的狀態只有兩種 可選和不可選。(已選是屬于可選)
那么B、L自始至終就為不可選狀態
當我們選中某個屬性時,比如G -- 那么對應的
可選擇屬性即為 :
G本身;
兄弟節點(同類可選屬性可切換) R(B已淘汰);
對應條件式中的其他節點 M、X、F、S;
乍一看,除了條件式外的都可選,這是因為故意弄成這個條件式以便于后面的講解。
實際上我們是通過遍歷他的兄弟屬性, 遍歷他所在的條件式,拿出對應的屬性。在多個條件式中會有重復的屬性,為了過濾重復的值可以利用集合來添加保存的(NSSet,NSMutableSet)
當我們選中多個屬性, 比如 F、 R, 由于已選屬性之間的相互牽制、這里情況就要復雜的多了
根據上面的分析,我們通常都會想到,遍歷各自的兄弟節點,以及條件式,最后各自所取的屬性值 去一個交集
(ps1:有的小伙伴可能看不懂,最終可選屬性,最傻的辦法就是你把可選屬性帶入條件式里面滿足條件即可,兄弟屬性在替換之后滿足條件式也是可選,比如選中R、他會替換原先的G,也滿足已選中屬性X,即為可選)
(ps2:排列順序為:本身、可選兄弟屬性、條件式)
F-可選:F、M、R、X、G、S
R-可選:R、G、F、X
交集:F、R、G、X
手動驗證,完全OK
But這種方法有漏洞
選擇 G、X
G-可選:G、R、F、S、M、X
X-可選:X、S、F、R、M、G
交集:G、X、R、S、R、F、M
手動驗證,錯誤 - F應該為不可選
手動原因:F既在G的條件式F-G-S中,又在X的條件式F-R-X中,但是卻不同時滿足G、X
這里首先要搞明白 問題絕對不會出現在已選屬性的兄弟屬性上,因為兄弟節點,在任何一個兄弟屬性存在的條件式中 其他兄弟屬性都不會出現,有F的條件式就不會有M。所以問題還是在條件式中。當有多個屬性被選中時,判斷一個非可選屬性的兄弟屬性是否可選,必須要滿足所有可選屬性的條件式。那么整體結論即判定某個屬性的可選性:該屬性要么同時滿足所有已選屬性的條件式,要么和已選中的某個屬性是兄弟屬性
算法優化
基于上面的思路,再來說一下算法的優化
-
降低已選屬性的遍歷
將上訴理論應用到實際代碼中,一般是這樣的,再求可選屬性集合時:每次一個新的屬性操作(選中、取消、切換),都會根據上訴結論 分別為每一個已選屬性的篩選出對應的 可選屬性,然后在做交集。
這樣的話,每次一個新的屬性操作,都可能會把的已選屬性重復查詢一遍。
優化方案構想-每次新的屬性操作,只篩選當前屬性的可選屬性,然后在已選屬性的基礎上進行增刪操作。
看到構想,感覺也不是很難,下面是實際情況——
實際操作分為三種情況:
1、選中新屬性 2、切換兄弟屬性 3、取消已選中屬性
第一種情況,和我們前面的思路吻合,只需要將篩選出新屬性對應的可選屬性集合,然后與當前的 可選集合 求得 交集即可
后面兩種情況,都含有一個取消操作(切換兄弟屬性、需要取消上一個兄弟屬性),取消操作,意味著,你要把該屬性所過濾掉的可選屬性,還回來,也就是恢復取交集前的 原集合。那問題的關鍵即為如何找到這些被該屬性過濾掉的可選屬性集合或是直接恢復原集合。
恢復:可以通過找到所有的原可選集合,并記錄下來,然后根據取消屬性的匹配原集合恢復(這里不能單純的記錄選中操作的可選集合,因為取消的順序不一樣)
查找過濾掉的可選集合:操作等同于重新計算可選集合
以上兩種方案都不可行。這里就不多做贅述,實際操作起來,遍歷查詢的次數更多了,不僅達不到優化的效果,還增加了算法的復雜程度(這里如果小伙伴有更好的想法,歡迎討論)。那么最終結論是:在對屬性有新操作時,只有新增屬性可以基于當前可選屬性集合過濾,其他情況需要重新計算
-
優化條件式
如果說上一個優化方案只是在瞎扯蛋的話,那這里就是整個優化所在的關鍵了,同時也是SKUDataFilter的核心
認真看了整篇文章就會發現,整個算法思路的核心,在于條件式,不管是查詢結果,還是查詢可選屬性集合,實際上都是依賴于條件式的,我們在查詢某個屬性時候可選,實際上是要遍歷他所在的條件式列表,這個列表又要求我們去遍歷所有的條件式,判斷這個屬性是否在條件式中,拿到列表之后,獲取非兄弟屬性又要遍歷這個屬性,是否同時滿足所有已選屬性的條件式。那么我們整個算法循環次數最多的地方便是判斷某個屬性是否存在于某個條件式中
**商品規格 :
為規格屬性加一個坐標,記錄他們的位置
0 1 2
0 F M
1 R G B
2 L X S
SKU: 用下標表示條件式
M,G,X - 66元,10件 --- (1,1,1)
F,G,S - 88元,12件 --- (0,1,2)
F,R,X - 99元,15件 --- (0,0,1)
在上一個例子中,為每個屬性加一個坐標,如L表示為(0, 2)
條件式中用坐標的某一部分表示
這樣一來
判斷某個屬性是否存在于某個條件式:
正常的操作是遍歷條件式中的屬性,分別和該屬性做判斷(containsObject方法 本質上也是在做遍歷) 。
而這里只需要一次判斷就夠了 ,設該屬性的坐標為(x,y)判斷條件式里的第y個值是否等于x即可(這里的判斷取決于條件式存入的是x、還是y)
如 判斷M(1,0) 是否在F-G-S(0,1,2)條件式中 即條件式的第0個值是否等于1就OK了(程序猿都是從0開始數的)
比如說,總共有5個條件式,每個條件式中有5個屬性,你要找出某個滿足某個屬性的所有條件式
如果你不去中斷遍歷,就要判斷25次,這種方式只需要判定5次就夠了,所以它的優化性實際上是非常高的。
實際上,真正神奇之處就在于這樣的下標條件式可以清楚的 知道他所擁有的 任何一個屬性 的坐標,進而知道屬性的值
解決方案-SKUDataFilter
SKUDataFilter 正是基于以上分析和算法優化實現的,使用NSIndexPath記錄每個屬性的坐標,更加直觀。表示為 第section 種屬性類下面的 第item個 屬性 (從0計數)
條件式下標(conditionIndexs)中記錄的屬性indexPath的 item
// 判斷屬性是否存在于條件式中
conditionIndexs[indexPath.section] == indexPath.row
數據通配
conditionIndexs 和indexPath的結合 為SKUDataFilter 不僅算法上取得了優勢,同時也在 數據通配 上起了莫大的作用
不同的后臺,不同的需求,返回的數據結構都不一樣。
然而SKUDataFilter真正關心的是屬性的坐標,而不是屬性本身的的值,那么不管你從后臺獲取的數據結構是怎樣的,也不管你是如何解析的。當然,你也不需要去關心坐標和條件式下標等等亂七八糟的。你需要做的只是把對應的數據放入對應的代理方法里面去就行了,不管數據是model,屬性ID、字典還是其他的。
使用說明
使用可以參考SKUDataFilterDemo
SKUDataFilter最終直接反映的是屬性的indexPath, 如果你的屬性在UI顯示上使用UICollectionView實現,那么indexPath是一一對應的,如果用的循環創建,找到對應的行和列即可。
1、初始化Filter 并設置代理
- (instancetype)initWithDataSource:(id<ORSKUDataFilterDataSource>)dataSource;
//當數據更新的時候 重新加載數據
- (void)reloadData;
2、通過代理方法 ,將數據傳給Filter
以下方法都必需實現,分別告訴Filter,屬性種類個數、每個種類的所有屬性(數組),條件式個數、每個條件式包含的所有屬性、以及每個條件式對應的結果(可以參考本文案例)
//屬性種類個數
- (NSInteger)numberOfSectionsForPropertiesInFilter:(ORSKUDataFilter *)filter;
/*
* 每個種類所有的的屬性值
* 這里不關心具體的值,可以是屬性ID, 屬性名,字典、model
*/
- (NSArray *)filter:(ORSKUDataFilter *)filter propertiesInSection:(NSInteger)section;
//滿足條件 的 個數
- (NSInteger)numberOfConditionsInFilter:(ORSKUDataFilter *)filter;
/*
* 對應的條件式
* 這里條件式的屬性值,需要和filter:propertiesInSection里面的數據 類型保持一致
*/
- (NSArray *)filter:(ORSKUDataFilter *)filter conditionForRow:(NSInteger)row;
//條件式 對應的 結果數據(庫存、價格等)
- (id)filter:(ORSKUDataFilter *)filter resultOfConditionForRow:(NSInteger)row;
3、點擊某個屬性的時候 把對應屬性的indexPath傳給Filter
- (void)didSelectedPropertyWithIndexPath:(NSIndexPath *)indexPath;
4、查詢結果(與代理方法resultOfConditionForRow:對應)-條件不完整會返回nil
@property (nonatomic, strong, readonly) id currentResult;
5、可選屬性集合列表、已選屬性坐標列表
//當前 選中的屬性indexPath
@property (nonatomic, strong, readonly) NSArray <NSIndexPath *> *selectedIndexPaths;
//當前 可選的屬性indexPath
@property (nonatomic, strong, readonly) NSArray <NSIndexPath *> *availableIndexPaths;
使用注意
1、雖然SKUDataFilter不關心具體的值,但是條件式本質是由屬性組成,故代理方法filter:propertiesInSection:和方法filter:conditionForRow:數據類型應該保持一致
2、因為SKUDataFilter關心的是屬性的坐標,那么在代理方法傳值的時候,代理方法filter:propertiesInSection:和方法filter:conditionForRow:各自的數據順序要保持一致 并且兩個方法的數據也要對應
如本文案例條件式是從上往下(M,G,X),傳過去的 屬性值 也都是從左到右(F、M)-各自保持一致。 同時
條件式為從上到下,那么propertiesInSection: 也應該是從上到下,先是(F、M)最后是(L、X、S)
實際項目中,這兩種情況發生的概率都非常小,因為 第一數據統一返回統一解析,格式99%都是一樣。第二數據是從服務器返回,服務器的數據要進行篩選和過濾,順序也不能弄錯,一旦錯誤,首先服務器就會出問題
作者:OrangeAL
鏈接:https://www.jianshu.com/p/295737e2ac77
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。
浙公網安備 33010602011771號