Null Object
Null Object supplies a valid object that intentionally does nothing. It can remove repeated optional checks where “no behavior” is a legitimate configuration.
When it helps in iOS
A feature records analytics in production, but previews and unit tests should not send events. A no-op analytics client lets the feature call the same interface in every environment.
Original example: a no-op analytics client
import Foundation
protocol AnalyticsTracking {
func track(_ event: String)
}
struct NoOpAnalytics: AnalyticsTracking {
func track(_ event: String) {}
}
struct ConsoleAnalytics: AnalyticsTracking {
func track(_ event: String) { print("Event: \(event)") }
}
final class SavedItemsFeature {
private let analytics: any AnalyticsTracking
init(analytics: any AnalyticsTracking) {
self.analytics = analytics
}
func itemSaved() {
analytics.track("item_saved")
}
}
let preview = SavedItemsFeature(analytics: NoOpAnalytics())
preview.itemSaved()The feature has no if analytics != nil branch. The composition root decides whether behavior is real or intentionally absent.
Production decisions
Use a null object only when silence is correct. A payment service that silently does nothing would hide a serious error. For tests that must verify events, inject a recording spy instead of NoOpAnalytics. Keep the no-op implementation visibly named so production wiring mistakes are easy to spot.
Why a real interface matters
SavedItemsFeature requires AnalyticsTracking, so it can always call track(_:) without an optional check. NoOpAnalytics fulfills the protocol while intentionally doing nothing. The feature does not know whether it is in a preview, test, or production build; the composition root chooses the implementation. This keeps environment policy outside the feature and makes absence of analytics explicit.
From example to an iOS app
Use a production analytics adapter at app startup, respecting consent and privacy settings. Inject NoOpAnalytics into previews, offline demos, or a user session that has not consented. If consent changes while the app runs, a higher-level analytics router can switch implementations or check policy before forwarding. The feature should log semantic events such as item_saved, not screen-specific implementation details.
Failure modes and verification
A no-op should never hide a required operation such as charging a card or saving a document. Test that preview mode sends nothing and use a recording spy to assert production feature events. Make the no-op type name obvious and audit composition-root wiring so it is not accidentally shipped where real tracking is required. An optional dependency may be clearer if absence itself is important to the feature's behavior.