本章探討 Unix 檔案搜尋系統的設計。目標是設計一組類別,抽象化檔案搜尋系統中的關鍵實體(目錄、檔案、過濾條件),建構出清晰、可運作且具擴展性的結構。

需求蒐集#
題目情境#
想像你是一位開發者,想在 Unix 系統的深層目錄中找特定檔案——例如某使用者擁有的、或符合某 pattern 的文字檔。你執行搜尋指令並指定條件,系統快速回傳符合的檔案。背景中,它遞迴遍歷目錄、評估檔案屬性並套用過濾條件。請設計這樣的系統。
需求釐清對話#
Candidate:find 指令以哪些屬性搜尋?
Interviewer:依 size、file type、filename、owner 等條件。
Candidate:需要處理目錄嗎?
Interviewer:是的,目錄也視為一種檔案,只是 file type 不同。
Candidate:支援哪些比較類型?
Interviewer:依屬性類型而定。字串支援 equals 與 regex match;數值支援 greater than、equals、less than。
Candidate:能否在同一屬性上組合多重條件?
Interviewer:可以,使用 and、or、not 邏輯運算組合。
Candidate:所以是設計一個從目錄遞迴搜尋並回傳符合條件檔案的系統?
Interviewer:是的。
具體範例#
- 簡單搜尋:在
/內遞迴尋找size > 10的檔案 - 複雜搜尋:
((size > 10 and size < 1000 and owner = "alice") or (size > 1000 and !(filename matches /prefix.*/)))
需求整理#
功能性需求#
- 可依 size、type、filename、owner 等屬性搜尋檔案
- 不同屬性支援不同比較類型(字串:equals/regex match;數值:greater than/equals/less than)
- 可用 and、or、not 邏輯運算組合多重條件
- 支援目錄內的遞迴搜尋
- 搜尋條件可同時套用於檔案與目錄
非功能性需求#
- 可擴展性:高效處理含數百萬檔案的大型目錄樹
- 可擴充性:支援新增屬性(如修改時間)與比較運算子,而不更動既有遍歷或過濾邏輯
- 關注點分離:遍歷邏輯與過濾邏輯需保持分離
辨識核心物件#
- FileSearch:管理搜尋流程的中心實體,作為應用邏輯入口。從某個 File(目錄)遞迴遍歷檔案系統,並依 FileSearchCriteria 回傳符合者
- File:模擬檔案系統中的檔案或目錄,儲存大小、類型、檔名、擁有者等屬性,並支援階層結構
Design Choice:File 物件以單一實體代表檔案與目錄,讓 FileSearch 能進行一致的遍歷與評估。這呼應 Unix「萬物皆檔案(everything is a file)」的設計哲學。
- FileSearchCriteria:封裝搜尋條件,並透過委派 Predicate 來判斷檔案是否符合。將執行邏輯(FileSearch)與條件評估邏輯(Predicate)解耦
- Predicate:定義評估檔案是否符合條件的契約介面,支援簡單檢查與複合條件
- SimplePredicate:實作 Predicate,比較單一屬性與目標值
- CompositePredicate:擴充 Predicate 以組合條件,包括 AndPredicate、OrPredicate、NotPredicate
- ComparisonOperator:定義屬性值如何比較的介面,具體類別包括 EqualsOperator、RegexMatchOperator、GreaterThanOperator、LessThanOperator
替代方案:可將 FileSearchCriteria 與 Predicate 合併並直接內嵌於 FileSearch。這雖簡化結構但會讓搜尋邏輯與條件評估緊密耦合,難以替換。
類別圖設計#
File#
我們不直接使用 Java 標準函式庫的 java.io.File,而是自訂 File 類別作為核心實體,搭配 FileAttribute enum 定義可搜尋屬性。
Design Choice:FileAttribute 定為 enum 提供型別安全的固定屬性集合,確保只用合法屬性,並避免執行期錯誤。新增屬性(如修改時間)只需擴充 enum,不動既有邏輯。
FileSearch#
FileSearch 負責從給定 File 開始遍歷檔案系統,使用 FileSearchCriteria 篩選並回傳符合的檔案。把遍歷與過濾分離維持了模組化與可擴展性。
FileSearchCriteria#
FileSearchCriteria 連結 FileSearch 與 Predicate,告訴 FileSearch 何謂「符合」。
Design Choice:FileSearchCriteria 委派 Predicate 處理評估邏輯,職責清楚且模組化。如此可在不更動 FileSearch 或 FileSearchCriteria 的前提下,更換或組合過濾邏輯。
Predicate 與 SimplePredicate#
關鍵設計之一是定義「哪些檔案應出現在結果中」的條件。這些條件可從簡單到複雜:
- 名稱符合 regex
report.* - 大小超過 10 bytes
我們以 Predicate 介面為評估基礎,定義單一方法:接受 File,回傳 boolean。
對於單純條件,SimplePredicate 評估單一屬性對某值的比較,運用 FileAttribute enum 與 ComparisonOperator 實例。
ComparisonOperator#
ComparisonOperator 介面定義「比較檔案屬性與期望值」的契約。具體實作:
- EqualsOperator — 確認兩值是否相等(例如 owner 是否為 ‘bob’)
- GreaterThanOperator — 驗證一值是否較大(例如 size 是否 > 10)
- LessThanOperator — 確認一值是否較小
- RegexMatchOperator — 評估字串值是否符合 regex
替代方案的取捨:
- 字串表示(如
"equals"、">"):實作簡單,但驗證移至執行期,效能差且易拋例外- enum(如
EQUALS、GREATER_THAN):型別安全且編譯期可檢,但新增運算需更動 enum- 介面(本設計):可建立新類別而不動既有程式碼,遵循開放/封閉原則
Composite Predicate#
真實搜尋常需以邏輯運算組合多重條件。我們採用 Composite design pattern 從簡單條件構築複雜述詞。
想了解 Composite Pattern,請參考章末**延伸閱讀**。
考慮以下搜尋:
((size > 10 and size < 1000 and owner = "alice") or (size > 1000 and !(filename matches /prefix.*/)))
把每個簡單條件標記為:
- A:size > 10
- B:size < 1000
- C:owner = ‘alice’
- D:size > 1000
- E:filename matches ‘prefix.*’
整體變為:((A and B and C) or (D and !(E)))
這種樹狀結構需要系統化的方式評估。
我們定義 CompositePredicate 介面(繼承 Predicate),具體實作:
- AndPredicate:接受多個 predicate,全部成立才回傳
true - OrPredicate:任一成立即回傳
true - NotPredicate:包裝單一 predicate 並反轉結果
完整類別圖#
程式實作#
File#
File 類別同時模擬檔案與目錄,封裝 size、name、owner 等屬性與一個布林旗標標示是否為目錄。它透過子 File 集合維持階層結構,並設計為不可變(immutable)——屬性在建構時設定後即不可更動。
// Represents a file or directory in the file system
// Contains basic file attributes and supports hierarchical structure
public class File {
private final boolean isDirectory;
private final int size;
private final String owner;
private final String filename;
// Set of directory entries (files and subdirectories)
private final Set<File> entries = new HashSet<>();
// Creates a new file with the specified attributes
public File(
final boolean isDirectory,
final int size,
final String owner,
final String filename) {
this.isDirectory = isDirectory;
this.size = size;
this.owner = owner;
this.filename = filename;
}
// Extracts the value of a specified file attribute
public Object extract(final FileAttribute attributeName) {
switch (attributeName) {
case SIZE -> {
return size;
}
case OWNER -> {
return owner;
}
case IS_DIRECTORY -> {
return isDirectory;
}
case FILENAME -> {
return filename;
}
}
throw new IllegalArgumentException("invalid filter criteria type");
}
// Adds a file or directory entry to this directory
public void addEntry(final File entry) {
entries.add(entry);
}
// getter methods omitted for brevity
}
// Represents the different attributes that can be checked for a file
public enum FileAttribute {
IS_DIRECTORY,
SIZE,
OWNER,
FILENAME
}關鍵方法:
extract— 將 FileAttribute enum 對應到實際欄位值,提供 SimplePredicate 評估所需資料addEntry— 將 File 加入 entries 集合,建構支援遞迴遍歷的階層結構
Predicate#
// Base interface for all file search predicates
public interface Predicate {
// Checks if the given file matches the search condition
boolean isMatch(final File inputFile);
}抽象介面確保彈性,支援從簡單檢查到複雜組合的所有情境。
ComparisonOperator#
我們使用 generics(<T>)強制型別安全,確保數值屬性只與數值比較、字串屬性只與字串比較。
// Base interface for all comparison operations in the file search system
public interface ComparisonOperator<T> {
boolean isMatch(final T attributeValue, final T expectedValue);
}具體實作:
// Implements exact equality comparison between values
public class EqualsOperator<T> implements ComparisonOperator<T> {
@Override
public boolean isMatch(final T attributeValue, final T expectedValue) {
return Objects.equals(attributeValue, expectedValue);
}
}
// Implements greater than comparison for numeric values
class GreaterThanOperator<T extends Number> implements ComparisonOperator<T> {
@Override
public boolean isMatch(final T attributeValue, final T expectedValue) {
return Double.compare(attributeValue.doubleValue(), expectedValue.doubleValue()) > 0;
}
}
// Implements less than comparison for numeric values
class LessThanOperator<T extends Number> implements ComparisonOperator<T> {
@Override
public boolean isMatch(final T attributeValue, final T expectedValue) {
return Double.compare(attributeValue.doubleValue(), expectedValue.doubleValue()) < 0;
}
}
// Implements regular expression pattern matching for string values
public class RegexMatchOperator<T extends String> implements ComparisonOperator<T> {
@Override
public boolean isMatch(final T attributeValue, final T expectedValue) {
final Pattern p = Pattern.compile(expectedValue);
return p.matcher(attributeValue).matches();
}
}Implementation Choice:使用 generics 在編譯期強制型別安全,例如
Double對應 size、String對應 filename。
SimplePredicate#
// A basic predicate that compares a file attribute with an expected value
public class SimplePredicate<T> implements Predicate {
// The name of the file attribute to check
private final FileAttribute attributeName;
// The operator to use for comparison (equals, contains, greater than, etc.)
private final ComparisonOperator<T> operator;
// The expected value to compare against
T expectedValue;
// Creates a new simple predicate with the specified attribute, operator, and
// expected value
public SimplePredicate(
final FileAttribute attributeName,
final ComparisonOperator<T> operator,
final T expectedValue) {
this.attributeName = attributeName;
this.operator = operator;
this.expectedValue = expectedValue;
}
@Override
public boolean isMatch(final File inputFile) {
// Extract the actual value of the attribute from the file
Object actualValue = inputFile.extract(attributeName);
// Check if the actual value is of the correct type
if (expectedValue.getClass().isInstance(actualValue)) {
// Perform the comparison using the specified operator
return operator.isMatch((T) actualValue, expectedValue);
} else {
return false;
}
}
}CompositePredicate#
CompositePredicate 介面繼承 Predicate 以組合多個述詞為複雜條件。我們遵循 Composite design pattern,將述詞組成一棵樹:簡單者(SimplePredicate)為葉節點、組合者(AndPredicate 等)為內部節點,皆共享 Predicate 介面。
public interface CompositePredicate extends Predicate {
// This interface is intentionally empty as it serves as a marker
// to identify predicates that combine multiple other predicates (AND, OR, NOT)
}
// Implements logical AND operation between multiple predicates
public class AndPredicate implements CompositePredicate {
// List of predicates that must all match for this predicate to match
private final List<Predicate> operands;
// Creates a new AND predicate with the specified predicates
public AndPredicate(final List<Predicate> operands) {
this.operands = operands;
}
// Checks if the given file matches ALL predicates
@Override
public boolean isMatch(final File inputFile) {
return operands.stream().allMatch(predicate -> predicate.isMatch(inputFile));
}
}
// Implements logical OR operation between multiple predicates
public class OrPredicate implements CompositePredicate {
// List of predicates, at least one of which must match
private final List<Predicate> operands;
// Creates a new OR predicate with the specified predicates
public OrPredicate(final List<Predicate> operands) {
this.operands = operands;
}
@Override
public boolean isMatch(final File inputFile) {
return operands.stream().anyMatch(predicate -> predicate.isMatch(inputFile));
}
}
// Implements logical NOT operation on a predicate
public class NotPredicate implements CompositePredicate {
// The predicate to negate
private final Predicate operand;
// Creates a new NOT predicate with the specified predicate to negate
public NotPredicate(final Predicate operand) {
this.operand = operand;
}
@Override
public boolean isMatch(final File inputFile) {
return !operand.isMatch(inputFile);
}
}重點:
- AndPredicate:所有條件皆成立才為真
- OrPredicate:任一條件成立即為真
- NotPredicate:取單一條件並反轉
- AndPredicate / OrPredicate 用
List<Predicate>支援任意數量條件 - NotPredicate 僅取一個條件
FileSearchCriteria#
// Wrapper class that encapsulates a search condition for file matching
public class FileSearchCriteria {
// The predicate that defines what makes a file match
private final Predicate predicate;
// Constructor that takes a predicate defining the criteria
public FileSearchCriteria(final Predicate predicate) {
this.predicate = predicate;
}
// Checks if the given file matches the search criteria
public boolean isMatch(final File inputFile) {
return predicate.isMatch(inputFile);
}
}Implementation Choice:將 Predicate 包在 FileSearchCriteria 中,可讓 FileSearch 專注於遍歷邏輯,並把條件評估隔離到獨立、可重用的層。
FileSearch#
FileSearch 使用堆疊式(stack-based)做法遍歷檔案系統,避免遞迴造成的 stack overflow。
// Main class responsible for performing file system searches
public class FileSearch {
// Performs a recursive search through the file system starting from root
// Returns a list of files that match the given criteria
public List<File> search(final File root, final FileSearchCriteria criteria) {
// List to store matching files
final List<File> result = new ArrayList<>();
// Stack to handle recursive traversal without actual recursion
final ArrayDeque<File> recursionStack = new ArrayDeque<>();
// Start with the root directory
recursionStack.add(root);
// Continue until we've processed all files
while (!recursionStack.isEmpty()) {
// Get the next file to process
File next = recursionStack.pop();
// Check if the file matches our criteria
if (criteria.isMatch(next)) {
result.add(next);
}
// Add all directory entries to the stack for processing
for (File entry : next.getEntries()) {
recursionStack.push(entry);
}
}
return result;
}
}流程:
- 初始化結果清單與包含 root 的
ArrayDeque堆疊 - 從堆疊取出檔案
- 用
criteria.isMatch評估,符合者加入結果 - 將其子項目推入堆疊以進行深度優先遍歷
- 回傳結果清單
Implementation Choice:選擇堆疊而非遞迴呼叫,是為了在深層檔案系統中避免 stack overflow。遞迴方法在小型結構可行,但在大或深層巢狀目錄上有失敗風險。
深度討論#
檔案搜尋測試#
實作完成後,建議用測試案例驗證端到端邏輯。以下測試 「擁有者匹配 ‘ge.*’ 的非目錄檔案」:
public class FileSearchTest {
@Test
public void testFileSearch() {
// Create a root directory and two files with different owners
final File root = new File(true, 0, "adam", "root");
final File a = new File(false, 2000, "adam", "a");
final File b = new File(false, 3000, "george", "b");
// Add files to the root directory
root.addEntry(a);
root.addEntry(b);
// Search criteria: Find non-directory files owned by users matching "ge.*"
final FileSearchCriteria criteria =
new FileSearchCriteria(
new AndPredicate(
List.of(
new SimplePredicate<>(
FileAttribute.IS_DIRECTORY,
new EqualsOperator<>(),
false),
new SimplePredicate<>(
FileAttribute.OWNER,
new RegexMatchOperator<>(),
"ge.*"))));
// Execute the search and get results
final FileSearch fileSearch = new FileSearch();
final List<File> result = fileSearch.search(root, criteria);
// Verify that only one file matches the criteria
assertEquals(1, result.size());
// Verify that the matching file is "b"
assertEquals("b", result.get(0).getFilename());
}
}本章小結#
系統的可維護性與可擴展性透過清晰的職責分離達成:
- File:代表檔案
- FileSearch:遍歷目錄
- FileSearchCriteria:評估條件
- Predicate:定義匹配邏輯
設計選擇如分離 FileSearch 與 FileSearchCriteria、在 ComparisonOperator 中使用 generics,皆提升了可擴展性與型別安全。雖然將 FileSearchCriteria 與 Predicate 合併能讓初始設計更緊湊,但會模糊兩者的角色,使更新或替換條件邏輯時容易影響遍歷。
延伸閱讀:Composite Design Pattern#
Composite Design Pattern#
Composite 是結構型設計模式,讓你能將物件組成樹狀結構,並像處理單一物件一樣處理整棵樹。
問題情境#
想像你有兩種物件:File 與 Folder。Folder 可包含多個 File 與更多子 Folder。如果要建立搜尋系統,如何讓搜尋對單一 File 與含巢狀 Folder 的結構都能一致運作?
解法#
Composite 模式建議透過一個共同介面來處理 File 與 Folder。介面宣告一個檢查條件的方法:
- 對 File:直接檢查條件(如 size > 10),回傳結果
- 對 Folder:
- 對其中每個項目逐一檢查
- 對任何巢狀資料夾遞迴套用此流程
- 也可加上自身限制(如「非目錄」)來精煉結果
Item 是 File 與 Folder 共同實作的介面。透過共同介面,無論 File 或巢狀 Folder 都能用相同方式處理。
何時使用#
- 需要建立樹狀物件結構
- 希望客戶端程式能統一處理簡單與複雜元素