- Introduction
- If you can visualize a system, you can probably implement it
- Means greatest limitation is the ability to understand the system we are creating
- Over time, complexity accumulates, leading to bugs which slows development
- two general approaches to fight complexity
- making code simpler and more obvious
- encapsulate complexity
- software design continual process, waterfall rarely works, initial design will have many problems
- design never done, should always look for opportunities to improve
- goal 1: describe the nature of software complexity
- goal 2: present techniques to minimize complexity
- try to use book in conjunction with code reviews
- easier to see design problems in others’ code
- consider alternative designs, don’t give up easily
- The Nature of Complexity
- complexity
- anything related to the structure of a software system that makes it hard to understand and modify
- overall complexity determined by the fraction of time developers spend working on that part
- isolating complexity in a place where it will never be seen is almost as good as eliminating it
- complexity more apparent to readers than writers
- system of complexity
- change amplification: simple changes requires code modifications in many different places
- cognitive load: how much a developer needs to know in order to complete a task
- sometimes more lines of code simpler because reduces cognitive load
- unknown unknowns: not obvious which pieces of code must be modified to complete a task
- an important goal in good design is for the system to be obvious
- causes of complexity
- 2 things: dependencies and obscurity
- when code cannot be modified in isolation
- try to reduce the number of dependencies
- obscurity: important information not obvious
- inconsistency or inadequate documentation is a major contributor
- complexity is incremental
- accumulates in lots of small chunks
- incremental nature makes hard to control
- adopt a “zero tolerance” philosophy
- conclusion
- complexity results in more time for modifications
- developers spend more time acquiring enough info to safely make changes
- Working Code Isn’t Enough
- Intro
- many organizations promote a quick tactical approach
- for good design, want to take a more strategic approach
- cheaper in the long run
- tactical programming
- the main focus is getting it working quickly
- makes it nearly impossible to produce a good design
- how systems incrementally become complicated
- the organization may view tactical tornadoes as heroes which require other engineers to clean up (making them look slower)
- strategic programming
- working isn’t enough
- most important is long-term structure of system
- most important job is to facilitate future extensions
- requires an investment mindset
- speeds you up in the long-term
- rather than implementing the first design, try to think of a couple of alternatives and pick the cleanest one
- inevitably will make mistakes, when you discover, take a little extra time to fix it
- how much to invest?
- the ideal design emerges from bits and pieces
- make lots of small investments on a continual basis
- 10-20% of dev time
- allows quicker development in the future
- startup and investment
- once a code base turns to spaghetti, almost impossible to fix
- an important factor for success is also quality of engineers
- best engineers care deeply about good design
- the company can succeed with both approaches, but usually more fun to work at a company that cares about design
- conclusion
- good design doesn’t come for free
- need to invest in continually
- if you keep delaying improvements, easy for delays to become permanent
- Modules Should Be Deep
- Intro
- one of the most important techniques is to the design system so developers only need to face a small fraction of the overall complexity at any given time
- Modular Design
- ideally independent, but in real life, must interact and call each other’s methods
- must know something about the other
- the goal of modular design is to minimize dependencies
- 2 parts: interface and implementation
- the interface is everything a developer working in a different module must know in order to use it
- the interface describes the “what” not the “how”
- the implementation carries out the promise
- best modules have interfaces that are much simpler than the implementation
- simple interface imposes less complexity to the system
- if the module is modified without changing the interface, it doesn’t affect other users
- What’s in an interface
- formal and informal info
- formal is code signature
- informal can’t be specified in code and can only be described in the comments
- abstractions
- a simplified view of an entity which omits unimportant details
- interface provides a simplified view of functionality
- abstractions go wrong when 1) include details that are not important (more complex than they need to be) and 2) omit details that are important (obscurity)
- the key is knowing what is important
- deep modules
- best modules provide powerful functionality but simple interfaces
- cost/benefit
- benefit: functionality
- cost: in terms of system complexity is the added interface
- shallow modules
- the complexity of the interface nearly as great as the implementation
- sometimes unavoidable
- doesn’t help manage complexity
- Class-itis
- conventional wisdom today is classes should be small not deep
- class may be simple, but produces a lot of complexity from accumulated interfaces
- Java and Unix I/O
- good to make the common case as simple as possible
- effective complexity is the complexity of commonly used features
- conclusion
- make modules deep with simple interfaces
- Information Hiding (and Leakage)
- Intro
- most important for achieving deep modules
- each module should encapsulate a few pieces of knowledge
- embedded in implementation but not visible in the interface
- info such as data structures, algorithms, low-level such as page size
- 1) this simplifies the interface
- 2) makes the system easier to evolve
- if info is hidden, no dependencies
- best is when completely hidden, but can still be visible if partially hidden e.g. particular feature can be accessed with another method
- Information Leakage
- when design decision reflected in multiple modules
- change requires changes to multiple modules
- can leak even if not in interface e.g. both depend on certain file format (not obvious)
- information leakage is one of the most important red flags in software design
- if affected classes are small, maybe should merge? or can pull out to a new class
- temporal decomposition
- when the order of operations matter
- e.g. having a separate class to read and another class to write a file
- when designing a module, focus on the knowledge that’s needed to perform each task, not order in which tasks occur
- HTTP example
- if classes share a lot of knowledge better to merge
- important to avoid exposing internal data structures as much as possible so implementation changes don’t affect the interface
- make the common case as simple as possible
- don’t force users to learn about rarely used features
- Information hiding within a class
- can use private methods
- minimize the number of places where each instance variable is used
- taking it too far
- only needed when information is not needed outside its module
- General-Purpose Modules are Deeper
- Intro
- general-purpose vs specific is a common decision
- hard to predict future
- you may never use the general purpose facilities
- make classes somewhat general purpose
- try to keep the interface general
- Generality leads to better information hiding
- cleaner separation
- one of the most important elements of software design is determining who needs to know what and when
- questions to ask yourself
- what is the simplest interface that will cover all my current needs
- reducing methods only makes sense as long as the API for each individual method stays simple i.e. not adding a bunch of new arguments
- in how many situations will this method be used?
- method for one particular use is a red flag
- is this API easy to use for my current needs?
- if you have to write a lot of additional code to use a class for your current purpose it is a red flag
- Different Layer, Different Abstraction
- Intro
- software systems are composed of layers
- each layer should provide a different abstraction from the layers above and below it
- pass-through methods
- when adjacent layers have similar abstractions
- makes classes shallower and creates dependencies
- the interface to a piece of functionality should be in the same class that implements the functionality
- 1) expose lower level class directly to the caller of the higher level
- 2) redistribute the functionality
- 3) merge functionality
- when is interface duplication ok?
- 1) dispatcher which uses the arguments to select one of several other methods to invoke e.g. web servers
- 2) interfaces with multiple implementations e.g. disk drivers for different operating systems
- decorators
- extends functionality
- separate special purpose extension of a class from a more generic core
- there are usually better alternatives
- 1) could you add directly to the underlying class?
- 2) if new functionality specialized for a use case, can you merge it with the use case?
- 3) add to an existing decorator?
- 4) does functionality need to wrap existing functionality or implement as a standalone class?
- interface vs implementation
- representations used internally should be different from abstractions that appear in the interface
- pass through variables
- may need to modify a large number of interfaces
- some solutions:
- 1) is there an object that is already shared by top/bottom method?
- 2) global var? other problems using this?
- 3) context object
- context object can become a huge grab bag of data that creates many non-obvious dependencies
- use immutable variables
- Pull Complexity Downwards
- Intro
- usually better for the developer to deal with complexity than users
- more important for the module to have a simple interface than implementation
- config parameters
- examples of moving complexity upwards
- some situations hard for low-level to know about the best policy
- usually should avoid
- think about if users will be better able to determine a value
- compute reasonable defaults
- taking it too far
- makes sense in following cases
- 1) complexity being pulled down is closely related to the class’s existing functionality
- 2) pulling down results in many simplifications elsewhere
- 3) simplifies the interface
- not simplifying higher level code and non-related functionality may be information leakage
- Better Together Or Better Apart?
- Intro
- given two functionality, should we implement together or apart?
- goal is to reduce complexity
- act of subdividing creates additional complexity
- more components adds complexity, more interfaces
- can result in more code
- separation can make it hard to see components at the same time
- can result in duplication
- bringing together better if closely related
- 1) shares info
- 2) used together, but bidirectionally, not one way
- 3) overlaps conceptually
- 4) hard to understand without looking at the other
- bringing together if information is shared
- do both methods end up with considerable knowledge of one thing?
- bringing together if it simplifies the interface
- easier to use?
- may be able to perform some functions automatically
- hide info
- bringing together to eliminate duplication
- factor out repeated code
- most effective if the repeated code is long and method has a simple signature
- can also refactor so snippet only needs to be executed once
- separate general-purpose and special purpose
- lower layer generally more general purpose
- pull special-purpose code upwards
- logging
- special class for logging errors
- if the class is shallow, can result in additional complexity with no benefit
- splitting and joining methods
- don’t break up unless makes the overall system simpler
- each method should do one thing and do it completely
- splitting up only makes sense if it results in cleaner abstractions
- child method generally more general-purpose
- if have to flip back and forth between parent and child method to understand how they work together, splitting is probably a bad idea
- may want to break up if the interface is overly complex
- good if split methods more general purpose
- should be possible to understand each method independently
- conclusion
- pick structure that results in best information hiding, fewest dependencies, and deepest interfaces
- Define Errors Out of Existence
- Intro
- one of the worst sources of complexity
- reduce the number of places where exceptions must be handled
- Why exceptions add complexity
- an uncommon condition that alters normal control flow
- first approach is to keep moving forward and try to complete
- second approach is to abort
- exception handling creates opportunities for more exceptions
- hard to ensure exception handling code is working correctly
- code that hasn’t been executed doesn’t work
- Too many exceptions
- exacerbate problems by handling unnecessary exceptions
- if you’re having trouble figuring out what to do, there is a good chance the caller won’t know either
- exceptions thrown by a class are part of its interface
- classes with a lot of exceptions have a complex interface and are shallower than classes with fewer exceptions
- throwing exceptions is easy, handling them is hard
- the best way to reduce the complexity from exception handling is reduce the number of places where exceptions have to be handled
- define errors out of existence
- define your API so there are no exceptions to handle
- e.g. return without doing anything
- example: file deletion in windows
- Windows does not permit a file to be deleted if it is open in a process
- the Unix operating system does not delete the file immediately, instead, it marks it for deletion, removes from directly and deletes after closed
- example: java substring method
- if using ranges out of range, get IndexOutOfBoundsException
- easier if perform adjustment automatically
- e.g. python returns an empty result for out-of-range list slices
- overall, the best way to reduce bugs is to make software simpler
- mask exceptions
- exception detected and handled at a low level
- common in distributed systems
- e.g. network transport e.g. TCP
- doesn’t work in all situations, but powerful when it does
- example of pulling complexity downward
- exception aggregation
- handle many exceptions with a single piece of code
- instead of catching in the individual service methods, let them propagate up to the top-level dispatch
- if a system processes a series of requests, useful to define an exception that aborts the current request, cleans up system state, and continues with next request
- aggregation works best if exception propagates several levels up allowing more the be handled in one place
- just crash?
- e.g. not much an application can do if out of memory
- IO errors
- inconsistent data structures
- whether to crash depends on use case e.g. don’t crash on I/O error in a replicated storage system
- design special cases out of existence
- design the normal case in a way that automatically handles the special cases without any extra code
- taking it too far
- defining away exceptions only makes sense if the exception information isn’t needed outside the module
- in some cases, it is essential to expose the exceptions even if it adds complexity
- as with other areas of software design, must determine what is important and what isn’t
- conclusion
- special cases of any form make code harder to understand and increases the likelihood of bugs
- the best way to do this is by redefining semantics to eliminate error conditions
- if you can’t define away, try to mask at a low level so the impact is limited
- can also aggregate several special-case handlers into a single more general handler
- Design it Twice
- unlikely first thought will produce best design
- end up with better result if you consider multiple options
- try to pick approaches that are radically different from each other
- even if you are certain there is only one reasonable approach, consider anyway no matter how bad you think it is
- make a list of pros and cons
- the most important consideration for an interface is ease of use for higher level software
- sometimes none of the alternatives are attractive
- use problems you identified to try to drive new designs
- can use this technique for many layers e.g. interface first then implementation
- for implementation, most important things are simplicity and performance
- design time small compared to days/weeks of implementation
- better design will more than pay for time spent on it
- Why Write Comments? The Four Excuses
- Intro
- in-code documentation plays a crucial role in software design
- process of writing comments, if done correctly, will actually improve a system’s design
- viewed not universally shared
- inadequate documentation creates a huge and unnecessary drag on software development
- Good code is self-documenting
- there is a significant amount of design information that can’t be represented in code
- e.g. rationale for a particular design or conditions under which it makes sense to call a particular method
- for large systems not practical for users to read the code to learn the behavior
- having to learn how the code works from only reading it may be time-consuming and painful
- if a user must read the code to use it, there is no abstraction
- comments allow us to capture additional information that the caller needs
- if you want to use abstractions to hide complexity, comments are essential
- I don’t have time to write comments
- if de-prioritized, you’ll end up with no documentation
- consider an investment mindset
- comments get out of date and become misleading
- keeping up to date does not require an enormous effort
- large changes only required if large changes to the code
- code reviews are a great mechanism for detecting and fixing stale comments
- all the comments I have seen are worthless
- solvable problem
- writing solid documentation is not hard, once you know how
- benefits of well-written comments
- the overall idea behind comments is to capture information that was in the mind of the designer but couldn’t be represented in the code
- without documentation, future developers will have to guess at the developer’s original knowledge
- complexity manifests itself in change amplification, cognitive load, and unknown unknown
- comments help with the last two
- Comment Should Describe Things that Aren’t Obvious from the Code
- intro
- comments should describe things that aren’t obvious from the code
- a comment can make the rule explicit and clear
- developers should be able to understand the abstraction provided by a module without reading any code other than it’s externally visible declarations
- Pick conventions
- conventions ensure consistency and ensure you actually write comments
- types of comments;
- interface: immediately precedes the declaration of a module such as a class, data structure, function, etc. describes behavior, arguments, return value, side effects, exceptions, and other requirements
- data structure member: comment next to the declaration of a field in a data structure
- implementation comment: how the code works internally
- cross-module comment: describing dependencies that cross module boundaries
- First two most important
- every class should have an interface comment, every class variable should have a comment, and every method should have an interface comment
- implementation comments often unnecessary
- Don’t repeat the code
- ask yourself, could someone who has never seen the code write the comment just by looking at the code?
- use different words in the comment from those in the name of the entity being described
- lower-level comments add precision
- comments augment the code by providing information at a different level of detail
- lower level add precision and higher level add intuition
- precision useful when commenting variable declarations
- what are the units?
- boundary conditions? inclusive or exclusive?
- if null is permitted, what does that imply?
- if a variable refers to a resource that must be freed, who is responsible for freeing or closing it?
- are there certain properties that are always true for the variable (invariants)?
- when documenting a variable, think nouns (what does it represent) not verbs
- higher-level comments
- help the reader understand overall intent and structure
- more difficult to write
- great software designers can also step back from the details and think about a system at a higher level
- comments of the form “how we get here” are very useful for helping people understand the code
- interface documentation
- one of the most important roles for comments is to define abstractions
- if you want code that presents good abstractions, you must document those abstractions with comments
- separate interface comments from implementation comments
- so users are not exposed to implementation details
- if interface comments must also describe the implementation, then the class or method is shallow
- implementation comments
- the main goal of implementation comments is to help readers understand what the code is doing
- add a comment before each of the major blocks to provide a high-level (more abstract) description of what the block does
- describe what the code is doing and why
- document any tricky aspects of the code that won’t be obvious from reading it
- for longer methods, can be helpful to write comments for a few of the most important local variables
- if the variable is used over a large span of code, then you should consider adding a comment to describe the variable
- focus on what variable represents
- cross-module design decisions
- cross-module decisions are often complex and subtle and account for many bugs so good documentation for them is crucial
- the biggest challenge is finding a place to put it where it will naturally be discovered by developers
- one option is duplicating documentation, but awkward and hard to keep up to date
- another approach is keeping a document in a central file called “designNotes”
- any piece of code that relates to something in there can have a comment that references that document
- a disadvantage is that the documentation is not near the code so may be difficult to keep up-to-date
- conclusion
- when following the rule that comment should describe things that aren’t obvious, “obvious” is from the perspective of the reader
- if a reader thinks it’s not obvious, then it’s not obvious
- Choosing Names
- intro
- one of the most underrated aspects of software design
- good names are a form of documentation
- name choice is an example of the principle that complexity is incremental
- example: bad names cause bugs
- can cause mental blocks or force mind to see things in a certain way
- create an image
- the goal is to create an image in the mind of the reader about the nature of the thing being named
- names can become unwieldy if they contain more than two or three words
- names should be precise
- most common problem is names that are too generic or vague
- you might think they could relate to many different things
- if you find it difficult to come up with a name, that is a red flag
- suggests the variable may not have a clear definition or purpose
- use names consistently
- consistent naming reduces cognitive load
- 1) always use the common name for the given purpose
- 2) never use the common name for anything other than the given purpose
- 3) make sure that the purpose is narrow enough that all variables with the name have the same behavior
- sometimes will need multiple variables that refer to the same general sort of thing
- use the common name, but add a distinguishing prefix e.g. srcFileBlock or dstFileBlock
- a different opinion: Go style guide
- some Go developers argue names should be very short
- readability must be determined by readers not writers
- conclusion
- developing the skill for naming is also an investment
- as you get more experience, becomes easier
- get to point where it takes almost no extra time so you’ll get the benefits almost for free
- Write The Comments First
- the best time to write comments is at the beginning of the process
- delayed comments are bad comments
- often means they never get written at all
- write the comments first
- produces better comments
- if you write comments as you are designing the class, key design issues will be fresh in your mind
- comments are a design tool
- improves system design
- if you write comments describing the abstractions at the beginning, you can review and tune them before writing implementation code
- if variable or method requires a long comment, red flag
- early comments are fun comments
- comments are how I record and test the quality of my design decisions
- looking for the design that can be expressed completely and clearly in the fewest words
- simpler the comments, simpler the design
- are early comments expensive?
- writing the comments first will mean that the abstractions will be more stable
- conclusion
- if you haven’t tried, give it a try
- think about how it will affect the quality of your comments, design, and overall enjoyment of the software development process
- Modifying Existing Code
- stay strategic
- in strategic programming, the most important goal is to produce a great system design
- tactical programming leads to a messy system design
- typical mindset is what is the smallest possible change that I can make that does what I need?
- ideally, after the change, the system will have structure as if you designed it from the start with that change
- every development organization should plan to spend a small fraction of its total effort on clean up and refactoring
- maintaining comments: keep the comments near the code
- inaccurate comments are frustrating to the readers
- the best way to ensure they get updated is to position them close to the code they describe
- when writing implementation comments, spread them out
- comments belong in the code, not the commit log
- developers who need the information is unlikely to think to scan the repository logs
- most important to get into the code
- place documentation in the place where developers are most likely to see it
- maintaining comments: avoid duplication
- try to document each design decision exactly once
- don’t re-document one module’s design decisions into another module
- just reference the external documentation
- important that readers can easily find all the documentation needed to understand your code
- maintaining comments: check the diffs
- scan overall all the changes for that commit
- make sure each change is properly reflected in the documentation
- higher-level comments are easier to maintain
- do not reflect the details of the code
- Consistency
- intro
- if not consistent, developers must learn about each situation separately and take more time
- examples of consistency
- names, coding style, interfaces, design patterns, invariants
- ensuring consistency
- create a document that lists most important overall conventions
- write a tool that checks for violations
- code reviews offer another opportunity for enforcing the convention
- the more nit-picky, the more quickly everyone on the team will learn the conventions
- when in Rome, do as the Romans do
- don’t change existing conventions
- a lot of value in consistency
- reconsidering established conventions is rarely a good use of developer time
- taking it too far
- don’t try to force dissimilar things into the same approach
- conclusion
- investment mindset
- the code will be more obvious
- developers will be able to understand the code’s behavior more quickly and accurately
- work faster with fewer bugs
- Code Should be Obvious
- intro
- obscurity is one of the two main causes of complexity
- the solution is to write code in a way that makes it obvious
- if the code isn’t obvious, the reader must expend a lot of time and energy to understand it
- the best way to determine obviousness is through code reviews
- if someone reading your code says it’s not obvious, it’s not obvious
- things that make code more obvious
- choosing good names
- consistency
- judicious use of white space
- when you can’t avoid code that is nonobvious, use comments to provide missing information
- things that make code less obvious
- event-driven programming
- to compensate, use interface comment for each handler to indicate when it is invoked
- generic containers e.g. tuples
- if you need a container, define a new class or structure that is specialized for a particular use case
- software should be designed for ease of reading not ease of writing
- code that violates reader expectations e.g. starting processes that aren’t closed when the application main ends
- important to document these cases
- conclusion
- if code is nonobvious, there is important information about the code that the reader doesn’t have
- 1) use design techniques such as abstraction and eliminating special cases
- 2) take advantage of information that readers have already acquired in other contexts
- 3) present important information to them in code, using good names and strategic comments
- Software Trends
- Object-oriented programming and inheritance
- one of key elements is inheritance
- first form is interface inheritance
- parent class defines the signatures and subclass must implement
- providers leverage against complexity by reusing the same interface for multiple purposes
- second form is implementation inheritance
- creates dependencies
- results in information leakage and makes hard to modify one class in the hierarchy without looking at the others
- use implementation inheritance with caution
- first consider an approach based on composition
- if you have to use, try to separate the state managed by the parent class from that managed by the subclass
- OOP can assist in clean design, but does not guarantee it
- if classes are shallow, have complex interfaces, permit external access to internal state will still result in high complexity
- Agile development
- one of most important elements is that development should be incremental and iterative
- isn’t possible to visualize a complex system well enough at the outset of a project to determine the best design
- best way to end up with good design is to develop a system in increments where each increment adds a few new abstractions and refactors existing abstractions
- risk of agile is can lead to tactical programming leading to rapid accumulation of complexity
- developing incrementally is generally a good idea, but increments should be abstractions not features
- unit tests
- they facilitate refactoring
- without test suite, dangerous to make major structural changes
- can be more confident in making structural improvements
- test-driven development
- not a fan
- problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design
- one place where it makes sense is when fixing bugs
- make sure you can reproduce the failure so you know that you fixed it
- design patterns
- generally good, but need to use in the correct situations
- greatest risk is over-application
- using design patterns doesn’t automatically improve a software system
- getters and setters
- better not to expose instance variables in the first place
- exposed instance variables mean that a part of the class’s implementation is visible externally
- getters and setters are shallow methods
- conclusion
- whenever you encounter a proposal, challenge it from the standpoint of complexity
- does it help minimize complexity?
- Designing for Performance
- Intro
- the important idea is still simplicity
- how to think about performance
- if you try to optimize every statement for maximum speed, will slow down development and create a lot of unnecessary complexity
- on the other hand, if you completely ignore, can easily be 5-10x slower in “death by a thousand cuts”
- can be hard to come back alter and improve because of no single improvement to make
- the best approach is something between these extremes
- choose design alternatives that are naturally efficient
- the key is to develop an awareness of which operations are fundamentally expensive e.g. network communications, I/O, dynamic memory allocation, cache misses
- best way to learn which things are expensive is to run micro-benchmarks
- if the only way to improve efficiency is by adding complexity, then the choice more difficult
- if adds only a small amount of complexity and if complexity is hidden, so it doesn’t affect any interfaces, may be worthwhile
- if faster design adds a lot of implementation complexity, may be better to start off with the simpler approach and optimize later
- measuring before modifying
- don’t rush off and start making performance tweaks
- programmers’ intuitions about performance are unreliable
- this is true even for experienced developers
- first measure systems’ existing behavior
- the goal is to identify a small number of very specific places
- there’s no point in retaining complexity unless it provides a significant speedup
- design around the critical path
- the best way to improve its performance is with a “fundamental” change such as introducing a cache, or using a different algorithmic approach
- sometimes there isn’t a fundamental fix
- how to redesign an existing piece of code so it runs faster
- start off by asking yourself what is the smallest amount of code that must be executed to carry out the desired task in the common case
- consider only the data needed for the critical path
- remove special cases from the critical path
- each special case adds a little bit of code to the critical path
- ideally, have a single if statement at the beginning which detects all special cases
- conclusion
- clean design and high performance are compatible
- Conclusion
- this book is about complexity
- dealing with complexity is the most important challenge in software design
- offered some general ideas: deep and generic classes, define errors out of existence, and separate interface and implementation documentation
- the downside is can create extra work in early stages
- if you aren’t used to it, can slow you down
- design is a fascinating puzzle
- how can I solve a particular problem with the simplest possible structure?
- clean, simple, and obvious design is a beautiful thing
- investments you make will pay off quickly in the future
- as you hone your design skills, you will find you can produce good designs more and more quickly
- the reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun
- poor designers spend most of their time chasing bugs in complicated and brittle code
- if you improve your design skills, not only will you produce higher quality software, but the software development process will be more enjoyable
Like this:
Like Loading...