Software Design and Architecture Roadmap
Topics
- Clean Code
- Programming Paradigms
Resources:
- How to Learn Software Design and Architecture | The Full-stack Software Design & Architecture Map
- Software Design and Architecture Roadmap
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Refactoring, by Martin Fowler (2nd edition)
- The Pragmatic Programmer, by Andy Hunt and Dave Thomas
- The Design of Everyday Things, by Don Norman
Content
- Use Intention-Revealing Names
- Avoid Disinformation
- Make Meaningful Distinctions
- Use Pronounceable Names
- Use Searchable Names
- Avoid Encodings
- Avoid Mental Mapping
- Class Names
- Method Names
- Don’t Be Cute
- Pick One Word per Concept
- Don’t Pun
- Use Solution Domain Names
- Use Problem Domain Names
- Don’t Add Gratuitous Context
- Resources
Use Intention-Revealing Names
The name of a variable, function, or class, should answer all the big questions. It should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent.
Example 1
Let’s say we want to define a variable to store elapsed time in days.
// No question is answered.
int d; // elapsed time in days
// The name has now more sense.
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
Example 2
Let’s try to analyze what’s the purpose of this code:
List<int[]> theList = new();
public List<int[]> GetThem() {
List<int[]> list1 = new();
foreach (int[] x in theList)
{
if (x[0] == 4)
{
list1.Add(x);
}
}
return list1;
}
There are some questions we need to answer, so we can understand the code:
- What kinds of things are in
theList
? - What is the significance of the zeroth subscript of an item in
theList
? - What is the significance of the value
4
? - How would I use the list being returned?
Let’s say we have the following context:
- We’re working in a mine sweeper game.
- We find that the board is a list of cells called
theList
. Let’s rename that togameBoard
. - Each cell on the board is represented by a simple array.
- We further find that the zeroth subscript is the location of a
status
value and that a status value of4
meansflagged
.
Just by giving these concepts names we can improve the code considerably:
List<int[]> gameBoard = new();
const int StatusValue = 0;
const int Flagged = 4;
public List<int[]> GetFlaggedCells() {
List<int[]> flaggedCells = new();
foreach (int[] cell in gameBoard)
{
if (cell[StatusValue] == Flagged)
{
flaggedCells.Add(cell);
}
}
return flaggedCells;
}
The simplicity of the code has not changed, but the code has become much more explicit.
There’s a chance to make it even clearer by creating a class for cells instead of using array of int
:
List<int[]> gameBoard = new();
public class Cell
{
private int Status { get; set; }
public bool IsFlagged()
{
... // The logic to handle each status goes here.
}
}
public List<Cell> GetFlaggedCells() {
List<Cell> flaggedCells = new();
foreach (Cell cell in gameBoard)
{
if (cell.IsFlagged())
{
flaggedCells.Add(cell);
}
}
return flaggedCells;
}
Avoid Disinformation
Programmers must avoid leaving false clues that obscure the meaning of code.
Example 1
Do not use abbreviation.
int hp;
string aix;
bool sco;
Example 2
Do not refer to a grouping of accounts as an accountList
unless it’s actually a List.
Dictionary<Account> accountList = new(); // If the container holding the accounts is not actually a List, it may lead to false conclusions.
// A better way to name the variable would be:
Dictionary<Account> accountGroup = new();
Dictionary<Account> bunchOfAccounts = new();
Dictionary<Account> accounts = new();
Example 3
How long does it take to spot the subtle difference between the following variables:
string XYZControllerForEfficientHandlingOfStrings;
string XYZControllerForEfficientStorageOfStrings;
Spelling similar concepts similarly is information. Using inconsistent spellings is disinformation. It is very helpful if names for very similar things sort together alphabetically and if the differences are very obvious
Example 4
A truly awful example of disinformative names would be the use of lower-case L or uppercase O as variable names, especially in combination.
int a = l;
if ( O == l )
{
a = O1;
}
else
{
l = 01;
}
The problem, of course, is that they look almost entirely like the constants one and zero, respectively.
Make Meaningful Distinctions
If names must be different, then they should also mean something different.
Example 1
Number-series naming (a1, a2, .. aN) is the opposite of intentional naming.
// a1 and a2 variables doesn't have any meaningful distinction.
public static void CopyChars(char[] a1, char[] a2) {
for (int i = 0; i < a1.Length; i++) {
a2[i] = a1[i];
}
}
// If we call them source and destination, they mean something different.
public static void CopyChars(char[] source, char[] destination) {
for (int i = 0; i < source.Length; i++) {
destination[i] = source[i];
}
}
Example 2
Noise words are another meaningless distinction.
public class Product {
...
}
public class ProductInfo { // This class name doesnt have a different meaning than Product class
...
}
public class ProductData { // This class name doesnt have a different meaning than Product class
...
}
Example 3
Some other recommendations.
// Wrong use of some words.
string NameVariable;
string NameString;
string[] UsersTable;
Customer customerObject = new();
CustomerObject customer = new();
// Avoid using type words.
string Name;
string[] Users;
Customer customer = new();
Use Pronounceable Names
Make your names pronounceable. If you can’t pronounce it, you can’t discuss it without sounding like an idiot.
Example
// Wrong use pronounceable names.
class DtaRcrd102 {
private DateTime Genymdhms;
private DateTime Modymdhms;
private String Pszqint = "102";
...
};
// Meaninful names and easy to pronounce.
class Customer {
private DateTime GenerationTimestamp;
private DateTime ModificationTimestamp;
private String RecordId = "102";
};
Use Searchable Names
If a variable or constant might be seen or used in multiple places in a body of code, it is imperative to give it a search-friendly name.
Example
// Short variable names which are not useful for searching.
int s = 0;
int t[] = [1, 2, 3, 4, 5, 6, ...];
for (int j=0; j<34; j++) {
s += (t[j]*4)/5;
}
// Meaninful names and are useful for searching.
int realDaysPerIdealDay = 4;
int realdays = 3;
const int WORK_DAYS_PER_WEEK = 5;
const int NUMBER_OF_TASKS = 34;
int sum = 0;
int taskEstimate[] = [1, 2, 3, 4, 5, 6, ...];
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
Avoid Encodings
Encoding type or scope information into names simply adds an extra burden of deciphering.
Hungarian Notation
Hungarian Notation is a naming convention in programming where variable and function names include information about their type, purpose, or intended use.
// Given we have a phone number variable in string type, we declare the following:
string phoneString;
// We then change the type to Phone Number:
PhoneNumber phoneString;
// The name will not changed when type changed!
Member Prefixes
You also don’t need to prefix member variables with m_ anymore.
// Using a short name for description. You need to have a comment explaining the context of the variable.
public class Part {
private string m_dsc; // The textual description
void SetName(String name) {
m_dsc = name;
}
}
// Using a meaninful name for description. It's not required any extra comment to explain what does the variable is used to.
public class Part {
string Description;
void SetDescription(String description) {
this.description = description;
}
}
Interfaces and Implementations
Interfaces and implementations will have the same name, so we need to somehow difference them.
public interface IAnimal { ... }
public class Animal : IAnimal { ... }
Avoid Mental Mapping
Readers shouldn’t have to mentally translate your names into other names they already know. This problem generally arises from a choice to use neither problem domain terms nor solution domain terms.
string[] groups = [ .... ];
string[] people = [ .... ];
// Using traditional i and j variables, so we need to have a mental mapping that i is iterating groups and j is iterating people.
for (int i = 0; i < groups.Length; i++) {
for (int j = 0; j < people.Length; j++) {
...
}
}
// If we use meaninful names for groupId and personId, then we don't need to have a mental mapping.
for (int groupId = 0; groupId < groups.Length; groupId++) {
for (int personId = 0; personId < people.Length; personId++) {
...
}
}
Class Names
// Classes and objects should have noun or noun phrase names.
public class Customer { ... }
public class WikiPage { ... }
public class Account { ... }
public class AddressParser { ... }
// Avoid words like Manager, Processor, Data, or Info in the name of a class.
public class CustomerData { ... }
public class CustomerInfo { ... }
// A class name should not be a verb.
public class Eating { ... }
Method Names
Methods should have verb or verb phrase names.
public void PostPayment() { ... }
public void DeletePage() { ... }
public void Save() { ... }
public void IsPosted() { ... }
Don't Be Cute
If names are too clever, they will be memorable only to people who share the author’s sense of humor, and only as long as these people remember the joke.
// Cute but not meaninful.
public void HolyHandGrenade() { ... }
// Meaninful named method.
public void DeleteItems() { ... }
Pick One Word per Concept
Pick one word for one abstract concept and stick with it.
// Using different names for the same purpose.
public class UserService {
public List<User> Fetch() { ... }
}
public class GroupService {
public List<Group> Retrieve() { ... }
}
public class TaskService {
public List<Task> Get() { ... }
}
// Using one same name for the same purpose.
public class UserService {
public List<User> Get() { ... }
}
public class GroupService {
public List<Group> Get() { ... }
}
public class TaskService {
public List<Task> Get() { ... }
}
Don't Pun
Avoid using the same word for two purposes. Using the same term for two different ideas is essentially a pun.
// Using the same name for diffent purposes.
public class UserService {
public void Add(User user) { ... } // This method let us create a new user.
}
public class GroupService {
public void Add(Group group, User user) { ... } // This method let us insert a user into a group.
}
// Using the appropriate name for each purpose.
public class UserService {
public void Create(User user) { ... } // This method let us create a new user.
}
public class GroupService {
public void Insert(Group group, User user) { ... } // This method let us insert a user into a group.
}
Use Solution Domain Names
Remember that the people who read your code will be programmers. So go ahead and use computer science (CS) terms, algorithm names, pattern names, math terms, and so forth.
- The name
AccountVisitor
means a great deal to a programmer who is familiar with the VISITOR pattern. - What programmer would not know what a
JobQueue
was?
There are lots of very technical things that programmers have to do. Choosing technical names for those things is usually the most appropriate course.
Use Problem Domain Names
If there are no programming terms, we can use terms related to the feature or business.
Example
public class Account {
private string AccountBalance; // This variable name has no programming terms, so it's related to the bussiness.
}
Separating solution and problem domain concepts is part of the job of a good programmer and designer. The code that has more to do with problem domain concepts should have names drawn from the problem domain.
Add Meaningful Context
There are a few names which are meaningful in and of themselves—most are not. Instead, you need to place names in context for your reader by enclosing them in well-named classes, methods, functions, etc.
// We have several fields that doesn't have a clear context
public class User {
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public int HouseNumber { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zipcode { get; set; }
}
User user = new();
// In this context, we only know the State property is part of the user.
Console.WriteLine(user.State);
// In this context, we only know the State propertyis part of the user's address
Console.WriteLine(user.Street);
Console.WriteLine(user.HouseNumber);
Console.WriteLine(user.City);
Console.WriteLine(user.State);
//
public class Address {
public string Street { get; set; }
public int HouseNumber { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zipcode { get; set; }
}
public class User {
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
User user = new();
// In this context, we do know the State property is part of the user's address.
Console.WriteLine(user.Address.State);
Don't Add Gratuitous Context
In an imaginary application called “Gas Station Deluxe,” it is a bad idea to prefix every class with GSD. Frankly, you are working against your tools. You type G and press the completion key and are rewarded with a mile-long list of every class in the system. Is that wise? Why make it hard for the IDE to help you?
// Likewise, say you invented a "MailingAddress" class in GSD's accounting module, and you named it "GSDAccountAddress"
public class GSDAccountAddress { ... }
// Later, you need a mailing address for your customer contact application. Do you use "GSDAccountCustomer"? Does it sound like the right name? Some characters are redundant or irrelevant.
public class GSDAccountCustomer { ... }
// The names "accountAddress" and "customerAddress" are fine names for instances of the class "Address" but could be poor names for classes.
public class Address { ... }
Address accountAddress = new();
Address customerAddress = new();
Shorter names are generally better than longer ones, so long as they are clear. Add no more context to a name than is necessary.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Identifier name - rules and conventions - C# | Microsoft Learn
- .NET Coding Conventions - C# | Microsoft Learn
- Clean Code in C# Part 1 Meaningful Names - DEV Community
Content
- Small
- Do One Thing
- One Level of Abstraction per Function
- Switch Statements
- Use Descriptive Names
- Function Arguments
- Have No Side Effects
- Command Query Separation
- Prefer Exceptions to Returning Error Codes
- Don’t Repeat Yourself
- Structured Programming
- Resources
Rules of functions
It’s importance to create small, focused, and single-responsibility functions that are easy to understand, test, and maintain.
Small
- The first rule of functions is that they should be small. They should hardly ever be 20 lines long.
- This implies that the blocks within
if
statements,else
statements,while
statements, and so on should be one line long. Probably that line should be a function call. - This also implies that functions should not be large enough to hold nested structures. Therefore, the indent level of a function should not be greater than one or two.
Do One Thing
- Functions should do one thing, should do it well, and should do it only.
- Functions should not be able to be divided into more sections/functions.
One Level of Abstraction per Function
- To make sure the functions are doing “one thing”, we need to make sure that the statements within the function are all at the same level of abstraction.
- We can follow the “The Stepdown Rule”. It’s all about reading code from top to bottom. This way each function introduces the next and make the code readable from top to bottom.
Switch Statements
- It’s hard to make a small
switch
statement. - It violates the Single Responsibility Principle (SRP) because there is more than one reason for it to change.
- It violates the Open Closed Principle8 (OCP) because it must change whenever new cases are added.
- They can be tolerated if they appear only once, are used to create polymorphic objects, and are hidden behind an inheritance
Use Descriptive Names
- Function name should describes what the function does.
- The smaller and more focused a function is, the easier it is to choose a descriptive name.
- Don’t be afraid to make a name long. A long descriptive name is better than a short enigmatic name.
Function Arguments
- The ideal number of arguments for a function is zero.
- Three arguments should be avoided where possible.
- Arguments should make sense with the function name. There are two very common reasons to pass a single argument into a function.
- You may be asking a question about that argument.
- Or you may be operating on that argument, transforming it into something else and returning it.
- Avoid using Flag Arguments.
- Use object arguments.
// In this case, the first two arguments seem to be related.
Circle MakeCircle(double x, double y, double radius);
// We can abstract x and y into Point class, and have one instance instead.
Circle MakeCircle(Point center, double radius);
- Function and arguments should form a very nice verb/noun pair.
Have No Side Effects
- Your function promises to do one thing, so it should not do other hidden things.
Command Query Separation
- Functions should either do something or answer something, but not both.
Prefer Exceptions to Returning Error Codes
- Use try/catch to handle error instead of returning error codes as exceptions can be added without forcing any recompilation or redeployment.
// Returning error codes from specific actions.
if (DeletePage(page) == StatusOk)
{
if (registry.DeleteReference(page.name) == StatusOk)
{
if (configKeys.DeleteKey(page.name.makeKey()) == StatusOk)
{
Console.WriteLine("page deleted");
}
else
{
Console.WriteLine("configKey not deleted");
}
}
else
{
Console.WriteLine("deleteReference from registry failed");
}
}
else
{
Console.WriteLine("delete failed");
return E_ERROR;
}
// We can replace the nested if statements with using try/catch.
try {
DeletePage(page);
registry.DeleteReference(page.name);
configKeys.DeleteKey(page.name.makeKey());
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
- Functions should do one thing. Error handing is one thing.
Don’t Repeat Yourself
- In case you find some code that is being used, try to separate it into a reusable function.
Structured Programming
- Edsger Dijkstra’s rule says “every function and every block within a function should have one entry and one exit”.
- Following these rules means that there should only be one return statement in a function, no
break
orcontinue
statements in a loop, and never, ever, anygoto
statements. - This rule provides significant benefit in large functions only.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 2 Methods - DEV Community
Content
- Rules of comments
- Good comments
- Bad comments
- Resources
Rules of comments
Truth can only be found in one place: the code. Only the code can truly tell you what it does. It is the only source of truly accurate information. Therefore, though comments are sometimes necessary, we will expend significant energy to minimize them.
One of the more common motivations for writing comments is bad code.
Clear and expressive code with few comments is far superior to cluttered and complex code with lots of comments.
// Using comments to explain the code:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) { ... }
// Use the code to explain the behavior:
if (employee.isEligibleForFullBenefits()) { ... }
Good comments
Some comments are necessary or beneficial.
Legal Comments
- Copyright and authorship statements are necessary and reasonable things to put into a comment at the start of each source file.
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.
Informative Comments
- It is sometimes useful to provide basic information with a comment. For example, adding a comment to explain return value of an abstract method.
// Returns an instance of the Responder being tested.
protected abstract Responder responderInstance();
Explanation of Intent
- Sometimes a comment goes beyond just useful information about the implementation and provides the intent behind a decision.
Example
- When comparing two objects, the author decided that he wanted to sort objects of his class higher than objects of any other.
public int compareTo(Object o)
{
...
return 1; // we are greater because we are the right type.
}
Clarification
- Sometimes it is just helpful to translate the meaning of some obscure argument or return value into something that’s readable.
- Comments can be useful when the code is part of the standard library, or in code that you cannot.
public string CompareStrings( string str1, string str2 )
{
// Compare the values, using the CompareTo method on the first string.
int cmpVal = str1.CompareTo(str2);
if (cmpVal == 0)
{
// str1 == str2
return 0;
}
else if (cmpVal < 0)
{
// str1 < str2
return -1;
}
else
{
// str1 > str2
return 1;
}
}
Warning of Consequences
- Sometimes it is useful to warn other programmers about certain consequences.
// WARNING: This method directly modifies the database.
// Any changes here will be permanent and irreversible.
public void UpdateDatabase(Data data)
{
// Database update logic here
}
TODO Comments
- It is sometimes reasonable to leave “To do” notes in the form of //TODO comments.
- TODO comments let the team know that something needs to be done.
- TODOs are jobs that the programmer thinks should be done, but for some reason can’t do at the moment.
// TODO: Implement error handling for database connection
public void ConnectToDatabase(string connectionString)
{
// ... code to connect to the database ...
}
Amplification
- A comment may be used to amplify the importance of something that may otherwise seem inconsequential.
string personName = " FirstName Lastname ";
string[] fullName = fullname.Trim().Split(' ');
// the trim is real important. It removes the starting and ending spaces that could cause the user instance to have unnecessary spaces in its parts.
User user = new();
user.FirstName = fullName[0];
user.LastName = fullName[1];
Public APIs
- If you are writing a public API, then you should certainly write good comments for it.
- You can use javadocs for Java code or jsdocs for JavaScript.
Bad comments
Most comments fall into this category. Usually they are crutches or excuses for poor code or justifications for insufficient decisions, amounting to little more than the programmer talking to himself.
Some examples
- Mumbling: Plopping in a comment just because you feel you should or because the process requires it, is a hack.
- Redundant Comments: The comment explains the same as you can deduce from function, variables, etc. It probably takes longer to read than the code itself.
- Misleading Comments: You find misinformation from the comment.
- Mandated Comments: It is just plain silly to have a rule that says that every variable must have a comment.
- Journal Comments: Sometimes people add a comment to the start of a module every time they edit it. Something like a changelog.
- Noise Comments: Sometimes you see comments that are nothing but noise. They restate the obvious and provide no new information.
- Position Markers: Sometimes programmers like to mark a particular position in a source file.
- Closing Brace Comments: Sometimes programmers will put special comments on closing braces.
- Attributions and Bylines: Source code control systems are very good at remembering who added what, when. There is no need to pollute the code with little bylines.
- Commented-Out Code: Few practices are as odious as commenting-out code. Don’t do this!
- Too Much Information: Don’t put interesting historical discussions or irrelevant descriptions of details into your comments.
- Inobvious Connection: The connection between a comment and the code it describes should be obvious.
- Function Headers: Short functions don’t need much description. A well-chosen name for a small function that does one thing is usually better than a comment header.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 3 Comments - DEV Community
Content
- Rules of formatting
- Vertical Formatting
- Horizontal Formatting
- Resources
Rules of formatting
The Purpose of Formatting
Code formatting is about communication, and communication is the professional developer’s first order of business.
Vertical Formatting
According to Robert C. Martin example, files long preferred to be 200 lines with an upper limit of 500 lines long.
Although this should not be a hard and fast rule, it should be considered very desirable. Small files are usually easier to understand than large files are.
The Newspaper Metaphor
Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story, hiding all the details while giving you the broad-brush concepts. As you continue downward, the details increase until you have all the dates, names, quotes, claims, and other minutia.
Having this example put on code formatting, we can think the following:
- File name should be simple but explanatory. The name, by itself, should be sufficient to tell us whether we are in the right module or not.
- The topmost parts of the source file should provide the high-level concepts and algorithms.
- Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file.
Vertical Openness Between Concepts
Nearly all code is read left to right and top to bottom. Each line represents an expression or a clause, and each group of lines represents a complete thought. Those thoughts should be separated from each other with blank lines.
Vertical Density
If openness separates concepts, then vertical density implies close association. So lines of code that are tightly related should appear vertically dense. For example, in a class, each property will follow the previous one, then we place a blank line to separate properties from methods.
Vertical Distance
Concepts that are closely related should be kept vertically close to each other. Clearly this rule doesn’t work for concepts that belong in separate files. But then closely related concepts should not be separated into different files unless you have a very good reason. Indeed, this is one of the reasons that protected variables should be avoided.
- Variable Declarations: Variables should be declared as close to their usage as possible.
- Instance variables: They should be declared at the top of the class. This should not increase the vertical distance of these variables.
- Dependent Functions: If one function calls another, they should be vertically close, and the caller should be above the callee, if at all possible. Constants should be kept at an appropriate level; therefore, we pass them from the place where it makes sense to know it to the low-level function where it is actually used.
- Conceptual Affinity: The stronger the affinity the less vertical distance there should be between them. This affinity could be direct dependence “One function calling another, or A group of functions perform similar operation”, or it could be conceptual affinity “Share a common naming scheme or Perform variation of the same basic task”.
Vertical Ordering
In general we want function call dependencies to point in the downward direction. That is, a function that is called should be below a function that does the calling. This creates a nice flow down the source code module from high level to low level.
Horizontal Formatting
Programmers clearly prefer short lines. The old Hollerith limit of 80 is a bit arbitrary, and there should not be a problem to lines edging out to 100 or even 120. But beyond that is probably just careless.
Horizontal Openness and Density
Use horizontal white space to associate things that are strongly related and disassociate things that are weakly related.
- Assignment operators surrounded with white space.
string name = "Fernando";
- No space between function name and the opening parenthesis while placing space between the closing parenthesis and the curly brackets:
GetFullName() { ... }
- Accentuate the precedence of operators.
CalculateTotal()
{
// The factors have no white space between them because they are high precedence.
// The terms are separated by white space because addition and subtraction are lower precedence.
return 4*4 + 3*a*c;
}
Indentation
A source file is a hierarchy rather like an outline. Each level of this hierarchy is a scope into which names can be declared and in which declarations and executable statements are interpreted.
To make this hierarchy of scopes visible, we indent the lines of source code in proportion to their position in the hiearchy.
- Statements at the level of the file, such as most class declarations, are not indented at all.
- Methods within a class are indented one level to the right of the class.
- Implementations of those methods are implemented one level to the right of the method declaration.
- Block implementations are implemented one level to the right of their containing block
- And so on … but we never ever break indentation.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 4 Formatting - DEV Community
Content
Rules of functions
Resources
Objects and Data Structures
Data Abstraction
Abstraction is the process of hiding implementation details and exposing only the essential features of a system or component. As well, using proper names that don’t expose the implementation details.
Data/Object Anti-Symmetry
- Objects hide their data behind abstractions and expose functions that operate on that data.
- Data structure expose their data and have no meaningful functions.
// Shared class
public class Point {
public double x;
public double y;
}
// Procedural Code
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public double PI = 3.141592653589793;
public double area(Object shape)
{
if (shape is Square) {
Square s = (Square)shape;
return s.side * s.side;
}
else if (shape is Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
}
else if (shape is Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
else
{
return 0;
}
}
}
// OO Code
public interface Shape
{
double area();
}
public class Square : Shape {
private Point topLeft;
private double side;
public double area() {
return side*side;
}
}
public class Rectangle : Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle : Shape {
private Point center;
private double radius;
public double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
Procedural
code (code using data structures) makes it easy to add new functions without changing the existing data structures.OO
code, on the other hand, makes it easy to add new classes without changing existing functions.Procedural
code makes it hard to add new data structures because all the functions must change.OO
code makes it hard to add new functions because all the classes must change.
The Law of Demeter
There is a well-known heuristic called the Law of Demeter that says a module should not know about the innards of the objects it manipulates.
More precisely, the Law of Demeter says that a method f of a class C should only call the methods of these:
- C (Same class)
- An object created by f (Inside the same method)
- An object passed as an argument to f
- An object held in an instance variable of C
Violation of the Law of Demeter
- Train Wrecks: In objects, avoid excessive chaining of method calls. It is usually best to split them up.
- Hybrids: This confusion sometimes leads to unfortunate hybrid structures that are half object and half data structure. Such hybrids make it hard to add new functions but also make it hard to add new data structures.
- Hiding Structure: Encapsulate implementation details.
Data Transfer Objects (DTO)
Data Transfer Objects (DTOs) are objects used to transfer data between different layers or components of a software system. DTOs are very useful structures, especially when communicating with databases or parsing messages from sockets, and so on. They often become the first in a series of translation stages that convert raw data in a database into objects in the application code.
Active Records are special format of DTOs as they have navigational methods like save and find.
public class Address {
private string Street { get; };
private string StreetExtra { get; };
private string City { get; };
private string State { get; };
private string Zip { get; };
public Address(
string street,
string streetExtra,
string city,
string state,
string zip)
{
Street = street;
StreetExtra = streetExtra;
City = city;
State = state;
Zip = zip;
}
}
Active Record
Active Records are special forms of DTOs. They are data structures with public (or beanaccessed) variables; but they typically have navigational methods like save and find. Typically these Active Records are direct translations from database tables, or other data sources.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 4 Formatting - DEV Community
Content
- Use Intention-Revealing Names
- Avoid Disinformation
- Make Meaningful Distinctions
- Use Pronounceable Names
- Use Searchable Names
- Avoid Encodings
- Avoid Mental Mapping
- Class Names
- Method Names
- Don’t Be Cute
- Pick One Word per Concept
- Don’t Pun
- Use Solution Domain Names
- Use Problem Domain Names
- Don’t Add Gratuitous Context
- Resources
Overview
The first paradigm to be adopted (but not the first to be invented) was structured programming, which was discovered by Edsger Wybe Dijkstra in 1968. Dijkstra showed that the use of unrestrained jumps (goto statements) is harmful to program structure. As we’ll see in the chapters that follow, he replaced those jumps with the more familiar if/then/else and do/while/until constructs.
We can summarize the structured programming paradigm as follows:
Structured programming imposes discipline on direct transfer of control.
Proof
The problem that Dijkstra recognized, early on, was that programming is hard, and that programmers don’t do it very well.
Dijkstra’s solution was to apply the mathematical discipline of proof. His vision was the construction of a Euclidian hierarchy of postulates, theorems, corollaries, and lemmas.
Same as mathematicians do, programmers should use proven structures, and tie them together with code that they would then prove correct themselves.
During his investigation, Dijkstra discovered that certain uses of goto statements prevent modules from being decomposed recursively into smaller and smaller units, thereby preventing use of the divide-and-conquer approach necessary for reasonable proofs.
Other uses of goto, however, did not have this problem. Dijkstra realized that these “good” uses of goto corresponded to simple selection and iteration control structures such as if/then/else
and do/while
. Modules that used only those kinds of control structures could be recursively subdivided into provable units.
This discovery was remarkable: The very control structures that made a module provable were the same minimum set of control structures from which all programs can be built. Thus structured programming was born.
As computer languages evolved, the goto statement moved ever rearward, until it all but disappeared.
At the end, there was no formal proof, and the Euclidean hierarchy of theorems was never built.
Functional Decomposition
Structured programming allows modules to be recursively decomposed into provable units, which in turn means that modules can be functionally decomposed.
By following these disciplines, programmers could break down large proposed systems into modules and components that could be further broken down into tiny provable functions.
Scientific Method
Given there was no formal proof, scientific method was then considered.
Science is fundamentally different from mathematics, in that scientific theories and laws cannot be proven correct, but can demonstrated in several ways.
Science does not work by proving statements true, but rather by proving statements false.
Resources
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design, by Robert C. Martin
Content
- Use Intention-Revealing Names
- Avoid Disinformation
- Make Meaningful Distinctions
- Use Pronounceable Names
- Use Searchable Names
- Avoid Encodings
- Avoid Mental Mapping
- Class Names
- Method Names
- Don’t Be Cute
- Pick One Word per Concept
- Don’t Pun
- Use Solution Domain Names
- Use Problem Domain Names
- Don’t Add Gratuitous Context
- Resources
Overview
The second paradigm to be adopted was actually discovered two years earlier, in 1966, by Ole Johan Dahl and Kristen Nygaard. These two programmers noticed that the function call stack frame in the ALGOL language could be moved to a heap, thereby allowing local variables declared by a function to exist long after the function returned. The function became a constructor for a class, the local variables became instance variables, and the nested functions became methods. This led inevitably to the discovery of polymorphism through the disciplined use of function pointers.
We can summarize the object-oriented programming paradigm as follows:
Object-oriented programming imposes discipline on indirect transfer of control.
Resources
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design, by Robert C. Martin
Content
- Use Intention-Revealing Names
- Avoid Disinformation
- Make Meaningful Distinctions
- Use Pronounceable Names
- Use Searchable Names
- Avoid Encodings
- Avoid Mental Mapping
- Class Names
- Method Names
- Don’t Be Cute
- Pick One Word per Concept
- Don’t Pun
- Use Solution Domain Names
- Use Problem Domain Names
- Don’t Add Gratuitous Context
- Resources
Overview
The third paradigm, which has only recently begun to be adopted, was the first to be invented. Indeed, its invention predates computer programming itself. Functional programming is the direct result of the work of Alonzo Church, who in 1936 invented l-calculus while pursuing the same mathematical problem that was motivating Alan Turing at the same time. His l-calculus is the foundation of the LISP language, invented in 1958 by John McCarthy. A foundational notion of l-calculus is immutability—that is, the notion that the values of symbols do not change. This effectively means that a functional language has no assignment statement. Most functional languages do, in fact, have some means to alter the value of a variable, but only under very strict discipline.
We can summarize the functional programming paradigm as follows:
Functional programming imposes discipline upon assignment.
Resources
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design, by Robert C. Martin