.NET 代码整洁手册

.NET 代码整洁手册

以下规则截取自Clean Code .NET

命名

命名要反应其作用和给定的上下文

Bad

int d;

Good

int daySinceModification;

命名变量名字要反应其意图,避免使用有误导性的命名

Bad

var dataFromDb = db.GetFromService().ToList();

Good

var listOfEmployee = _employeeService.GetEmployees().ToList();

避免使用匈牙利命名法

匈牙利命名法最初思想是在变量和函数名中加入前缀以增进人们对程序的理解,对于现代程序开发是没有意义的,因为现代编译器及IDE能自动识别其类型

Bad

int iCounter;
string strFullName;
DateTime dModifiedDate;

Good

int counter;
string fullName;
DateTime modifiedDate;

使用一致的大小写规则

大写字母能告诉你关于变量、函数等的更多信息。大小写规则是主观性的,所以只要与团队里其他人的规则保持一致即可。但重要的是,无论团队中使用的规则如何,必须要保持一致

Bad

const int DAYS_IN_WEEK = 7;
const int daysInMonth = 30;

var songs = new List<string> { 'Back In Black', 'Stairway to Heaven', 'Hey Jude' };
var Artists = new List<string> { 'ACDC', 'Led Zeppelin', 'The Beatles' };

bool EraseDatabase() {}
bool Restore_database() {}

class animal {}
class Alpaca {}

Good

const int DaysInWeek = 7;
const int DaysInMonth = 30;

var songs = new List<string> { 'Back In Black', 'Stairway to Heaven', 'Hey Jude' };
var artists = new List<string> { 'ACDC', 'Led Zeppelin', 'The Beatles' };

bool EraseDatabase() {}
bool RestoreDatabase() {}

class Animal {}
class Alpaca {}

变量、方法参数使用驼峰命名法

Bad

var employeephone;

public double CalculateSalary(int workingdays, int workinghours)
{
    // some logic
}

Good

var employeePhone;

public double CalculateSalary(int workingDays, int workingHours)
{
    // some logic
}

变量

避免嵌套层次过深

过多的if else 语句会使代码非常难读,显式优于隐式
即使是嵌套层次不深的厂字形代码也相当丑陋

Bad

public bool IsShopOpen(string day)
{
    if (!string.IsNullOrEmpty(day))
    {
        day = day.ToLower();
        if (day == "friday")
        {
            return true;
        }
        else if (day == "saturday")
        {
            return true;
        }
        else if (day == "sunday")
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    else
    {
        return false;
    }

}

Good

public bool IsShopOpen(string day)
{
    if (string.IsNullOrEmpty(day))
    {
        return false;
    }

    var openingDays = new[] { "friday", "saturday", "sunday" };
    return openingDays.Any(d => d == day.ToLower());
}

Bad

public long Fibonacci(int n)
{
    if (n < 50)
    {
        if (n != 0)
        {
            if (n != 1)
            {
                return Fibonacci(n - 1) + Fibonacci(n - 2);
            }
            else
            {
                return 1;
            }
        }
        else
        {
            return 0;
        }
    }
    else
    {
        throw new System.Exception("Not supported");
    }
}

Good

public long Fibonacci(int n)
{
    if (n == 0)
    {
        return 0;
    }

    if (n == 1)
    {
        return 1;
    }

    if (n > 50)
    {
        throw new System.Exception("Not supported");
    }

    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

丑陋的厂字形代码

public async Task ChangePersonState(string id){
    if(!string.IsNullOrEmpty(id)){
        await _personService.ChangePersonState(id);
    }
}

优化 ==>

public async Task ChangePersonState(string id){
    if(string.IsNullOrEmpty(id)) return;
    await _personService.ChangePersonState(id);
}

避免使读代码的人产生头脑映射

不要强迫你代码的读者去翻译变量的含义,显式优于隐式

Bad

var l = new[] { "Austin", "New York", "San Francisco" };

for (var i = 0; i < l.Count(); i++)
{
    var li = l[i];
    DoStuff();
    DoSomeOtherStuff();

    // ...
    // ...
    // ...
    // Wait, what is `li` for again?
    Dispatch(li);
}

Good

var locations = new[] { "Austin", "New York", "San Francisco" };

foreach (var location in locations)
{
    DoStuff();
    DoSomeOtherStuff();

    // ...
    // ...
    // ...
    Dispatch(location);
}

避免使用魔法字符串

Bad

if (userRole == "Admin")
{
    // logic in here
}

Good

const string ADMIN_ROLE = "Admin"
if (userRole == ADMIN_ROLE)
{
    // logic in here
}

不要增加不必要的上下文

如果你的 class/object 能告诉你一些信息,不要在变量名里重复这些信息

Bad

public class Car
{
    public string CarMake { get; set; }
    public string CarModel { get; set; }
    public string CarColor { get; set; }

    //...
}

Good

public class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public string Color { get; set; }

    //...
}

相同的变量使用相同的词汇

Bad

GetUserInfo();
GetUserData();
GetUserRecord();
GetUserProfile();

Good

GetUser();

使用可检索的名称

我们读的代码会比写的代码多,所以我们所编写的代码的可读性和可搜索性是非常重要的,没有意义的变量命名会打击代码的读者

Bad

// What the heck is data for?
var data = new { Name = "John", Age = 42 };

var stream = new MemoryStream();
var ser = new DataContractJsonSerializer(typeof(object));
ser.WriteObject(stream, data);

stream.Position = 0;
var sr = new StreamReader(stream);
Console.Write("JSON form of Data object: ");
Console.WriteLine(sr.ReadToEnd());

Good

var person = new Person
{
    Name = "John",
    Age = 42
};

var stream = new MemoryStream();
var ser = new DataContractJsonSerializer(typeof(Person));
ser.WriteObject(stream, person);

stream.Position = 0;
var sr = new StreamReader(stream);
Console.Write("JSON form of Data object: ");
Console.WriteLine(sr.ReadToEnd());

Bad

var data = new { Name = "John", Age = 42, PersonAccess = 4};

// What the heck is 4 for?
if (data.PersonAccess == 4)
{
    // do edit ...
}

Good

public enum PersonAccess : int
{
    ACCESS_READ = 1,
    ACCESS_CREATE = 2,
    ACCESS_UPDATE = 4,
    ACCESS_DELETE = 8
}

var person = new Person
{
    Name = "John",
    Age = 42,
    PersonAccess= PersonAccess.ACCESS_CREATE
};

if (person.PersonAccess == PersonAccess.ACCESS_UPDATE)
{
    // do edit ...
}

方法

避免副作用

当一个方法除了接收值和return其他值时,会产生副作用。
副作用可以是写入文件、修改全局变量等
当然,在某些时刻副作用是不可避免的。比如像之前的例子,你可能需要写入到一个外部文件。
你要做的就是集中处理你正在做的事情,不要有好几个方法或者类可以写入文件。用一个Service处理文件写入,有且只有一个

避免副作用最主要的意义是避免常见的坑比如在没有任何组织的情况下在对象间共享状态、使用可以被任意修改的可变数据类型,不集中控制副作用的发生。

Bad

// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
var name = "Ryan McDermott";

public void SplitAndEnrichFullName()
{
    var temp = name.Split(" ");
    name = $"His first name is {temp[0]}, and his last name is {temp[1]}"; // side effect
}

SplitAndEnrichFullName();

Console.WriteLine(name); // His first name is Ryan, and his last name is McDermott

Good

public string SplitAndEnrichFullName(string name)
{
    var temp = name.Split(" ");
    return $"His first name is {temp[0]}, and his last name is {temp[1]}";
}

var name = "Ryan McDermott";
var fullName = SplitAndEnrichFullName(name);

Console.WriteLine(name); // Ryan McDermott
Console.WriteLine(fullName); // His first name is Ryan, and his last name is McDermott

避免使用否定的条件语句

Bad

public bool IsDOMNodeNotPresent(string node)
{
    // ...
}

if (!IsDOMNodeNotPresent(node))
{
    // ...
}

Good

public bool IsDOMNodePresent(string node)
{
    // ...
}

if (IsDOMNodePresent(node))
{
    // ...
}

避免使用条件语句

这看起来似乎是不可能的。刚听到时,许多人可能会说,“没有if 语句我改怎么写程序呢”,答案是,可以在许多情况下使用多态性来实现相同的任务。
第二个问题通常是,“好吧,很不错,但是我为什么要这么做”。答案是一个简单的代码概念:一个方法只做一件事。当你有包含if语句的方法或者类时,你在告诉别人,你的方法做的不止一件事。
记住,只做一件事

Bad

class Airplane
{
    // ...

    public double GetCruisingAltitude()
    {
        switch (_type)
        {
            case '777':
                return GetMaxAltitude() - GetPassengerCount();
            case 'Air Force One':
                return GetMaxAltitude();
            case 'Cessna':
                return GetMaxAltitude() - GetFuelExpenditure();
        }
    }
}

Good

interface IAirplane
{
    // ...

    double GetCruisingAltitude();
}

class Boeing777 : IAirplane
{
    // ...

    public double GetCruisingAltitude()
    {
        return GetMaxAltitude() - GetPassengerCount();
    }
}

class AirForceOne : IAirplane
{
    // ...

    public double GetCruisingAltitude()
    {
        return GetMaxAltitude();
    }
}

class Cessna : IAirplane
{
    // ...

    public double GetCruisingAltitude()
    {
        return GetMaxAltitude() - GetFuelExpenditure();
    }
}

避免使用类型检查

Bad

public Path TravelToTexas(object vehicle)
{
    if (vehicle.GetType() == typeof(Bicycle))
    {
        (vehicle as Bicycle).PeddleTo(new Location("texas"));
    }
    else if (vehicle.GetType() == typeof(Car))
    {
        (vehicle as Car).DriveTo(new Location("texas"));
    }
}

Good

public Path TravelToTexas(Traveler vehicle)
{
    vehicle.TravelTo(new Location("texas"));
}

Bad

public int Combine(dynamic val1, dynamic val2)
{
    int value;
    if (!int.TryParse(val1, out value) || !int.TryParse(val2, out value))
    {
        throw new Exception('Must be of type Number');
    }

    return val1 + val2;
}

Good

public int Combine(int val1, int val2)
{
    return val1 + val2;
}

方法参数避免出现flags

参数有flag就表示方法的职责不够单一。最佳实践是一个方法只有一个职责,即只做一件事。如果一个布尔参数为一个方法增加了多于的职责,将该方法分割成两个

Bad

public void CreateFile(string name, bool temp = false)
{
    if (temp)
    {
        Touch("./temp/" + name);
    }
    else
    {
        Touch(name);
    }
}

Good

public void CreateFile(string name)
{
    Touch(name);
}

public void CreateTempFile(string name)
{
    Touch("./temp/"  + name);
}

不要使用单例模式

单例模式是一种反模式:

  1. 它们通常被用作全局变量,为什么不好呢?因为这隐藏了你代码中的依赖关系,而不是通过接口暴露它们。使一些东西全局化来避免到处传递是一种代码坏味道
  2. 这违背了单一职责原则:
    因为他们同时也控制着自己的创建和生命周期。
  3. 它们本质上导致代码紧密耦合。在许多情况下,这导致在测试的时候很难fake或者mock它们
  4. 它们在应用程序的生命周期中携带状态,这是对测试的另一个打击,因为这可能导致你遇到测试需要按照顺序执行的情况,这对单元测试来说是一个打的问题,因为每个单元测试都应该独立于其他单元测试

Bad

class DBConnection
{
    private static DBConnection _instance;

    private DBConnection()
    {
        // ...
    }

    public static GetInstance()
    {
        if (_instance == null)
        {
            _instance = new DBConnection();
        }

        return _instance;
    }

    // ...
}

var singleton = DBConnection.GetInstance();

Good

class DBConnection
{
    public DBConnection(IOptions<DbConnectionOption> options)
    {
        // ...
    }

    // ...
}

创建 DBConnection 类的实例并且使用Option模式来配置

var options = <resolve from IOC>;
var connection = new DBConnection(options);

方法参数 (理想情况下2个或者更少)

限制参数数量是及其重要的,因为这使得测试你的方法更容易。多于3个参数会导致组合爆炸,因为你不得不针对每个参数进行无数种不同的测试用例测试。
零个参数是理想的,一到两个是可以的,三个以上应该避免。任何多于3个参数的方法应该被重新整理。通常来说,如果你的参数数量超过两个,那可能说明你的方法做的事情太多,在这种情况下,一个高级别的对象作为参数更合适

Bad

public void CreateMenu(string title, string body, string buttonText, bool cancellable)
{
    // ...
}

Good

public class MenuConfig
{
    public string Title { get; set; }
    public string Body { get; set; }
    public string ButtonText { get; set; }
    public bool Cancellable { get; set; }
}

var config = new MenuConfig
{
    Title = "Foo",
    Body = "Bar",
    ButtonText = "Baz",
    Cancellable = true
};

public void CreateMenu(MenuConfig config)
{
    // ...
}

方法应该只做一件事

在软件生涯中这应该是最重要的规则。当方法做不止一件事时,它们更难编写、测试和推导。当你能够把方法分离到只有一个行为,他们可以被轻易地重构并且你的代码读起来更加整洁。
If you take nothing else away from this guide other than this, you'll be ahead of many developers

Bad

public void SendEmailToListOfClients(string[] clients)
{
    foreach (var client in clients)
    {
        var clientRecord = db.Find(client);
        if (clientRecord.IsActive())
        {
            Email(client);
        }
    }
}

Good

public void SendEmailToListOfClients(string[] clients)
{
    var activeClients = GetActiveClients(clients);
    // Do some logic
}

public List<Client> GetActiveClients(string[] clients)
{
    return db.Find(clients).Where(s => s.Status == "Active");
}

方法名应该说明它们是做什么的

Bad

public class Email
{
    //...

    public void Handle()
    {
        SendMail(this._to, this._subject, this._body);
    }
}

var message = new Email(...);
// What is this? A handle for the message? Are we writing to a file now?
message.Handle();

Good

public class Email
{
    //...

    public void Send()
    {
        SendMail(this._to, this._subject, this._body);
    }
}

var message = new Email(...);
// Clear and obvious
message.Send();

方法中的代码应该只有一个抽象级别

当你的方法有不止一个抽象级别,说明你的方法做了太多的事。分割方法会使代码的可复用性和可测试性更好

Bad

public string ParseBetterJSAlternative(string code)
{
    var regexes = [
        // ...
    ];

    var statements = explode(" ", code);
    var tokens = new string[] {};
    foreach (var regex in regexes)
    {
        foreach (var statement in statements)
        {
            // ...
        }
    }

    var ast = new string[] {};
    foreach (var token in tokens)
    {
        // lex...
    }

    foreach (var node in ast)
    {
        // parse...
    }
}

或者

Bad

public string Tokenize(string code)
{
    var regexes = new string[]
    {
        // ...
    };

    var statements = explode(" ", code);
    var tokens = new string[] {};
    foreach (var regex in regexes)
    {
        foreach (var statement in statements)
        {
            tokens[] = /* ... */;
        }
    }

    return tokens;
}

public string Lexer(string[] tokens)
{
    var ast = new string[] {};
    foreach (var token in tokens)
    {
        ast[] = /* ... */;
    }

    return ast;
}

public string ParseBetterJSAlternative(string code)
{
    var tokens = Tokenize(code);
    var ast = Lexer(tokens);
    foreach (var node in ast)
    {
        // parse...
    }
}

该方法的目的目标是Parse,显然Tokenize和Lexer部分代码与其下方的parse相关的细节代码不属于同一抽象级别,应该提取这两部分至其自己的抽象级别中,提升代码的可复用性和可测试性

Good

class Tokenizer
{
    public string Tokenize(string code)
    {
        var regexes = new string[] {
            // ...
        };

        var statements = explode(" ", code);
        var tokens = new string[] {};
        foreach (var regex in regexes)
        {
            foreach (var statement in statements)
            {
                tokens[] = /* ... */;
            }
        }

        return tokens;
    }
}

class Lexer
{
    public string Lexify(string[] tokens)
    {
        var ast = new[] {};
        foreach (var token in tokens)
        {
            ast[] = /* ... */;
        }

        return ast;
    }
}

class BetterJSAlternative
{
    private string _tokenizer;
    private string _lexer;

    public BetterJSAlternative(Tokenizer tokenizer, Lexer lexer)
    {
        _tokenizer = tokenizer;
        _lexer = lexer;
    }

    public string Parse(string code)
    {
        var tokens = _tokenizer.Tokenize(code);
        var ast = _lexer.Lexify(tokens);
        foreach (var node in ast)
        {
            // parse...
        }
    }
}

方法调用者与被调用者应该保持接近

如果一个方法调用另一个方法,在源文件中保持这些函数垂直接近。理想情况下,让调用者在被调用者的正上方。我们倾向于从上到下阅读代码,就像阅读报纸一样。因此,让你的代码也以这种方式阅读。

Bad

class PerformanceReview
{
    private readonly Employee _employee;

    public PerformanceReview(Employee employee)
    {
        _employee = employee;
    }

    private IEnumerable<PeersData> LookupPeers()
    {
        return db.lookup(_employee, 'peers');
    }

    private ManagerData LookupManager()
    {
        return db.lookup(_employee, 'manager');
    }

    private IEnumerable<PeerReviews> GetPeerReviews()
    {
        var peers = LookupPeers();
        // ...
    }

    public PerfReviewData PerfReview()
    {
        GetPeerReviews();
        GetManagerReview();
        GetSelfReview();
    }

    public ManagerData GetManagerReview()
    {
        var manager = LookupManager();
    }

    public EmployeeData GetSelfReview()
    {
        // ...
    }
}

var  review = new PerformanceReview(employee);
review.PerfReview();

Good

class PerformanceReview
{
    private readonly Employee _employee;

    public PerformanceReview(Employee employee)
    {
        _employee = employee;
    }

    public PerfReviewData PerfReview()
    {
        GetPeerReviews();
        GetManagerReview();
        GetSelfReview();
    }

    private IEnumerable<PeerReviews> GetPeerReviews()
    {
        var peers = LookupPeers();
        // ...
    }

    private IEnumerable<PeersData> LookupPeers()
    {
        return db.lookup(_employee, 'peers');
    }

    private ManagerData GetManagerReview()
    {
        var manager = LookupManager();
        return manager;
    }

    private ManagerData LookupManager()
    {
        return db.lookup(_employee, 'manager');
    }

    private EmployeeData GetSelfReview()
    {
        // ...
    }
}

var review = new PerformanceReview(employee);
review.PerfReview();

封装条件

Bad

if (article.state == "published")
{
    // ...
}

Good

if (article.IsPublished())
{
    // ...
}

移除 dead code

Dead code就像冗余的代码一样糟糕。没有理由在你的代码库中保留他们。如果不被调用,干掉它们!如果你在某一时刻需要召唤它们,你可以再历史版本中找到它们。

Bad

public void OldRequestModule(string url)
{
    // ...
}

public void NewRequestModule(string url)
{
    // ...
}

var request = NewRequestModule(requestUrl);
InventoryTracker("apples", request, "www.inventory-awesome.io");

Good

public void RequestModule(string url)
{
    // ...
}

var request = RequestModule(requestUrl);
InventoryTracker("apples", request, "www.inventory-awesome.io");

对象和数据结构

使用getter和setter

你可以为方法设置,public, protected 和 private等访问修饰符,这样,你可以更灵活地控制对象属性的修改

  • 当你想要做的不只是获取一个对象的属性值,你不必在代码库中查找和更改每个访问器
  • 在执行set的时候更容易添加验证逻辑
  • 封装内部表现
  • 在get或set时容易添加log和异常处理
  • 在继承该类时,可以override默认功能实现
  • 可以实现懒加载对象的属性,比如从数据库获取属性值

而且,这符合开闭原则

Bad

class BankAccount
{
    public double Balance = 1000;
}

var bankAccount = new BankAccount();

// Fake buy shoes...
bankAccount.Balance -= 100;

Good

class BankAccount
{
    private double _balance = 0.0D;

    pubic double Balance {
        get {
            return _balance;
        }
    }

    public BankAccount(balance = 1000)
    {
       _balance = balance;
    }

    public void WithdrawBalance(int amount)
    {
        if (amount > _balance)
        {
            throw new Exception('Amount greater than available balance.');
        }

        _balance -= amount;
    }

    public void DepositBalance(int amount)
    {
        _balance += amount;
    }
}

var bankAccount = new BankAccount();

// Buy shoes...
bankAccount.WithdrawBalance(price);

// Get balance
balance = bankAccount.Balance;

为对象设置private/protected 成员

Bad

class Employee
{
    public string Name { get; set; }

    public Employee(string name)
    {
        Name = name;
    }
}

var employee = new Employee("John Doe");
Console.WriteLine(employee.Name); // Employee name: John Doe

Good

class Employee
{
    public string Name { get; private set; }

    public Employee(string name)
    {
        Name = name;
    }
}

var employee = new Employee("John Doe");
Console.WriteLine(employee.Name); // Employee name: John Doe

使用方法链

方法链模式是非常有用并且在许多类库里是非常常见的。这会使你的代码更具表现力的同事减少冗余。因此,使用方法链然后你会发现你的代码会多么整洁。

Good

public static class ListExtensions
{
    public static List<T> FluentAdd<T>(this List<T> list, T item)
    {
        list.Add(item);
        return list;
    }

    public static List<T> FluentClear<T>(this List<T> list)
    {
        list.Clear();
        return list;
    }

    public static List<T> FluentForEach<T>(this List<T> list, Action<T> action)
    {
        list.ForEach(action);
        return list;
    }

    public static List<T> FluentInsert<T>(this List<T> list, int index, T item)
    {
        list.Insert(index, item);
        return list;
    }

    public static List<T> FluentRemoveAt<T>(this List<T> list, int index)
    {
        list.RemoveAt(index);
        return list;
    }

    public static List<T> FluentReverse<T>(this List<T> list)
    {
        list.Reverse();
        return list;
    }
}

internal static void ListFluentExtensions()
{
    var list = new List<int>() { 1, 2, 3, 4, 5 }
        .FluentAdd(1)
        .FluentInsert(0, 0)
        .FluentRemoveAt(1)
        .FluentReverse()
        .FluentForEach(value => value.WriteLine())
        .FluentClear();
}

组合优于继承

就像在著名的4人帮的设计模式一书中提到的一样,你应该在任何地方更倾向于组合而不是继承。
这条准则的主要观点是,如果你的头脑本能地倾向于继承,试着思考一下组合是否能更好的解决你的问题。在大多数情况下是这样的。
你可能会问,"什么时候应该使用继承?",这取决于你面对的问题,以下是一个使用继承比使用组合更有意义的情况列表

  • 你的继承体现了"is-a"关系而不是"has-a"关系(Human->Animal vs. User->UserDetails).
  • 你可以复用基类的代码 (Humans can move like all animals).
  • 你想通过修改基类来实现对所有子类的全局修改(注意,被子类override的方法,无法被基类控制,所以不满足)

Bad

class Employee
{
    private string Name { get; set; }
    private string Email { get; set; }

    public Employee(string name, string email)
    {
        Name = name;
        Email = email;
    }

    // ...
}

// Bad because Employees "have" tax data.
// EmployeeTaxData is not a type of Employee

class EmployeeTaxData : Employee
{
    private string Name { get; }
    private string Email { get; }

    public EmployeeTaxData(string name, string email, string ssn, string salary)
    {
         // ...
    }

    // ...
}

Good

class EmployeeTaxData
{
    public string Ssn { get; }
    public string Salary { get; }

    public EmployeeTaxData(string ssn, string salary)
    {
        Ssn = ssn;
        Salary = salary;
    }

    // ...
}

class Employee
{
    public string Name { get; }
    public string Email { get; }
    public EmployeeTaxData TaxData { get; }

    public Employee(string name, string email)
    {
        Name = name;
        Email = email;
    }

    public void SetTax(string ssn, double salary)
    {
        TaxData = new EmployeeTaxData(ssn, salary);
    }

    // ...
}

SOLID 原则

单一职责原则(SRP)

一个类只有一个使其改变的原因(一个类只对一类行为者负责).
在一个类里塞满许多功能是非常有吸引力的,就像你只能带一个行李箱上飞机的时候,你的行李箱里塞满了各种东西。
然而,当你想要取任何一件东西的时候,你都要打开它。这种类时不具有高度内聚性的,因为你有许多原因可以修改它。
最小化你修改累的次数是非常重要的

因为如果一个类中有太多的功能,而你修改了其中的一部分,就很难理解这将如何影响代码库中的其他依赖模块。

Bad

class UserSettings
{
    private User User;

    public UserSettings(User user)
    {
        User = user;
    }

    public void ChangeSettings(Settings settings)
    {
        if (verifyCredentials())
        {
            // ...
        }
    }

    private bool VerifyCredentials()
    {
        // ...
    }
}

Good

class UserAuth
{
    private User User;

    public UserAuth(User user)
    {
        User = user;
    }

    public bool VerifyCredentials()
    {
        // ...
    }
}

class UserSettings
{
    private User User;
    private UserAuth Auth;

    public UserSettings(User user)
    {
        User = user;
        Auth = new UserAuth(user);
    }

    public void ChangeSettings(Settings settings)
    {
        if (Auth.VerifyCredentials())
        {
            // ...
        }
    }
}

开闭原则(OCP)

软件实体(类、模块、方法等)应该针对扩展开放,针对修改关闭。这意味着应该允许其他人添加显功能的同时不用修改已经存在的代码

Bad

abstract class AdapterBase
{
    protected string Name;

    public string GetName()
    {
        return Name;
    }
}

class AjaxAdapter : AdapterBase
{
    public AjaxAdapter()
    {
        Name = "ajaxAdapter";
    }
}

class NodeAdapter : AdapterBase
{
    public NodeAdapter()
    {
        Name = "nodeAdapter";
    }
}

class HttpRequester : AdapterBase
{
    private readonly AdapterBase Adapter;

    public HttpRequester(AdapterBase adapter)
    {
        Adapter = adapter;
    }

    public bool Fetch(string url)
    {
        var adapterName = Adapter.GetName();

        if (adapterName == "ajaxAdapter")
        {
            return MakeAjaxCall(url);
        }
        else if (adapterName == "httpNodeAdapter")
        {
            return MakeHttpCall(url);
        }
    }

    private bool MakeAjaxCall(string url)
    {
        // request and return promise
    }

    private bool MakeHttpCall(string url)
    {
        // request and return promise
    }
}

Good

interface IAdapter
{
    bool Request(string url);
}

class AjaxAdapter : IAdapter
{
    public bool Request(string url)
    {
        // request and return promise
    }
}

class NodeAdapter : IAdapter
{
    public bool Request(string url)
    {
        // request and return promise
    }
}

class HttpRequester
{
    private readonly IAdapter Adapter;

    public HttpRequester(IAdapter adapter)
    {
        Adapter = adapter;
    }

    public bool Fetch(string url)
    {
        return Adapter.Request(url);
    }
}

里氏替换原则(LSP)

常规定义:如果S是T的子类型,那么类型T的对象可以替换类型S的队形,而不改变程序的任何预期特性(正确性、执行的任务等),一个可怕的定义。
最好的解释是,如果你有一个父类和一个其子类,那么父类和子类可以相互交换而不引起不正确的结构。这也许仍然很令人困惑,所以让我们看一下长方形-正方形的例子。从数学角度来说
正方形是长方形,但是你用"is-a"来评估这个继承关系,你会遇到麻烦

Bad

class Rectangle
{
    protected double Width = 0;
    protected double Height = 0;

    public Drawable Render(double area)
    {
        // ...
    }

    public void SetWidth(double width)
    {
        Width = width;
    }

    public void SetHeight(double height)
    {
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }
}

class Square : Rectangle
{
    public double SetWidth(double width)
    {
        Width = Height = width;
    }

    public double SetHeight(double height)
    {
        Width = Height = height;
    }
}

Drawable RenderLargeRectangles(Rectangle rectangles)
{
    foreach (rectangle in rectangles)
    {
        rectangle.SetWidth(4);
        rectangle.SetHeight(5);
        var area = rectangle.GetArea(); // BAD: Will return 25 for Square. Should be 20.
        rectangle.Render(area);
    }
}

var rectangles = new[] { new Rectangle(), new Rectangle(), new Square() };
RenderLargeRectangles(rectangles);

Good

abstract class ShapeBase
{
    protected double Width = 0;
    protected double Height = 0;

    abstract public double GetArea();

    public Drawable Render(double area)
    {
        // ...
    }
}

class Rectangle : ShapeBase
{
    public void SetWidth(double width)
    {
        Width = width;
    }

    public void SetHeight(double height)
    {
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }
}

class Square : ShapeBase
{
    private double Length = 0;

    public double SetLength(double length)
    {
        Length = length;
    }

    public double GetArea()
    {
        return Math.Pow(Length, 2);
    }
}

Drawable RenderLargeRectangles(Rectangle rectangles)
{
    foreach (rectangle in rectangles)
    {
        if (rectangle is Square)
        {
            rectangle.SetLength(5);
        }
        else if (rectangle is Rectangle)
        {
            rectangle.SetWidth(4);
            rectangle.SetHeight(5);
        }

        var area = rectangle.GetArea();
        rectangle.Render(area);
    }
}

var shapes = new[] { new Rectangle(), new Rectangle(), new Square() };
RenderLargeRectangles(shapes);

接口隔离原则(ISP)

客户端不应该被强制依赖于它不需要的接口

一个好的例子是看一个依赖于复杂的setting对象的类。因为大多数时候,我们的类不需要所有的设置项,所以不要求客户端设置大量的选项是有益的。使它们可选有助于防止出现“臃肿的接口”。

Bad

public interface IEmployee
{
    void Work();
    void Eat();
}

public class Human : IEmployee
{
    public void Work()
    {
        // ....working
    }

    public void Eat()
    {
        // ...... eating in lunch break
    }
}

public class Robot : IEmployee
{
    public void Work()
    {
        //.... working much more
    }

    public void Eat()
    {
        //.... robot can't eat, but it must implement this method
    }
}

Good:

Not every worker is an employee, but every employee is an worker.

public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface IEmployee : IFeedable, IWorkable
{
}

public class Human : IEmployee
{
    public void Work()
    {
        // ....working
    }

    public void Eat()
    {
        //.... eating in lunch break
    }
}

// robot can only work
public class Robot : IWorkable
{
    public void Work()
    {
        // ....working
    }
}

依赖倒置原则 (DIP)

这个原则说明了两件基本的事:

  1. 高层模块不应该依赖低层模块,二者应该依赖共同的抽象
  2. 抽象不应该依赖于实现(细节),实现(细节)应该依赖于抽象

咋看之下很难理解,单是如果你用过.NET/.NET Core framework,你已经见过这个原则的实现了,那就是依赖注入(DI)。
尽管它们不是完全相同的概念,依赖倒置原则使高级模块无法了解低层模块的细节和设置它们。而这可以通过依赖注入完成。
这样做的一个巨大好处是减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使代码难以重构。

Bad

public abstract class EmployeeBase
{
    protected virtual void Work()
    {
        // ....working
    }
}

public class Human : EmployeeBase
{
    public override void Work()
    {
        //.... working much more
    }
}

public class Robot : EmployeeBase
{
    public override void Work()
    {
        //.... working much, much more
    }
}

public class Manager
{
    private readonly Robot _robot;
    private readonly Human _human;

    public Manager(Robot robot, Human human)
    {
        _robot = robot;
        _human = human;
    }

    public void Manage()
    {
        _robot.Work();
        _human.Work();
    }
}

Good

public interface IEmployee
{
    void Work();
}

public class Human : IEmployee
{
    public void Work()
    {
        // ....working
    }
}

public class Robot : IEmployee
{
    public void Work()
    {
        //.... working much more
    }
}

public class Manager
{
    private readonly IEnumerable<IEmployee> _employees;

    public Manager(IEnumerable<IEmployee> employees)
    {
        _employees = employees;
    }

    public void Manage()
    {
        foreach (var employee in _employees)
        {
            _employee.Work();
        }
    }
}

Don’t repeat yourself (DRY)

尽最大努力避免重复代码。重复代码是不好的,因为这意味着如果你需要修改一些逻辑,你需要修改不止一个地方。

想象一下,如果你经营一家餐馆,并记录你的库存:所有的番茄、洋葱、大蒜、香料等。如果你有多个列表,那么当你上菜的时候,所有的列表都要更新。如果你只有一个列表,那么只有一个地方可以更新!

通常情况下,你有重复的代码,因为你有两个或更多稍微不同的东西,它们有很多共同点,但它们的差异迫使你有两个或更多独立的函数来满足这些稍微不同的东西。删除重复的代码意味着创建一个抽象,只使用一个函数/模块/类就可以处理这一组不同的东西。

获得正确的抽象是至关重要的,这就是为什么你应该遵循本节中列出的SOLID原则。糟糕的抽象可能比重复的代码更糟糕,所以要小心!

Bad

public List<EmployeeData> ShowDeveloperList(Developers developers)
{
    foreach (var developers in developer)
    {
        var expectedSalary = developer.CalculateExpectedSalary();
        var experience = developer.GetExperience();
        var githubLink = developer.GetGithubLink();
        var data = new[] {
            expectedSalary,
            experience,
            githubLink
        };

        Render(data);
    }
}

public List<ManagerData> ShowManagerList(Manager managers)
{
    foreach (var manager in managers)
    {
        var expectedSalary = manager.CalculateExpectedSalary();
        var experience = manager.GetExperience();
        var githubLink = manager.GetGithubLink();
        var data =
        new[] {
            expectedSalary,
            experience,
            githubLink
        };

        render(data);
    }
}

Good

public List<EmployeeData> ShowList(Employee employees)
{
    foreach (var employee in employees)
    {
        var expectedSalary = employees.CalculateExpectedSalary();
        var experience = employees.GetExperience();
        var githubLink = employees.GetGithubLink();
        var data =
        new[] {
            expectedSalary,
            experience,
            githubLink
        };

        render(data);
    }
}

Very good

public List<EmployeeData> ShowList(Employee employees)
{
    foreach (var employee in employees)
    {
        render(new[] {
            employee.CalculateExpectedSalary(),
            employee.GetExperience(),
            employee.GetGithubLink()
        });
    }
}

测试

测试的基本概念

测试比发布更重要。如果没有测试或数量不足,那么每次交付代码时,你将不能确定您没有破坏任何东西。
决定什么构成了足够的数量取决于你的团队,但是拥有100%的覆盖率(所有声明和分支)是你获得非常高的信心和安心的方式。这意味着除了拥有一个优秀的测试框架外,您还需要使用一个良好的覆盖工具。

没有理由不编写测试。我们有很多优秀的.NET测试框架,所以找一个你团队喜欢的。

每个测试负责单一概念

确保你的测试是聚焦的,而不是测试混杂的(不相关的)东西,强制使用AAA模式使你的代码更干净和可读。当你发现一个适合你的团队时,那么致力于始终为你引入的每个新特性/模块编写测试。如果你的首选的方法是测试驱动开发(TDD),那就太好了,但关键是要确保在启动任何特性或重构现有特性之前达到覆盖目标。

Bad

public class MakeDotNetGreatAgainTests
{
    [Fact]
    public void HandleDateBoundaries()
    {
        var date = new MyDateTime("1/1/2015");
        date.AddDays(30);
        Assert.Equal("1/31/2015", date);

        date = new MyDateTime("2/1/2016");
        date.AddDays(28);
        Assert.Equal("02/29/2016", date);

        date = new MyDateTime("2/1/2015");
        date.AddDays(28);
        Assert.Equal("03/01/2015", date);
    }
}

Good


public class MakeDotNetGreatAgainTests
{
    [Fact]
    public void Handle30DayMonths()
    {
        // Arrange
        var date = new MyDateTime("1/1/2015");

        // Act
        date.AddDays(30);

        // Assert
        Assert.Equal("1/31/2015", date);
    }

    [Fact]
    public void HandleLeapYear()
    {
        // Arrange
        var date = new MyDateTime("2/1/2016");

        // Act
        date.AddDays(28);

        // Assert
        Assert.Equal("02/29/2016", date);
    }

    [Fact]
    public void HandleNonLeapYear()
    {
        // Arrange
        var date = new MyDateTime("2/1/2015");

        // Act
        date.AddDays(28);

        // Assert
        Assert.Equal("03/01/2015", date);
    }
}

并发

使用Async/Await

Best Practice

异常处理

异常处理的基本概念

抛出异常是一件好事,异常意味着运行时成功地识别出了程序中的错误,并通过停止当前堆栈上的函数执行、终止( .NET/.NET Core)进程来让你知道,并在控制台中使用堆栈跟踪通知你

不要在catch块中使用'throw ex'

如果你需要在捕获异常后重新抛出异常,只需使用'throw'。使用这个,你将保存堆栈跟踪。在下面的Bad代码中,你将丢失堆栈跟踪。

Bad

try
{
    // Do something..
}
catch (Exception ex)
{
    // Any action something like roll-back or logging etc.
    throw ex;
}

Good

try
{
    // Do something..
}
catch (Exception ex)
{
    // Any action something like roll-back or logging etc.
    throw;
}

不要忽略捕捉到的异常

对捕获的错误不做任何处理不会让你有修复他们的机会或对它们做出反应。抛出错误并不好,因为它经常会被打印到控制台的大量内容所淹没。如果你在try/catch中包装了任何代码,这意味着你认为哪里可能会发生异常,因此你应该有一个计划,或创建一个代码路径,以应对它发生时的情况。

Bad

try
{
    FunctionThatMightThrow();
}
catch (Exception ex)
{
    // silent exception
}

Good

try
{
    FunctionThatMightThrow();
}
catch (Exception error)
{
    NotifyUserOfError(error);

    // Another option
    ReportErrorToService(error);
}

使用多个catch块来取代if语句

如果需要根据异常类型采取相应的操作,最好使用多个catch块进行异常处理。

Bad

try
{
    // Do something..
}
catch (Exception ex)
{

    if (ex is TaskCanceledException)
    {
        // Take action for TaskCanceledException
    }
    else if (ex is TaskSchedulerException)
    {
        // Take action for TaskSchedulerException
    }
}

Good

try
{
    // Do something..
}
catch (TaskCanceledException ex)
{
    // Take action for TaskCanceledException
}
catch (TaskSchedulerException ex)
{
    // Take action for TaskSchedulerException
}

在重新抛出异常时保持异常堆栈跟踪

c#允许使用throw关键字在catch块中重新抛出异常。使用throw e;抛出捕获的异常是一个不好的实践。该语句会重置堆栈跟踪。取而代之的是使用throw,这将保持堆栈跟踪,并提供对异常的更深入的了解。另一种选择是使用自定义异常。简单地实例化一个新的异常,并通过抛出new CustomException("some info", e)将其内部异常属性设置为已捕获的异常。向异常添加信息是一个很好的实践,因为这将有助于调试。但是,如果目标是记录异常,那么使用throw;

Bad

try
{
    FunctionThatMightThrow();
}
catch (Exception ex)
{
    logger.LogInfo(ex);
    throw ex;
}

Good

try
{
    FunctionThatMightThrow();
}
catch (Exception error)
{
    logger.LogInfo(error);
    throw;
}

Good

try
{
    FunctionThatMightThrow();
}
catch (Exception error)
{
    logger.LogInfo(error);
    throw new CustomException(error);
}

注释

禁止位置标记

他们通常只会增加噪音。让函数和变量名以及适当的缩进和格式为代码提供可视化结构。

Bad

////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
var model = new[]
{
    menu: 'foo',
    nav: 'bar'
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
void Actions()
{
    // ...
};

Bad


#region Scope Model Instantiation

var model = {
    menu: 'foo',
    nav: 'bar'
};

#endregion

#region Action setup

void Actions() {
    // ...
};

#endregion

Good

var model = new[]
{
    menu: 'foo',
    nav: 'bar'
};

void Actions()
{
    // ...
};

禁止在代码库中留下注释掉的代码

版本控制的存在是有原因的。将旧代码留在历史中。

Bad

doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

Good

doStuff();

禁止写日志注释

记住,使用版本控制! 我们不需要dead code、注释掉的代码,尤其是日志注释。使用git log来获取历史记录!

Bad

/**
 * 2018-12-20: Removed monads, didn't understand them (RM)
 * 2017-10-01: Improved using special monads (JP)
 * 2016-02-03: Removed type-checking (LI)
 * 2015-03-14: Added combine with type-checking (JR)
 */
public int Combine(int a,int b)
{
    return a + b;
}

Good

public int Combine(int a,int b)
{
    return a + b;
}

只对具有业务逻辑复杂性的东西写注释

Comments are an apology, not a requirement. Good code mostly documents itself.

An apology is what you say or do when you want to let someone know (or fool them into thinking) that you regret something.

Bad

public int HashIt(string data)
{
    // The hash
    var hash = 0;

    // Length of string
    var length = data.length;

    // Loop through every character in data
    for (var i = 0; i < length; i++)
    {
        // Get character code.
        const char = data.charCodeAt(i);
        // Make the hash
        hash = ((hash << 5) - hash) + char;
        // Convert to 32-bit integer
        hash &= hash;
    }
}

Better but still Bad:

public int HashIt(string data)
{
    var hash = 0;
    var length = data.length;
    for (var i = 0; i < length; i++)
    {
        const char = data.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;

        // Convert to 32-bit integer
        hash &= hash;
    }
}

如果注释解释了代码正在做什么,那么它可能是一个无用的注释,可以用一个命名良好的变量或函数来实现。前面代码中的注释可以用名为ConvertTo32bitInt的函数替换,因此这个注释仍然是无用的。然而,很难用代码来表达为什么开发者选择djb2 hash算法而不是sha-1或其他hash函数。在这种情况下,注释是可以接受的。

Good

public int Hash(string data)
{
    var hash = 0;
    var length = data.length;

    for (var i = 0; i < length; i++)
    {
        var character = data[i];
        // use of djb2 hash algorithm as it has a good compromise
        // between speed and low collision with a very simple implementation
        hash = ((hash << 5) - hash) + character;

        hash = ConvertTo32BitInt(hash);
    }
    return hash;
}

private int ConvertTo32BitInt(int value)
{
    return value & value;
}

posted @ 2022-02-07 16:18  青玄鸟  阅读(66)  评论(0编辑  收藏  举报