Day by day, software programmers make all kinds of little design decisions. Software architects may determine the high-level organization of a system and its key abstractions - but high quality software depends on those implementing it making good choices too. Individually these choices are small, however put together they have a large impact on software quality.Mer information & fakta
It's not easy to make all of these small, but significant, design choices - in no small part because there are so many factors. How does modern hardware actually work? What does the CLR do with .NET bytecode? Does encapsulation and abstraction have a cost? How does the behavior of the garbage collector impact programs? What data structures are appropriate in what situations? How can we actually use OO effectively? When are mutable and immutable designs preferable? How can software be extensible and composable? How should dates and times be handled?
There are courses that teach you software architecture at a high level, others that teach you how to use a programming language and its libraries effectively, and yet more covering specific frameworks or tools. Instead, this course takes on the gritty, tricky, day-to-day choices that developers are expected to make, as they go about the heroic task of transforming usually ill-defined and ever-changing needs into working software that solves problems. It juggles a dozen topics. Because that's exactly what good software developers have to do.
You're already a decent C# programmer - but you know there's more to software than writing the code. You know you have to make dozens of little decisions in your day to day work: about performance trade-offs, data structures, and OO design. And you'd like to deepen your knowledge in a range of areas to help you make better decisions.
This course assumes that participants are able to follow and write code using modern C# language features, including Linq and lambda expressions. Taking our C# Intermediate course is suitable preparation, otherwise you should have at least the skills covered by the C# Intermediate course.
The environment we build for
It's important to understand what we're running on, and what we're building in terms of. This section takes a deep dive down to the hardware, and then back up through the .NET CLR.
- CPU essentials: pipelining, micro-ops, branches, data dependencies, instruction level parallelism
- Memory essentials: virtual memory, CPU caches, cache lines
- The nature of .Net IL and JIT compilation
- The cost of calls: inlining, interface call optimization, lambdas
- Cost of field accesses
- Cost of exceptions
Garbage Collection and Resource Management
Programs need to store information as they execute. Managing memory is a decades-old problem. The CLR offers garbage collection, which takes away many potential opportunities for mistakes - but is certainly not magical. In this section its behavior and expectations are considered, along with the need to manage things that have limited lifetime, but are not good to leave to the GC.
- Exploring the stack
- Exploring the heap
- Value types vs. reference types
- The challenge garbage collection takes on
- The illusion of resizing
- Boxing and unboxing
- The effect of generational GC
- The large object heap
- Local to heap promotion
- The disposable pattern
Selecting Data Structures
There are a huge number of built-in data types in the .Net framework. Many are useful, but rarely used. Furthermore, there are a number of data structures not found in the class library that are incredibly useful, can be built with relative ease or found on NuGet, and are widely recognized and formalized. Choosing appropriate data structures is a huge part of effective program design.
- A quick guide to time complexity
- A small guide to space complexity
- The importance of constant factors
- Choosing appropriate numeric types
- List vs. Array
- When to turn to linked lists, queues, and stacks
- Dictionaries, and making objects suitable for use as keys
- When bits are enough
The idea of objects has been around for decades, yet understanding of their power is still very unevenly distributed. Programming languages giving the illusion that classes are really just containers for procedures or useful for building record types has not helped matters. In this section we'll look at how objects can be used more effectively, and how understanding messaging is at the heart of good OO design.
- The origins of object orientation
- Encapsulation, messaging, and late binding
- Method-first object design
- The importance of invariants
- Introducing late binding
- Tell, don't ask
- Properties: the good and the bad
- OO and performance
Mutability, Immutability, and what lies between
For years, imperative programming - centered around manipulating state - has been contrasted with functional programming - centered around computing values. The former is sometimes said to perform better and be easier to learn, while the latter is said to produce code that is easier to reason about, compose, and parallelize. Both have something to offer.
- Mutability: the good and the bad
- Immutability: the good and the bad
- Value objects
- Study: Linq queries vs. imperative code
- The immutable collections, and when to consider them
- Persistent data structures
Extensibility and Composability
Over time, needs change. We have to grow our software, and we'd like to try and minimize risk as we do so. Since changing working code is risky, we tend to prefer designs that allow extension without modification. We sometimes also need designs that let us compose building blocks together in a safe and simple way. This section considers some strategies for achieving these goals.
- Examples of extensibility
- Examples of composability
- Inheritance vs. composition
- Inversion of control
- A composable approach
Coping with, and tolerating, failures
Our software often must work with inherently unreliable components. Networks can fail, we can be handed corrupt data, databases can pick us as transaction deadlock victims, and so forth. How can we cope?
- Sources of failure
- Exceptions: when to use them and when to avoid them
- Domain exceptions
- Validation: domain and UI
- Kinds of exception handler
- Transient failures and retries
- Persist stuff early
Working with time
Time is a gnarly domain. Humans talk plenty about dates and times, but often without precision and relying on context to resolve ambiguities. To make things worse, the .Net date/time API makes it all to easy for these ambiguities to make their way into software - often leading to difficult bugs. How can we do better?
- Instantaneous time and elapsed time
- DateTime and TimeSpan
- Local times, timezones, and DST
- The pain of DateTime.Now
- Floating dates and times
- Sending messages to the future
Measuring and logging
Feedback loops are essential to a designer. These can occur at many levels. Key to all of them is ensuring the data obtained is reliable and complete enough.
- Feedback cycles and staying on course
- Benchmarking and its pitfalls
- Profiling time and memory
- Performance counters
- A few thoughts on logging
After looking at a bunch of ideas for producing good designs, we'll spend a little time considering some problems to be on the watch for.
- Changes touching many places in the code
- ThingyManager, DoodahService, getters/setters everywhere, etc.
- Comment deodorant
- Surprise mutation
- Repeated mistakes