什么開閉原則污尉?
開閉原則(Open Closed Principle)是Java世界里最基礎的設計原則,它指導我們?nèi)绾谓⒁粋€穩(wěn)定的、靈活的系統(tǒng)斥滤。
設計模式之六大原則——開閉原則(OCP):一個軟件實體如類修然、模塊和函數(shù)應該對擴展開放非区,對修改關閉郭膛。
例子
這是一個實戰(zhàn)中的項目麸拄,需求目標很簡單:提供統(tǒng)一內(nèi)容搜索能力 惜索,包括 文檔特笋,知識,視頻巾兆×晕铮可以通過目錄樹切換查看該庫 的 文檔詳情/知識列表/視頻列表。
搜索頁面比較簡單角塑,這里就不講了蔫磨。重點看詳情,列表圃伶,目錄樹/文檔樹 設計堤如。
概念
- 庫:每種內(nèi)容類型都歸屬于一個庫蒲列,比如,有文檔庫A煤惩,文檔庫B....
- 內(nèi)容類型:目前搜索范圍 是
- 文檔:下面簡稱Doc
- 知識:下面簡稱Faq
- 視頻:下面簡稱Video
- 類目:不是所有的內(nèi)容類型都有類目樹嫉嘀。在這個例子里面,F(xiàn)aq和Video有目錄節(jié)點魄揉,即每個目錄節(jié)點對應一組Faq/Video剪侮。但 Doc 類型是通過每一篇文檔指定parent屬性將文檔上下級關系串聯(lián)起來,所以洛退,它的類目樹就是文檔樹瓣俯。
類似的交互圖
用例圖
第一步:梳理異同
動手之前,先擼一擼基于內(nèi)容類型兵怯,交互的相同點和不同點彩匕。
- 相同點:
1.1 目錄樹/文檔樹 展示UI完成一樣,都是標準<Tree />
組件
1.2 目錄樹點擊只會觸發(fā)兩種方式:展示【列表】 或者 【詳情】
1.3 文案型的詳情都是富文本展示 - 不同點
1.1 列表頁面展示UI基于內(nèi)容類型不同而不同
1.2 詳情頁展示UI基于內(nèi)容類型不同而不同媒区,但是部分可歸類
最后考慮下拓展性驼仪。假設,以后新增了 【案例】這種內(nèi)容類型袜漩,列表可能用<Table />
組件绪爸,詳情頁可能是JSON式格式化數(shù)據(jù)渲染,那么宙攻,如何最小成本支持該類型呢奠货?
這就是該實戰(zhàn)需要解決的問題:對擴展開放,對修改關閉
第二步:按照“面條”思維做第一版本
先不要急著一蹴而就座掘,可以流程化的做一個簡單版本递惋,注意,此時不要將重點放在UI上(別急著畫樣式)溢陪,搭建框架更重要萍虽。
第一版文件結構可能如下:
++ /pages
++++++/List // 列表頁
+++++++++/index.tsx
+++++++++/Faq.tsx // Faq列表組件
+++++++++/Doc.tsx // Doc列表組件
+++++++++/Video.tsx // Video列表組件
++++++/Detail // 詳情頁
+++++++++/index.tsx
+++++++++/Faq.tsx // Faq列表組件
+++++++++/Doc.tsx // Doc列表組件
+++++++++/Video.tsx // Video列表組件
++ /components
++++++/CategoryTree // 目錄樹組件
++++++/RichHtml // 富文本渲染組件
...
看起來還不錯哦,只要在List & Detail/index.tsx
和 CategoryTree
代碼里面里面判斷下內(nèi)容類型形真,就可以愉快的加載不同的內(nèi)容組件了贩挣。
export enum ContentTypes {
FAQ = 'Faq',
DOC = 'Doc',
VIDEO = 'Video',
}
想一想,這個方案的問題在哪里没酣?
如果新增了一個【案例】case類型,需要修改多少地方卵迂?
- 新增兩個case.tsx組件裕便,分別為列表和詳情
- 修改兩個入口文件
index.tsx
,新增case類型 - 修改
CategoryTree
組件见咒,新增新類型點擊事件
可以看出來偿衰,第1點是必須要做的,而其他修改比較散亂。有沒有什么更好的方案呢下翎?
第三部:抽象缤言,封裝
詳情和列表的主頁面需要關系類型內(nèi)容嗎?可以不需要视事!
先看下新版的列表主頁代碼胆萧。
import React, { FC } from 'react'
import { useParams } from 'react-router-dom'
import CategoryTree from '@/components/CategoryTree'
import { isCorrectType, getTreeLink } from '@/components/ContentComp'
import TwoColsLayout from '@/components/TwoColsLayout'
import ContentList from '@/components/ContentComp/List'
type RouteParams = {
contentType: string
libraryCode: string
cateCode: string
}
export type DetailListParams = {
contentType: string
data: Record<string, any>
}
/**
* 列表頁面 /list/[contentType]/[libraryCode]/[cateCode]
*/
const List: FC = () => {
const { contentType, libraryCode, cateCode } = useParams<RouteParams>()
const isCurrentList = isCorrectType(contentType)
return (
<TwoColsLayout
isShow={isCurrentList}
leftComponent={
<CategoryTree
contentType={contentType}
libraryCode={libraryCode}
libraryCode={libraryCode}
currentCategoryCode={cateCode}
getTreeLink={getTreeLink(contentType)}
/>
}
rightComponent={
<ContentList
contentType={contentType}
libraryCode={libraryCode}
cateCode={cateCode}
/>
}
/>
)
}
export default List
其中,最重要的就是 @/components/ContentComp/List
組件 和 @/components/ContentComp
提供的 { isCorrectType, getTreeLink }
函數(shù)俐东。
一探究竟吧跌穗!
// @/components/ContentComp/List 組件
import React, { useState, useEffect } from 'react'
import ListFooterHandler, { DEFAULT_PAGE_SIZE } from '@/components/ListFooter'
import { ContentTypes } from '@/utils/const'
import { getContentList } from '@/services/index'
import FaqList from './Faq/List'
import VideoList from './Video/List'
export const ContentListConfig = {
[ContentTypes.FAQ]: FaqList,
[ContentTypes.VIDEO]: VideoList,
}
/**
* 因為列表數(shù)據(jù)只有List組件使用,所以虏辫,List 組件自行獲取數(shù)據(jù)且渲染蚌吸。
*
* @param { contentType, libraryCode, cateCode }
* @returns
*/
const ContentList = ({ contentType, libraryCode, cateCode }) => {
const [listData, setListData] = useState({
datas: [],
totalCount: 0,
})
const [searchParam, setSearchParam] = useState({
contentType,
libraryCode,
cateCode,
offset: 0,
limit: DEFAULT_PAGE_SIZE,
})
useEffect(() => {
console.log('get content list!')
const newParams = { ...searchParam, contentType, libraryCode, cateCode }
const result = getContentList(newParams)
setListData(result)
setSearchParam(newParams)
}, [contentType, libraryCode, cateCode])
const ListContent = ContentListConfig[contentType]
return (
<ListContent
data={listData}
footerConfig={ListFooterHandler.getConfig({
routerChange: (offset) => setSearchParam({ ...searchParam, offset }),
total: listData.totalCount,
current: Number(searchParam.offset) / DEFAULT_PAGE_SIZE + 1,
})}
/>
)
}
export default ContentList
可以看到“可變”配置了,
export const ContentListConfig = {
[ContentTypes.FAQ]: FaqList,
[ContentTypes.VIDEO]: VideoList,
}
那可變部分的接口入?yún)⑹鞘裁茨仄鲎咳缦拢?/p>
<ListContent
data={...}
footerConfig={...}
/>
遵循接口標準羹唠,再看一下Faq列表組件如何實現(xiàn)功能的:
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { List } from 'antd'
import { ListParams } from '../const'
import { getDetailUrl } from '@/utils/url'
import EmptyContent from '@/components/EmptyContent'
import { ContentTypes } from '@/utils/const'
import styles from './index.less'
const Faq: FC<ListParams> = ({ data = { datas: [], totalCount: 0 }, footerConfig = {} }) => {
const { totalCount, datas } = data
return (
<>
{totalCount != 0 ? (
<List
className={styles.faqList}
itemLayout="horizontal"
dataSource={datas}
split={false}
{...footerConfig}
renderItem={(item: any) => {
const { title, libraryCode, contentCode } = item as any
const href = getDetailUrl({
contentType: ContentTypes.FAQ,
libraryCode,
contentCode,
lang: 'zh',
})
return (
<Link to={href}>
<div className={styles.listTitle}>{title}</div>
</Link>
)
}}
/>
) : (
<EmptyContent />
)}
</>
)
}
export default Faq
UI組件部分解決了,那<Tree />
事件點擊如何根據(jù)不同內(nèi)容類型而操作不同呢娄昆?探探 @/components/ContentComp
提供的 { isCorrectType, getTreeLink }
函數(shù)吧佩微。
import { ContentTypesConfig } from '@/utils/const'
import { getDetailUrl, getListUrl } from '@/utils/url'
import { ContentListConfig } from './List'
import { ContentConfig } from './Detail'
const types = Object.keys(ContentTypesConfig)
/**
* 判斷是否支持該內(nèi)容類型
* @param type
* @returns
*/
export const isCorrectType = (type) => {
return types.includes(type)
}
/**
* 1. 如果支持List,展示列表頁面稿黄;
* 2. 不滿足條件1喊衫,且支持詳情頁面,展示詳情頁面杆怕;
* 3. 條件1和2都不支持族购,什么都不做;
* @param type
* @returns 返回跳轉url相對路徑地址
*/
export const getTreeLink = (type) => {
// ContentListConfig 哪里定義的陵珍,還記得嗎寝杖?往上翻翻就找到了 :)
if (ContentListConfig[type]) {
return ({ libraryCode, categoryCode }) => {
return getListUrl({ contentType: type, libraryCode, cateCode: categoryCode })
}
} else if (ContentConfig[type]) {
return ({ libraryCode, categoryCode }) => {
return getDetailUrl({
contentType: type,
libraryCode,
contentCode: categoryCode,
lang: 'zh',
})
}
}
}
整個可變部分的封裝結構如下圖:
回到之前的問題,“如果新增了一個【案例】case類型互纯,需要修改多少地方瑟幕?”
- 新增兩個case.tsx組件,分別為列表和詳情
- 在
@/components/ContentComp/List
或@/components/ContentComp/Detail
里面配置新類型留潦,如下:
export const ContentListConfig = {
[ContentTypes.FAQ]: FaqList,
[ContentTypes.VIDEO]: VideoList,
[ContentTypes.CASE]: CaseList,
}
如果Case和Doc類似只盹,沒有列表頁面,那更簡單了兔院,只要在@/components/ContentComp/Detail
里新增配給即可殖卑。
結論
多看看設計模式,還是挺香的坊萝。