2022年10月20日 星期四

Angular Architecture

首先我們想想為什麼需要「明確的」前端架構?很多時候我們都是希望快速應付使用者或客戶需求,在先求有再求好的心態下,草草開發上線,這樣的做法看似成本很低廉,但是這些作為,卻讓我們在後續的維護、修復和功能擴充上吃足苦頭,付出高昂的代價,無法騰出更多時間去做其他更有價值的事。

在參考很多文章和框架後,歸納出幾點要注意的:

  • 首先,我們希望以後的系統能易於維護,也易於擴充
  • 其次,我們希望很多可以元件或工具可以重複使用
  • 最後,我們得注意執行效能,建置時只導入你需要的套件,runtime時只載入你需要的功能
基於以上幾點,我們開始思考,應該怎麼做,第一個引起注意的,是我們需要分離一些關注點,譬如可以把很多的程序,盡可能地分解成更多個小單元,就像樂高積木一樣,由很多很小的積木,去組成一個大的成品。而這其中,又分成水平分割以及垂直分割,

  • 水平分割:類似於MVC這樣的概念,希望每個面向各司其職。
┃Presentation Layer:專注於使用者互動(UI/UX)
┃Business Layer:系統的業務邏輯應用領域
┃Resource Layer:數據資料流

  •  垂直分割:把應用程序中的相同功能或子系統相關的功能放在一起,希望能更易於後續維護。
┃Module A┃Module B┃Module C ...

            譬如ModuleA負責病患掛號作業,Module B負責醫師看診作業,ModuleC負責藥劑作業等


接著動手Angular專案建置未來的開發架構,並嘗試把架構畫下來,並做說明。

可以看到App module中包含了core module、shared module和更多的project modules,這樣的概念如同上述的垂直切割,我將分別說明其功用。


  • core module:整個架構裡底層的工具,譬如Http發送request 和response,攔劫用的interceptor加工等
  • shared module:存放共享的工具,如組件、服務、指令等
  • project module:可能是一個子系統或功能,需要import core module和shared module,在project 跟project 之間應該要彼此獨立。
在每一個project裡,又做了更進一步的規劃,包含了views、services、models、pipes、...,這樣的概念如上述的水平切割,
  • views:主要進行ui/ux的設計
  • services:業務邏輯、資料處理
  • models:定義資料架構、類別

如此一來,接手的人只要了解系統架構,就能更明瞭整個專案運作方式,想要擴充功能、維護繼有作業,也能有更好的方向。

補充,至於一開始談到的第三點,後來使用lazy loading的方式載入子系統,只載入需要的,藉以解決龐大系統問題。

最後附上整個開發架構的目錄結構,希望對大家有幫助。


|—–app

|—–core module

—–core_module.ts

|—–shared module

|—–services

|—–views

|—–models

|—–pipes

|—–directives

—–shared.module.ts

|—–project1 module

|—–services

|—–views

|—–models

|—–pipes

|—–directives

—–project1.module.ts

—–project1-routing.ts

|—–project2 module

|—–services

|—–views

|—–models

|—–pipes

|—–directives

—–project1.module.ts

—–project1-routing.ts

—–app.component.html

        —–app.component.css

—–app.component.ts

        —–app.modules.ts

        —–app-routing.module.ts



2019年3月21日 星期四

C# 執行緒(Thread)的CountdownEvent探討

CountdownEvent主要是用來等待直到一定數量的執行緒完成。

以下範例有三個執行緒,但等待兩個完成後即可繼續往下執行。

程式碼如下:

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        //等待一定數量完成才繼續往下執行,在這邊設定等待2個執行緒完成
        static CountdownEvent _event = new CountdownEvent(2);

        static void PerformOperation(string message,int seconds)
        {
            try
            {
                Thread.Sleep(TimeSpan.FromSeconds(seconds));
                Console.WriteLine(message);
                //發送信號計數+1
                _event.Signal();
            }
            catch(System.ObjectDisposedException)
            {
                Console.WriteLine("CountdownEvent has disposed");
            }
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => { PerformOperation("operation 1 is completed.",2); });
            t1.Name = "t1";
            Thread t2 = new Thread(() => { PerformOperation("operation 2 is completed.", 4); });
            t2.Name = "t2";
            Thread t3 = new Thread(() => { PerformOperation("operation 3 is completed.", 12); });
            t3.Name = "t3";

            t1.Start();
            t2.Start();
            t3.Start();

            //開始等待完成
            _event.Wait();
            Console.WriteLine("有兩個已完成!");
            _event.Dispose();
            Console.WriteLine("繼續往下執行...");
            Console.ReadKey();
        }
    }
}

打印結果如下:

2019年3月11日 星期一

C# 執行緒(Thread)的AutoResetEvent及ManualResetEvent探討

AutoResetEvent及ManualResetEvent都是可以讓執行緒藉由發出訊號,與彼此進行通訊,控制執行緒暫停或繼續,就像高速公路上的閘道一樣,控制關閉或通行,初始的bool變量,來指明閘道的狀態,true表示可以通行、false表示不可通行。閘道主要有三個方法:WaitOne、Set和Reset。他們就像閘道一樣控制開關,控制執行緒暫停或繼續,Set()表示繼續、WaitOne表示暫停。而這兩者的主要差別在於:

AutoResetEvent

  • 只會給一個執行緒發送訊號
  • Set()後狀態設置會自動設置成false
ManualResetEvent
  • 給所有執行續發送訊號
  • Set()後狀態設置會設置成true,需要手動呼叫Reset()將狀態改為false,在狀態為true時候,WaitOne是沒有作用的
AutoResetEvent.Set() = ManualResetEvent.Set() + ManualResetEvent.Reset();


以下是程式碼:

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        private static AutoResetEvent _guard1Event = new AutoResetEvent(false);
        private static AutoResetEvent _guard2Event = new AutoResetEvent(false);
        private static ManualResetEvent _gateEvent = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            /** *******************
             * Thread的AutoResetEvent機制
             * ********************/
            var t = new Thread(() => Process(10));
            t.Start();

            Console.WriteLine("1.Waiting for another thread completed its work.");
            //等待通行信號
            _guard2Event.WaitOne();
            Console.WriteLine("5.First operation is completed.");
            Thread.Sleep(TimeSpan.FromSeconds(5));
            //發送信號通知正在等待的Thread工作已經完成
            _guard1Event.Set();
            Console.WriteLine("6.Second operation is Starting.");
            //等待通行信號
            _guard2Event.WaitOne();
            Console.WriteLine("9.Second operation is completed.");
            Console.ReadKey();

            Console.WriteLine("---------------------------------");


            /** *******************
             * Thread的ManualResetEvent機制
             * ********************/
            var t1 = new Thread(() => TravelThroughGates("thread 1",5));
            var t2 = new Thread(() => TravelThroughGates("thread 2", 7));
            var t3 = new Thread(() => TravelThroughGates("thread 3", 9));

            t1.Start();
            t2.Start();
            t3.Start();
            Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine($"the gates are open now.");
            _gateEvent.Set();
            Console.WriteLine($"the gates have been closed.");
            _gateEvent.Reset();
            Thread.Sleep(TimeSpan.FromSeconds(5));
            Console.WriteLine($"the gates are open for a while.");
            _gateEvent.Set();
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine($"the gates have been closed.");
            _gateEvent.Reset();
            Console.ReadKey();
        }

        static void Process(int seconds)
        {
            Console.WriteLine("2.Start running");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine("3.Work is done.");
            //發送信號通知可通行
            _guard2Event.Set();
            Console.WriteLine("4.Waiting for a main thread completed its work.");
            //等待信號主程序給通行信號
            _guard1Event.WaitOne();
            Console.WriteLine("7.Start second operation");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine("8.Work is done.");
            _guard2Event.Set();
        }

        static void TravelThroughGates(string threadName, int seconds)
        {
            Console.WriteLine($"{threadName} is falling asleep");
            //開放通行
            _gateEvent.Set();
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine($"{threadName} is waiting for gates to open");
            //等待通行信號
            _gateEvent.WaitOne();
            Console.WriteLine($"{threadName} entered the gates");
        }
    }
}

這是執行結果:


2019年3月5日 星期二

C# 利用iTextSharp合併pdf

using iTextSharp.text;
using iTextSharp.text.pdf;


/// <summary>
/// 合併PDF
/// </summary>
/// <param name="fileList">被合併的文件集合</param>
/// <param name="outMergeFile">合併文件路徑</param>
/// <param name="iFlag">0:A4直印, 1:A4橫印</param>
public void MergePDFFiles(string[] fileList, string outMergeFile, int iFlag)
{
    PdfReader reader;
    Document document = new Document();

    if (iFlag != 0)
    {
        document = new Document(iTextSharp.text.PageSize.A4.Rotate());
    }

    PdfWriter writer = PdfWriter.GetInstance(document, new FileStream(outMergeFile, FileMode.Create));

    document.Open();

    PdfContentByte cb = writer.DirectContent;
    PdfImportedPage newPage;

    for (int i = 0; i < fileList.Length; i++)
    {
        if (fileList[i] != null && fileList[i] != string.Empty)
        {
            reader = new PdfReader(fileList[i]);
            int iPageNum = reader.NumberOfPages;

            for (int j = 1; j <= iPageNum; j++)
            {
                document.NewPage();
                newPage = writer.GetImportedPage(reader, j);
                cb.AddTemplate(newPage, 0, 0);
            }
        }
    }

    document.Close();
}



/// <summary>
/// 合併PDF 自動判斷方向
/// </summary>
/// <param name="fileList">被合併的文件集合</param>
/// <param name="outMergeFile">合併文件路徑</param>
public void MergePDFFiles(string[] fileList, string outMergeFile)
{
    int f = 0;

    PdfReader reader = new PdfReader(fileList[f]);
    Document document = new Document();

    document = new Document(reader.GetPageSizeWithRotation(1));

    PdfWriter writer = PdfWriter.GetInstance(document, new FileStream(outMergeFile, FileMode.Create));

    document.Open();

    PdfContentByte cb = writer.DirectContent;
    PdfImportedPage newPage;
    int rotation = 0;

    for (int i = 0; i < fileList.Length; i++)
    {
        if (fileList[i] != null)
        {
            reader = new PdfReader(fileList[i]);
            int iPageNum = reader.NumberOfPages;

            for (int j = 1; j <= iPageNum; j++)
            {
                document.NewPage();
                newPage = writer.GetImportedPage(reader, j);
                rotation = reader.GetPageRotation(j);
                if (rotation == 90 || rotation == 270)
                {
                    cb.AddTemplate(newPage, 0, -1f, 1f, 0, 0, reader.GetPageSizeWithRotation(i).Height);
                }
                else
                {
                    cb.AddTemplate(newPage, 1f, 0, 0, 1f, 0, 0);
                }
            }
        }
    }
}

2019年3月4日 星期一

C# 執行緒(Thread)的Mutex及Semaphore探討

Mutex
作用在不同Process之間,同一時間內只授予一個Thread在共享資源的獨佔訪問。就好像是馬桶一樣,一次只能有一個人用,下一個想用馬桶的人只能等待。
 Semaphore
限制在同一時間內,允許訪問共享資源的Thread數量上限。就像百貨公司的廁所一樣,假設共有10間,一次最多就只能10個人同時上廁所,其餘想上廁所的人就得排隊等待。

以下是 Mutex範例:

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            /** *******************
            * Thread的Mutex機制
            * ********************/
            string mutexName = "互斥量";
            using (var mutex = new Mutex(false, mutexName))
            {
                if (mutex.WaitOne(TimeSpan.FromSeconds(5), false))
                {
                    Console.WriteLine("占用一下");
                    Console.ReadLine();
                    mutex.ReleaseMutex();
                }
                else
                {
                    Console.WriteLine("我搶不到");
                }
            }
            Console.WriteLine("Hello World!");
            Console.ReadLine();
        }
    }
}

同時執行兩個Process時,第一個Process會看到占用一下,第二個Process會看到我搶不到,


以下是 Semaphore範例:

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(3);

        /// <summary>
        /// 在上廁所
        /// </summary>
        /// <param name="seconds"></param>
        static void Toilet(int seconds)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}等待中");
            _semaphoreSlim.Wait();
            Console.WriteLine($"{Thread.CurrentThread.Name}上廁所");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine($"{Thread.CurrentThread.Name}上完了!");
            _semaphoreSlim.Release();
        }

        static void Main(string[] args)
        {
            // 假设廁所只有3個,但有5人要上廁所,所以有2位需要等待
            for (int i = 0; i < 5; i++)
            {
                Thread t = new Thread(() => {
                    Toilet(new Random().Next(2, 4));
                });
                t.Name = $"t{i}";
                t.Start();
            }

            Console.ReadKey();
        }
    }
}

執行結果如下:
廁所有三間,一開始 t0, t1, t4先上廁所,等 t0上完廁所了,t2才進入上廁所狀態。

C# 執行緒(Thread)的Mointer探討

Mointer跟Lock一樣,具有「鎖」的功用,用來確保在critical section中只有一個執行緒能夠執行,使用Monitor.Enter()進入鎖定,Monitor.Exit() 退出鎖定,他其實就是Lock機制的應用:

bool lockWasTaken = false;
var temp = obj;
try
{
    Monitor.Enter(temp, ref lockWasTaken);
    { body }
}
finally
{
    if (lockWasTaken) Monitor.Exit(temp);
}

另外他還提供了其他方法以供運用:
  • Monitor.Enter() : Acquires an exclusive lock on the specified object. This action also marks the beginning of a critical section.
  • Monitor.Exit() : Releases an exclusive lock on the specified object. This action also marks the end of a critical section protected by the locked object.
  • Monitor.Pules() : Notifies a thread in the waiting queue of a change in the locked object's state.
  • Monitor.Wait() : Releases the lock on an object and blocks the current thread until it reacquires the lock.
  • Monitor.PulesAll() : Notifies all waiting threads of a change in the object's state.
  • Monitor.TryEnter() : Attempts to acquire an exclusive lock on the specified object.

2019年3月1日 星期五

C# 執行緒(Thread)的Lock及Interlocked探討

同時啟動不同執行緒,但是對同一塊記憶體空間做內容的改變,可能會影響該內容的正確性(Race Condition),此時我們需要使用Lock來控制,當該執行緒執行時,其他執行緒無法使用這個被Lock的資源,以確保資料的正確性。另外C#也提供了一個Interlocked的方式,讓你能夠在Increment、Decrement、Add等基本數學操作不使用Lock的狀況下,保持結果正確,

以下測試的概念是對同一個記憶體空間做++和--,正確的結果應該為0,但若沒有Lock或Interlocked機制,不同執行緒會更改同一塊記憶體的值,譬如threadOne執行了++,尚未執行--,而threadTwo此時又執行++,這時候就會出現誤差,導致最終結果錯誤。

執行10次的結果,Counter每次都不一樣,但CounterWithLock及CounterNoLock則每次都為0。

以下為程式代碼:

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            /** *******************
             * Thread的Lock機制
             * ********************/

            //沒有使用Lock
            var c = new Counter();
            var threadOne = new Thread(() => CountNumbers(c));
            var threadTwo = new Thread(() => CountNumbers(c));
            var threadThree = new Thread(() => CountNumbers(c));
            threadOne.Start();
            threadTwo.Start();
            threadThree.Start();
            threadOne.Join();
            threadTwo.Join();
            threadThree.Join();
            Console.WriteLine($"Total is {c.Count}");

            //使用Lock
            var c1 = new CounterWithLock();
            threadOne = new Thread(() => CountNumbers(c1));
            threadTwo = new Thread(() => CountNumbers(c1));
            threadThree = new Thread(() => CountNumbers(c1));
            threadOne.Start();
            threadTwo.Start();
            threadThree.Start();
            threadOne.Join();
            threadTwo.Join();
            threadThree.Join();
            Console.WriteLine($"Total is {c1.Count}");

            //使用Interlocked
            var c2 = new CounterNoLock();
            threadOne = new Thread(() => CountNumbers(c2));
            threadTwo = new Thread(() => CountNumbers(c2));
            threadThree = new Thread(() => CountNumbers(c2));
            threadOne.Start();
            threadTwo.Start();
            threadThree.Start();
            threadOne.Join();
            threadTwo.Join();
            threadThree.Join();
            Console.WriteLine($"Total is {c2.Count}");
        }

        static void CountNumbers(CountBase cnt)
        {
            for (int i = 0; i <= 100000; i++)
            {
                cnt.Increase();
                cnt.Descrease();
            }
        }
    }

    abstract class CountBase
    {
        public abstract void Increase();
        public abstract void Descrease();
    }
    class Counter : CountBase
    {
        private int _count;
        public int Count { get { return _count; } }
        public override void Increase()
        {
            _count++;
        }
        public override void Descrease()
        {
            _count--;
        }
    }
    class CounterWithLock : CountBase
    {
        private int _count;
        public int Count { get { return _count; } }
        private readonly object _SyncRoot = new Object();
        public override void Increase()
        {
              //lock的對象必須是object(但不要lock string),不能是int, long等數值型
              //若為public函數,不要lock(this),不然很容易造成deadlock
            lock (_SyncRoot)
            {
                _count++;
            }
        }
        public override void Descrease()
        {
            lock (_SyncRoot)
            {
                _count--;
            }
        }
    }
    class CounterNoLock : CountBase
    {
        private int _count;
        public int Count { get { return _count; } }
        public override void Increase()
        {
            Interlocked.Increment(ref _count);
        }
        public override void Descrease()
        {
            Interlocked.Decrement(ref _count);
        }
    }
}