SOLID中L与D原则:接口使用者对实现的依赖边界疑问
Great question—this is one of those places where SOLID principles meet the messy reality of real-world code, and the line between "contract" and "implementation detail" gets blurry. Let's break this down step by step.
Liskov Substitution and Dependency Inversion aren't asking you to stick your head in the sand and ignore all implementation context. They're asking you to only rely on what the contract explicitly guarantees.
The key distinction here is:
- Do rely on what the contract says the implementation will do (e.g., "this method creates a NumericParameter").
- Don't rely on what the contract doesn't say—whether that's "the implementation will also create a related entity" (extra behavior) OR "the implementation won't modify X row in the database" (absence of behavior).
Both of these are unstated implementation details, and relying on either sets you up for pain when the implementation changes.
Your transaction example hits the nail on the head: when you assume foo() won't touch a row you're holding with for update, you're building a fragile dependency. If a future refactor makes foo() need to update that row (to fix a bug, add a feature, etc.), your code will break in a hard-to-debug way—because the contract never said foo() wouldn't do that.
This isn't just a "code smell"—it's a ticking time bomb. It forces every future developer working on foo() to know every single place it's called, and every transaction context those calls live in. That's impossible to scale in a large codebase.
The solution isn't to "ignore implementation details entirely"—it's to turn critical implementation constraints into part of the contract. Here's how to do that in practice:
Explicitly document side effects in the contract
Your interface's documentation (Javadoc, comment blocks, or formal specs) should spell out any behavior that affects callers' context. For example:createNumericParameter(): Creates a new NumericParameter entity. Note: This method will also update the related ParameterMetadata table in the current transaction, and may modify existing rows in the Parameter table if validation requires it.This turns an implementation detail into a contractual guarantee. Now callers aren't relying on "what it won't do"—they're relying on "what it will do" (and what it might do), which is fair game.
Design contracts to minimize hidden side effects
If possible, split methods into pure, side-effect-free operations and explicit side-effect operations. For example, instead of havingcreateNumericParameter()secretly update other tables, have:buildNumericParameter(): Pure method that creates the in-memory entity (no DB calls)persistNumericParameter(): Method that saves it to the DB, with explicit documentation about related table updates
This makes the contract clearer and reduces the chance of unexpected interactions.
Use architectural guardrails to enforce boundaries
If your code relies on transactional integrity, enforce rules like:- Methods that modify shared rows must declare their transaction propagation behavior (e.g., "this method requires a new transaction" or "this method joins the existing transaction")
- Use database-level locks or optimistic concurrency control to detect conflicts, even if you don't know every implementation detail
These guards catch conflicts before they cause silent data loss, even if an implementation changes.
Test for contractual behavior (not just implementation)
Write integration tests that validate the contract's guarantees—including side effects. For example, a test that checks if callingcreateNumericParameter()in a transaction with a locked row causes a conflict. If the implementation changes and breaks this test, you'll catch it before it reaches production.
In your scenario, the problem isn't that you're "looking at implementation details"—it's that the contract is incomplete. The fact that foo() modifies the row you're holding should be part of the interface's documented behavior. If it's not, that's a design flaw, not a failure of SOLID.
Once that's part of the contract, you can adjust your code accordingly—maybe by calling foo() before you lock the row, or by using a different isolation level, or by splitting the transaction. You're no longer relying on an implementation detail; you're relying on the contract.
内容的提问来源于stack exchange,提问作者Maksim Gumerov

