Академический Документы
Профессиональный Документы
Культура Документы
Code Health
Overview
Coming up... Defining Terms Analyzing example design decisions From specifics to generalization - Why it works Influencing development efforts Q&A
Flexible Design
Easy to Modify Extend Reuse Understand Changes Not Intrusive Don't explode complexity
Testable Design
Focused on Unit Testing Many ideas extend to components/subsystems Easy to Instantiate class and collaborators Setup into the desired state Drive code through each execution path Witness effects
Isolated from one another (hermetic) Pinpoint bugs (vs just indicating the system regressed) Easy to write and understand Validate
Happy paths Corner cases Exceptions
A Google motto. . .
Aside
End-to-End Testing
End-to-end tests validate the big picture
Configuration Assembly Large-scale behavior
Unit tests Don't eliminate end-to-end testing Are effective for internal details Can reduce number of end-to-end tests
Design Choices...
For each of several design choices We'll introduce a Situation Propose Design 1
Why it has poor flexibility Why it has poor testability
Propose Design 2
Why flexibility has improved Why testability has improved
Grouping Responsibilities
Situation
How large should our classes be? I don't want to write a bunch of little classes for no reason
Grouping Responsibilities
Grouping Responsibilities
Aside
Grouping Responsibilities
Same with private methods If many unrelated methods have access, how private is private?
Grouping Responsibilities
Grouping Responsibilities
Changing tests (e.g., the golden files) in lockstep with production code is an inferior safety net
It's difficult to know whether the new file you're generating as a golden file is actually valid
Grouping Responsibilities
Design 2: Preferred
Separate each distinct behavior into its own class
RequestHandler PreferenceReader PreferenceParser FilterSelector MusicMixer etc...
Grouping Responsibilities
Design 2: Flexibility
Each class is easy to understand: conceptual chunk We regain the power that comes from OO features that support polymorphism and encapsulation If a responsibility has variable behavior
Further divided into an inheritance hierarchy Select a sibling dynamically
Grouping Responsibilities
Grouping Responsibilities
Situation
A Class needs configuration data that's stored in a File How should we supply the information to the class?
These aren't User's core responsibility, so this is a smaller violation of Single Responsibility Principle It's such a pervasive example we want to call it out
Design 1: Code
class User { private Preferences prefs; public User(File preferenceFile) { prefs = parseFile(preferenceFile); } public void doSomething() { // using prefs } private Preferences parseFile {. . . } }
Inhibited Reuse
We've buried reading, parsing and validation inside User
Test File for each type of preference (or each important combination) If file format changes (e.g., a new piece of required data), update every test file
Design 2: Preferred
Direct injection of Preferences
class User { private Preferences prefs; public User(Preferences prefs) { this.prefs = prefs; } public void doSomething() { // using prefs } } We've just moved File parsing to the creator of User, we'll address this later
Design 2: Flexibility
User is stable if the file format changes Data can easily come from another source:
An RPC A Database An internal cache
Classes dedicated to each source can produce a Preferences object for User
Add a source = add an RpcPreferenceSource or DatabasePreferenceSource class
Design 2: Testability
Runs fast Preferences is trivial to initialize in a test
Much easier to create many variations for testing
Aside
General Input
Reasoning applies to other forms of Input
Raw sockets RPC mechanisms Data stores Etc Often: add an abstraction layer to isolate the input
Introduce a mediating class to retrieve input and supply transformed data to consuming classes Flexibility & Testability issues the same as for Files
Situation
One class in the system provides a stateless service that other classes in the system consume. How should behavior of that service be accessed?
Design 1: Code
If a music score is changed, inform Authors via email class MusicalScore { private void sendScoreUpdate() { // set up message ... EmailSender.send(message); } }
Caveat
Extremely simple stateless utilities don't cause the problems we're describing Math.abs() is fine as a static utility
Design 2: Preferred
Make the service instantiable Unify variant implementations in inheritance hierarchy Inject the appropriate Sender into a MusicalScore
Design 2: Code
class MusicalScore { public MusicalScore(Sender sender) { this.sender = sender} private Sender sender; private void sendUpdate() { // set up message sender.send(message); } }
Sender is the base class for EmailSender, TextSender, BuzzSender, etc. We'll explain later how to get the sender to the Score
Design 2: Flexibility
MusicalScore doesn't know each Sender's type Conditional logic about which sender to use is separated from a MusicalScore Adding new sender types, MusicalScore remains stable
no more intrusive changes
Design 2: Testability
Fakes or Mocks are possible
Avoid expensive operations (time, memory, etc.) Avoid undesirable side effects Easily simulate exceptional cases in the service for testing
Situation
Many pieces of data needed by classes throughout the system
Data supplied on a command line Data in an initialization file Data from a request Anything derivable/discoverable from these
Classes like Account that require the country code dig it out of User
Digging code is often duplicated Digging code locks the structure it walks in place Deeper Law of Demeter violation
// } It may be easier said than done, now we'll talk about how
System Assembly
Top Down Bottom-Up
Ca r Winds hield Engi ne Spark plug Spark plug
System Assembly
System Assembly
System Assembly
Design 2: Code
class Engine { public Engine(Set<Sparkplug> sparkplugs) { this.sparkplugs = sparkplugs; } class Car { public Car(Engine engine, Windshield windshield) { this.engine = engine; this.windshield = windshield; } }
Design 2: Flexibility
Easy to assemble Car with
FuelEfficientEngine or HighPerformanceEngine TintedWindshield or ClearWindshield
Design 2: Testability
Easy to test pieces in isolation Easy to create fakes or mocks
Car with FakeEngine that refuses to start Spark plug that validates it was asked to fire
Situation
Clusters of behaviors that change together Multiple sites where these behaviors are used Clarifying example...
Example
Free, Basic, and Premium Accounts By account type, MusicMixer limits:
generated music duration available filters available instruments ...
Logic is spread around so it's hard to understand everything about a given account type Invites more bugs!
We were charged for 1 million extra uses of premium echo filtering. Oops! Where was the bug?
A 3-way decision at each of 5 features = 15 tests Additional account type = another test per feature! Additional feature = another test per account type!
Design 2: Preferred
Isolate Free, Basic and Premium behavior into separate classes
FreeMixer, BasicMixer, PremiumMixer
Design 2: Preferred
class Director { private MusicMixer mixer; private void truncateAudio() { mixer.truncateAudio(); //... } private void applyEchoFilter() { mixer.applyEchoFilter(); //... }
Design 2: Flexibility
Easy to support another type of Account
Director remains stable MusicMixer interface remains stable
All the logic for FreeMixer (or Basic or Premium) is together in one place Director is simpler (no conditional logic) Safer to make the mixer subclass decision in one place
Design 2: Testability
Eliminated the combinatorial explosion of tests May write a test that validates when we have a premium account, we get a premium mixer (if not already clearly tested by end results) No longer need multiple tests to ensure that Director is calling appropriate MusicMixer method
Generalization
Generalization
Generalization
Generalization
Generalization
Inject exactly what a class uses Encapsulation isn't violated Loose Coupling
Classes tied only to collaborators they use
Generalization
Tight Coupling
Car is tied to engine types, engine is tied to sparkplug types
System Assembly Bottom up (Engine injected into Car) Assembly consolidated in Factories/Builders
separated from business logic
Loose Coupling
Car only knows Engine interface
Generalization
High Cohesion
Behavior for each Account type is together in one class
Generalization
Summary
Encapsulation, Coupling, Cohesion are common themes Optimize for these
Flexibility increases Testability increases
Flexibility: Crisp units of behavior you can exchange, vary and reuse Testability: Small units that can be exercised through paths, with the ability to substitute collaborators, isolate behavior and witness results.
Customization 1/2
Based on our experience across industry Working with 100s of individuals and teams Canned arguments and prepared speeches aren't enough Each case is unique. Tap into this to influence an engineer's design habits.
Customization 2/2
To find the right convincing argument, understand Developer Team Their motivations The context they're working in Challenges on the horizon
Good Design
I don't want to corrupt my design just to make it testable.
For Theoretical
First establish common ground Set the stage (as we did earlier, with global mutable state) Transition to a related issue
If By Example
Find examples in their code Make your points using their code
Conclusion
Extend these ideas to inform your own design decisions and influence your teams to build flexible and testable systems.
Q&A
Flexible Code? Testable Code? You Don't Have To Choose! Thank you!