Course Review - C# Intermediate Classes, Interfaces and OOP

2020-06-25

五月的時候趁著 300 元特價的時候一口氣買了十門課程,這段期間各課程都散亂的看了一點,尚未完成任何一門課程。而上週開始一點一滴的看,默默就把這門課程看完了,過程中刷新了自己對於物件導向的觀念、各種修飾詞的實務使用時機以及開發操作上的技巧,總體而言獲益良多,也助燃提升學習 C# 的興趣。

logo

課程的收穫

Field & Property (Encapsulation)

曾經的我並不是那麼清楚兩者的差別,現在終於知道是為了基於物件的封裝(Encapsulation),才有 Field 與 Property 的差別。Field 作為物件本身的內部資訊,提供方法的行為邏輯使用,並不直接面對外界;Property 則是外界需要存取的關於物件的公開資訊。兩者容易令人混淆的原因就是往往 Property 看似就只是語法差異的 Field,例如:

public string Name; // Field
public string Name { get; set; } // Property

對於物件實例 (instance) 而言兩者存取 (get) 或者寫入 (set) 值的方式並沒有差異,然而實際上最大的差異就是封裝的精神。Public field 是將內部資訊暴露於外部,property 則隱含了一個 private field ,property 只是此 private field 與外界互動的中介:

private string name; // Private Field
public string Name { get => _name; set => _name = value; } // Property

而在 Visual Studio IDE 的提示下,會鼓勵使用 AUTO Property 於是最後就成為了:

public string Name { get; set; } // Property

試作一個小範例,遵循著封裝的精神,並且依照 IDE 的提示使用 AUTO Property。類別 (Class) 有 Weight Property,可以提供外界存取或者寫入,同時也提供了 kg, lb, oz 等單位轉換,但不是透過 Method ,因為 Method 是關於物件的行為,Private Field 的轉換就讓 Property 來完成。

public class CellPhone
{
    public int Weight { get; set; }
    public double WeightKg { get => Weight * 0.001; }
    public double WeightLb { get => Weight * 0.0022; }
    public double WeightOz { get => Weight * 0.03527; }
}

class Program
{
    static void Main(string[] args)
    {
        var cellphone = new CellPhone();
        cellphone.Weight = 300;
        Console.WriteLine($"grams : {cellphone.Weight}");
        Console.WriteLine("-----Convert-----");
        Console.WriteLine($"Kg : {cellphone.WeightKg}");
        Console.WriteLine($"Lb : {cellphone.WeightLb} \nOz : {cellphone.WeightOz}");
    }
}

可以觀察到 IDE 提示 Property, Filed, Method 的不同符號

Object Initializers over Constructor

儘管 Constructor 提供了多載 (Overloading) 的方式,可以用不同的函式簽名 (Signature),但如果有多個 Field 為此建造多個 Constructor 會讓程式碼變得凌亂。因此課程建議除非是有物件的建構有需要複雜的建構行為,否則建議使用 Object Initializers 來替代多數的 Constructor。因為 Object Initializers 可以為 Public Field 及 Property 設定值,甚至是其他型別物件參與也可以:

⚠️示範多個建構子,程式碼凌亂

public class Person
{
    public string Name { get; set; }
    public DateTime BirthDate { get; set; }

    public Person(string name)
    {
        this.Name = name;
    }

    public Person(DateTime birthdate)
    {
        this.BirthDate = birthdate;
    }

    public Person(string name, DateTime birthdate)
    {
        this.Name = name;
        this.BirthDate = birthdate;
    }

    public int Age { get => (DateTime.Now - BirthDate).Days / 365; }
}

✔️使用 Object Initializers

class Program
{
    static void Main(string[] args)
    {
        var webber = new Person { Name = "Webber", BirthDate = new DateTime(2002, 6, 25) };
        Console.WriteLine($"{webber.Name}'s age : {webber.Age}");        
    }
}

public class Person
{
    public string Name { get; set; }
    public DateTime BirthDate { get; set; }
    public float Age { get => (DateTime.Now - BirthDate).Days / 365; }
}

Params Modifier

讓 Method 可以支援不定數量的 arguments。

public int Count(params int[] numbers)
{
    return numbers.Sum();
}

object.Count(10, 20, 30);
object.Count(10, 20, 30, 40, 50);

Indexer

public class HttpCookie
{
    private readonly Dictionary<string, string> _dictionary;
    public HttpCookie()
    {
        _dictionary = new Dictionary<string, string>();
    }
    public string this[string key]
    {
        get { return _dictionary[key]; }
        set { _dictionary[key] = value; }
    }
}

var cookie = new HttpCookie();
cookie["over18"] = "1";
Console.WriteLine(cookie["over18"]);

Favour Composition over Inheritance

Inheritance 的缺點:

  • 容易被濫用,造成日後維護上的不便(牽一髮動全身)
  • 高耦合

Composition 的優點:

  • Reuse Code as Inheritance
  • 低耦合
// Inheritance
public Car : Vehicle
{

}

// Compisitoin
public Car
{
  public system EngineSystem {get; set;}
  public system DriveSystem {get; set;}
}

Constructors and Inheritance

  • 建構子不會被子類別繼承,必須重新為子類別定義建構子
  • 當建立物件實體時,會先執行父類別的建構子、再依序執行至子類別的建構子
  • 可以使用 base 來指定執行父類別的建構子
public class Vehicle
{
    public Vehicle()
    {
        Console.WriteLine("vehicle is constructed");
    }

    public Vehicle(string materialName)
    {
        Console.WriteLine($"vehicle is constructed with material : {materialName}");
    }
}

public class Car : Vehicle
{
    public Car(string name) : base("ZMC")
    {
        Console.WriteLine($"car is constructed : {name}");
    }
}

var car = new Car("Neo-TriDagger");
// vehicle is constructed with material : ZMC
// car is constructed :

Upcasting & Downcasting

  • Upcasting : 將子類別轉型為父類別,會失去子類別專有的屬性與方法,與多型(Polymorphism) 結合使用
  • Downcasting : 將父類別轉型為子類別,取得子類別專有的屬性與方法
Vehicle vehicle = new Car(); // Upcasting
Car car = (Car)vehicle; // Donwcasting

Boxing and Unboxing

  • C# types 分為 value types(int, c) 與 reference types.
    • Value types (如 int, char, bool, all primitive types and struct) are stored in the stack.
    • Reference types (如類別) are stored in the heap.
  • 因為 object class 是所有類別的父類別 (parent of all classes) 所以 value type instance 可以被隱含地 (implicitly) 轉換為 object reference。
  • Boxing 是將 value type instance 轉換為 object reference.
  • Unboxing 則相反,將 object reference 轉換為 value type.
  • Boxing & Unboxing 轉換上有效能的成本,當轉換的數量大的時候影響會非常顯著

Where Should I Use It

例如 ArrayList collection 僅接受物件 (reference type) 當作其元素,因此必須藉由 boxing 來將 value type 存入其中。

MSDN - Boxing 和 Unboxing

Virtual & Override Modifier

用以表示父類別中的方法可以被子類別覆寫 (Override),藉由 Method Override 可以實踐 Polymorphism ,避免 procedural switch statements or conditionals,並且讓類別保持 Open Closed Principle (對於需求保持開放實作新程式碼的彈性;對於修改既有程式碼保持封閉的避免)。

⚠️Override 的函式必須要與父函式有相同的簽名。

public class Shape
{
    public virtual double GetArea()
    {
        throw new NotImplementedException();
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double GetArea()
    {
        return Radius * Radius * Math.PI;
    }
}

public class Triangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double GetArea()
    {
        return Width * Height * 0.5;
    }
}

var shapes = new List<Shape>
{
    new Circle { Radius = 3 },
    new Triangle { Width = 5, Height = 3 }
};

foreach (var shape in shapes)
{
    Console.WriteLine(shape.GetArea());
}

MSDN - Virtual
MSDN - Override


Sealed Modifier

可以令屬性或者方法無法被衍生類別覆寫。一般而言父類別沒有加上 virtual 修飾就無法被衍生類別給覆寫,但覆寫除了受 virtual 所允許外,父類別覆寫自祖父類別的 virtual 或者實作祖父類別的 abstract 都可以被子類別覆寫,因此使用的時機便是如果要覆寫結束在父類別,便可以在父類別的實作上加上 sealed,避免子類別再次覆寫或者實作。

class X
{
    protected virtual void F() { Console.WriteLine("X.F"); }
    protected virtual void F2() { Console.WriteLine("X.F2"); }
}

class Y : X
{
    sealed protected override void F() { Console.WriteLine("Y.F"); }
    protected override void F2() { Console.WriteLine("Y.F2"); }
}

class Z : Y
{
    // Attempting to override F causes compiler error CS0239.
    // protected override void F() { Console.WriteLine("Z.F"); }

    // Overriding F2 is allowed.
    protected override void F2() { Console.WriteLine("Z.F2"); }
}

MSDN - Sealed
MSDN - Abstract

Interface

  • Interface 作為一種宣告,其成員中的方法在宣告時不實作 (implementation),而其所宣告成員包括方法 (methods) 及 屬性 (properties), 但不包括 fields (因為 fields 是關於細節的實作)
  • Interface 的屬性及方法成員沒有存取修飾詞
  • Interfaces 是建造低耦合應用程式的利器,藉由 interfaces 如果互相關係的類別之一改變了,只要介面仍保持一致,對於依賴 (dependent) 它的類別能不受影響
public interface ITaxCalculator
{
    int Calculate();
}

public class TaxCalculator : ITaxCalculator
{
    public void Calculate() {}
}
public interface IPizzaIngredientFactory
{
    Dough CreateDough();
    Sauce CreateSauce();
    Cheese CreateCheese();
    List<Veggies> CreateVeggies();
    Pepperoni CreatePepperoni();
    Clams CreateClam();
}

Testability

為了達成單元測試,類別之間不能有依賴,藉由 interface 可以處理類別之間依賴的問題。

Extensibility

藉由實作介面可以擴充程式碼(新類別),而不需要修改既有的程式碼。對於依賴介面的既有類別,新實作的類別可以透過實作介面,成為既有類別在執行期間的新演算邏輯,而不需要異動既有的程式碼結構。

Inheritance

Interface 不是為了解決多重繼承而出現的,interface 提供了程式碼的方法的宣告,但實作必須由類別所完成,因此不是繼承的關係。

三個練習題,重構教我們的事

StopWatch

  1. 釐清 property 是否有必要?注意物件的封裝是否使用 private field 即可以滿足演算邏輯,避免資訊的過度暴露造成可能造成使用上的非預期的問題
  2. if 先處理例外狀況,正常流程則放在 else
if (booleanCondition)
{
    throw new Exception();
}
else
{
    ...
}

Stack

  1. 注意程式碼是否有冗餘的部分(沒有也不影響演算邏輯),移除它。
  2. 注意命名的習慣 (conventions)

Workflow Engine

  1. 回傳的型別可以藉由 Interface 來縮限外界能夠操作的範圍,並確保外界使用公開的函式以符合預期。例如回傳 IEnumerable 型別,保護 IList ,讓外界無法直接使用 Add, Remove 等方法,必須透過公開的操作函式來進行操作。

物件導向的操作

多數的時候透過按下 Alt + Enter 讓 IDE 來提示最佳化的物件導向重構。

  • private field 命名慣例 _variable
  • private field 如果是 reference instance 加上 readonly
  • 將 Property <=> Auto Property <=> Field & Method 等型式之間做轉換
  • 抽出 Class 為 Class.cs 獨立檔案