← 返回 rippling 的题目列表Logger OOD
类型:qbank
AI-coding OOD prompt for a configurable logger. Implement output transformations and an internal message store, then add search while discussing read/write trade-offs.
Requirements
Implement a logger with configurable behaviors:
remove all occurrences of a configured substring before printing;
truncate messages to a maximum number of characters before printing;
capitalize the entire message before printing;
store messages in an internal list without printing.
Use OOD / design-pattern structure rather than a single monolithic function.
Follow-up: search stored messages for a list of keywords such as [x, y, z]. Return matching logs in original order without duplicates.
Follow-up: explain how the storage and index change when duplicate results do not matter.
Discuss read/write trade-offs for the stored data structure.
A common canonical shape is a Logger constructed from an ordered list of handlers, where each handler transforms the message and signals whether output should still be printed:
from typing import Optional
class Logger:
def __init__(self, handlers: list["LogHandler"]): ...
def log(self, message: str) -> Optional[str]: ...
# Runs the message through handlers in order.
# Returns the printed message, or None when output is suppressed
# (e.g. a store handler consumes the message instead of printing it).
class LogHandler:
def handle(self, message: str) -> tuple[str, bool]: ...
# Returns (new_message, should_print).
# Logger prints only if every handler returned should_print=True.
Typical handlers: RemoveStringHandler(target) → message.replace(target, ""); TruncateHandler(max_chars) → message[:max_chars]; CapitalizeHandler() → message.upper(); StoreHandler() appends to an internal list and returns should_print=False.
Notes
The clean structure is a pipeline or decorator chain of message processors plus a sink interface for printing vs storing. The Decorator pattern lets each transformation (substring removal, truncation, capitalization) wrap an inner sink with the same log(msg) interface; the Strategy pattern is the alternative — a list of transformer objects applied in sequence before the sink. Either works; pick one and justify based on extension axes. The handle(msg) -> (msg, should_print) handler-list shape above is the most common concrete rendering: adding a behavior is just a new handle implementation, and should_print is the boolean the Logger folds with AND across all handlers.
For search, start with linear scan for simplicity, then discuss inverted indexes, prefix indexes, or normalized token maps if reads dominate. A common follow-up asks to optimize keyword search by building a per-word index incrementally at log-write time (token → message references), so search becomes a direct lookup instead of a traversal — the classic write-time-cost-for-read-time-speed trade-off.
The prompt is thin on exact output format; clarify transformation order and whether storage happens before or after transformations.
A related but not equivalent LC anchor is 359 (Logger Rate Limiter) — same Logger framing, but the rate-limiter problem is a (message, timestamp) dedup over a 10-second window, not a transformation pipeline. Useful for warming up the OOD vocabulary, not as the target solution.
Precise handler semantics to nail
"Capitalize" means uppercase the ENTIRE message — use str.upper(), not str.capitalize() (which only touches the first character). This is a deliberate trap.
Handler ordering changes the result — removing before truncating differs from truncating before removing. Truncation keeps trailing whitespace, so remove("debug ") then truncate(12) on "debug hello world from rippling" yields "hello world " → "HELLO WORLD " (note the retained trailing space). State the order explicitly.
StoreHandler suppresses output by returning should_print=False, modeling "store without printing"; place it at the end of the chain when stored logs should hold the fully-transformed message.
Guard TruncateHandler against negative max_chars (raise / clamp) — otherwise message[:negative] silently drops from the tail.
Inverted-index shape for the search follow-up
Maintain word -> set[int] mapping each token to the ids of messages containing it; append the message, then index its tokens at write time inside handle. Tokenize with re.findall(r"[a-zA-Z0-9]+", message) and normalize to lowercase so search is case-insensitive.
For multi-keyword search, union the posting lists for the requested terms, then sort the unique message ids to restore insertion order. When duplicates are acceptable, merge the per-keyword posting lists by message id without deduplicating them (or concatenate and sort all ids); this preserves original order while allowing a log that matches multiple keywords to appear more than once.
search(keyword) normalizes the keyword and returns [messages[i] for i in sorted(ids)] — a direct dict lookup, O(M + M log M) for M matches, versus O(S·L) for the linear scan over S stored messages of length L.
Trade-off: using a list[int] per word instead of a set[int] makes search O(M) (no sort, ids already in insertion order) but you must dedup so a word appearing multiple times in one message isn't recorded twice.
Examples
Logger([RemoveStringHandler("debug "), TruncateHandler(12), CapitalizeHandler()]) on "debug hello world from rippling" → prints "HELLO WORLD " (remove → "hello world from rippling", truncate → "hello world ", upper → "HELLO WORLD ").
Logger([RemoveStringHandler("[internal] "), CapitalizeHandler(), StoreHandler()]) after logging "[internal] payroll sync failed" and "[internal] employee import completed" prints nothing; the store holds ['PAYROLL SYNC FAILED', 'EMPLOYEE IMPORT COMPLETED'].
Preparation
Implement a small logger using Strategy / Decorator and write tests for each transformation order.
Practice adding a SearchableStore abstraction without rewriting the processing pipeline.
Prepare read-heavy and write-heavy indexing trade-offs for substring and token search.