[Day4] C# MVC的程式架構淺談 - C#&AspNetCore

這篇主要檢討上回 [Day3] 建立C# MVC專案 - C#&AspNetCore 透過.net core 3.1版本建立出的程式碼架構。

檔案架構

  • wwwroot資料夾: 是放置靜態檔案如js、css等前台需要的檔案
  • Controllers資料夾: 是放置MVC的控制器Controller的地方,與使用者請求並給予回應有關
  • Models資料夾: 是放置MVC的控制器Models的地方,與資料庫有緊密關係
  • Views資料夾: 是放置MVC的控制器Views的地方,在頁面上和使用者互動的畫面
  • appsetting.json 設定檔案。
  • Program.cs 主要的進入點Main的所在,在此建立Host。
  • Startup.cs 在這裡可以設定網站的啟用設定,如添加中介層(Middleware)

程式碼探究

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
  • Main:程式碼進入點,並在裡面呼叫CreateHostBuilder,建立初始化的IHostBuilder。之後,Build建立實體,再透過Run運行。
  • CreateDefaultBuilder:預設為空參數,也可以自行添加。
  • ConfigureWebHostDefaults:其中包含用來裝載 web 應用程式的預設值
  • UseStartup:指定要供 web 主機使用的啟動類型,這裡套用Startup.cs的程式碼,也可以自訂Class然後套用至此。

小結:透過一個Console程式運行網站伺服器,可在此設定一開始要執行什麼、套用什麼設定。

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// 添加服務
services.AddControllersWithViews();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

  • Startup:建構式,程式進入點,裡面設定configuration(如:appsetting.json的資料就會在此被帶進來)。
  • ConfigureServices:設定添加的服務。AddControllersWithViews,主要用來設定添加MVC服務,舊版是AddMvc。
  • Configure:設定HTTP request pipeline,針對http請求進行設定。
    • IsDevelopmen:套用開發版/上線版兩種模式。
    • UseHsts:將 HTTP Strict 傳輸安全性通訊協定 (HSTS) 標頭傳送給用戶端
    • UseDeveloperExceptionPage:使用開發者例外頁面
    • UseExceptionHandler:例外時導向某頁
    • UseHttpsRedirection:強制導向HTTPS
    • UseStaticFiles:會在中呼叫方法 Startup.Configure,以啟用靜態檔案的服務
    • UseRouting:將路由對應新增到Middleware中
    • UseAuthorization:授權使用端點路由傳送的資源時,此呼叫必須出現在對應用程式的呼叫之間 。UseRouting () 和 應用程式。UseEndpoints ( … ) ,讓中介軟體能夠正常運作。
    • UseEndpoints:Middleware提供了endpoints可以擴充一些方法去針對route去做設定

小結:添加哪些服務、設定一些初始架構(HTTP協定、開發版/上線版)、中介層設定都在這Startup.cs裡。

appsettings.json

1
2
3
4
5
6
7
8
9
10
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

小結:在此建立的值可以在Startup.cs被讀取,通常是一些外部字串設定檔,像是舊版的webconfig

HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
return View();
}

public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
  • ILogger:提供寫入log的介面,在 ASP.NET Core 統一透過 ILogger 來記錄 Log。
  • Index()、Privacy():是Controller裡的action,用來載入的View資料夾的頁面。
  • ResponseCache:設定 Response 的暫存方式。並套用到要使用 Response 快取的 Controller 或 Action。以此就是套用到Error()。
  • Error():錯誤時返回的頁面,這邊回傳了ErrorViewModel模型格式的內容。
    [補充]C#問號用法:
    1
    2
    int? x = null;//定義可空型別變數
    int? y = x ?? 1000;//使用合併運算子,當變數x為null時,預設賦值1000

小結:控制器可依需求返回頁面、可在此紀錄LOG。

ErrorViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
using System;

namespace WebApplication1.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}
  • ErrorViewModel:建立Class
  • RequestId:model中的string值
  • ShowRequestId:model中的bool值,返回一個lambda函式結果

給View用的Model,跟MVVM概念中所提到的ViewModel是不一樣的
在MVC的View中,一個View Binding一個Model,如果要使用多個不同的資料表就會用到View Model。

小結:在此建立一些模型規格,供頁面呈現使用。

Index.cshtml

1
2
3
4
5
6
7
8
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
  • ViewData:引用System.Web.Mvc.ViewDataDictionary,屬於Dictionary Object,能放Property或是一個Model的IEnumerable,使用Key/Value的概念存取。生命週期為一個頁面
  • @{}區塊:Razor語法,可以在裡面寫C#語法,但建議前台盡量少業務邏輯、只以純顯示為幕的比較好。

小結:在此可以寫html、前端畫面。

Privacy.cshtml

1
2
3
4
5
6
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
  • @ViewData:可用Razor語法叫出值顯示。

小結:同 Index.cshtml

_Layout.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplication1</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2021 - WebApplication1 - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>

  • @ViewData[“Title”]:底下頁面PASS過來的Title。
  • @RenderBody():把整塊子頁面放進來。
  • @RenderSection():建立放js區塊的程式碼區域,required表示是否一定要提供。

小結:共用的頁面主架構。

_ValidationScriptsPartial.cshtml

1
2
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

小結:放切出來的區塊。

Error.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

@model:Razor語法,可以使用model的物件
小結:套用了ErrorViewModel,主要印出ErrorViewModel內的資料。

_ViewImports.cshtml

1
2
3
@using WebApplication1
@using WebApplication1.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

小結:設定使用那些程式模組

_ViewStart.cshtml

1
2
3
@{
Layout = "_Layout";
}

小結:頁面進入點套用。

總結

從程式碼片段中可以得知整個架構上如何運作。
從一開始的Console建置啟動程式、Startup設定,至單個頁面程式碼符合MVC架構的撰寫,最後運行起整個系統。

因為MVC所以知道這設計模式的人就能很快的熟悉這架構的寫法與邏輯。
如果是自幹的專案,就會有這地方是什麼架構、要怎麼改的才能達到功能的各種歪路。

預計下回會針對在Model、View、Controller上的細節程式碼撰寫。

參考資料
https://www.tutorialsteacher.com/core/aspnet-core-startup
https://blog.johnwu.cc/article/asp-net-core-3-application-lifetime.html
https://ithelp.ithome.com.tw/articles/10240077
https://docs.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder?view=dotnet-plat-ext-5.0
https://ithelp.ithome.com.tw/articles/10243332
https://ithelp.ithome.com.tw/articles/10186520

[Day3] 建立C# MVC專案 - C#&AspNetCore

在上回 [Day2] 什麼是MVC,有什麼好處 - C#&AspNetCore 中我們理解到的MVC的核心概念。
在這回中我們要利用Visual Studio來建立一個C# MVC專案,並運行起來。

安裝

Visual Studio Community 2019:
https://visualstudio.microsoft.com/zh-hant/downloads/

建立專案

1.下載完後就可以開啟Visual Studio建立專案,New>Project

2.選ASP.NET Core Web App(Model-View-Controller),不然會建立出傳統沒有設計模式(Design Pattern)架構的專案。

3.設定專案名稱及資料夾

4.Enable Razor runtime compilation 執行期編譯要記得打勾,不然每次要重新執行才能看到修改的結果。

5.專案建立

6.可看見Model、View、Controller

7.點下Express執行

8.成功運行在網頁上了!

總結

建立MVC專案成功!
相對於PHP的LARAVEL框架來說,這套建立起來的MVC系統比較輕量,也就代表未來擴充的程式碼要設計好,否則量大起來加上多人協作可能就會有難以維護的問題。
為了在維護、co-work好改,之後也會注重在設計模式上。
最後,對於微軟程式只要能用IDE運行起來就成功一半了。因為程式碼除錯、追蹤功能強大。

基礎資料結構

資料結構:

  1. 如何在電腦中儲存資料。
  2. 是一種工具,如:演算法搭配適合的資料結構。

選擇不同的資料結構:

  1. 時間複雜度:非常頻繁拿、取,希望時間可以短一點。
  2. 空間複雜度:需要另外用空間來加速,那空間可能要比較多。
  3. coding複雜度:比如有兩種結構可以實作這程式,那選較容易、不容易出錯的那個。

常見操作

push放入一個元素、pop取出一個元素、query查詢

堆疊

堆疊(stack,FILO,first in last out),先進後出,如碟盤子。
實作搭配linked list
push:在head插入一個節點
pop:刪除head指向的節點
時間複雜度:
push:O(1)
pop:O(1)

佇列

佇列(queue,FIFO,first in first out),先進先出,如排隊。有環狀、單向之分。
實作搭配linked list或環狀佇列。
push:在end後面插入一個節點
pop:刪除head指向的節點
時間複雜度:
enqueue:O(1)
dequeue:O(1)

鏈結串列

鏈結串列(linked list)。
基本單元:Node(data+pointer)
將很多Node接起。
head指向第一個node,最後一個指標指向NULL。

性質:

  1. 每個元素只記錄他下一個元素,外部只記錄起點(head)
  2. 動態宣告記憶體,需要時就要,不需要就還給系統
  3. 分類:單向、雙向、環狀
  4. 不能隨機存取,因為外部只記錄起點(head),所以只能從頭去一個一個下去找,第一個告訴我第二個在哪,第二個告訴我第三個在哪…。

操作:
insert新增元素,在兩個元素間,或是頭、尾插入
delete刪除元素

時間複雜度:
insert:O(1),只要重新更改pointer指向
delete:O(1),只要重新更改pointer指向

為什麼不用陣列,還能隨機存取?
1.避免記憶體浪費,但array的dynamic array也能做到
2.可以快速刪除、插入節點,這array沒辦法做到。

下面兩個是延伸,不過要控制的資料變多,可能維護下來除起bug的機率會高。

雙向鏈結串列
環狀鏈結串列

時間複雜度比較

此外,兩個linked list合併速度很快,因為只要指向另一個的頭即可,只要O(1)。

總結

從這章中理解到為什麼要用資料結構,選用適合的資料結構可以改善程式的速度、程式碼難易度。在前面這幾篇中預計以理解原理為主,底層實作則未來有時間再另外拿出來寫。

如果是頻繁讀取,可以用array;如果頻繁新增、刪除,可以用linked list。

參考資料
https://www.youtube.com/watch?v=EH5XO2iYJvM

Big-O 複雜度分析

好程式的迷思

一個程式好不好通常看你要解決的問題。

  1. 時間花得少(運行速度) ex:網頁跑很快、資料載入快
  2. 空間花得少 ex:硬體不夠充裕,要使用比較少記憶體
  3. 正確率較高 ex:防毒軟體隊攻擊作防範、更高機率偵測
  4. 相容性較高 ex:好延伸擴充、裡面程式太複雜造成很難擴充
  5. 開發成本低 ex:開發軟體時,很快的做法但寫起來很困難,普通速度的作法但寫起來容易。

所以要適當的平衡,並不是哪種一定是好。
相容性、開發成本,通常仰賴當下開發者評估。
正確率則是在實作的細節上、Case量、理論分析上。

複雜度概念

把所有操作都視為一樣的時間,如:加減乘除、位運算、存取記憶體、判斷運算子…等
把所有操作次數計算出來。
看量級大小,來評估執行時間。有些程式容易TLE超時就是因為這問題,比較在意的是量級,而不是精確的值。

舉例:被指派長期作業,有兩種方案可選:

  1. 每天算10題。
  2. 第一天算1題,第二天算2題,第三天算3題…。

兩種方案怎麼選?
如果每算一題要5秒
第n天:

  1. 10 * n * 5 = 50n
  2. (n+1) * n/2 * 5 = 2.5(n^2+n)

n=5天時,

  1. 250秒
  2. 75秒

n=365天時,

  1. 18250秒
  2. 333975秒

可以發現操作數的量級(複雜)比較多,所以以長期來看會是方案1比較好。

量級概念

成長的速度不同等級。
如f(n)與g(n)兩個函數,當n趨近無窮大,誰會比較大?

  1. 3n^2+n+20 vs 100n 如果n = 10000,左邊會變成億;右邊只有百萬,所以左邊量級大
  2. n^100 vs 2^n
  3. n^2 vs 10nlogn
  4. 100^n vs n! 如果n=200,左邊100倍,右邊成長200倍,所以右邊量級大於左邊。
  5. 30*2^n vs 3^n
  6. 100n vs 200n 同一量級,因為極限的概念

極限概念

如果f(n)/g(n),n趨近無限大時趨近0,那麼說f(n)比g(n)小。
如果f(n)/g(n),n趨近無限大時趨近無限大,那麼說f(n)比g(n)大。
否則稱作兩者一樣量級。
所以,100n/200n = 1/2,所以不管n多大都一樣,稱作一樣量級。
但是,科技進步可以補100n與200n的差異,如平行運算可能可以把100n與200n的作法都視為一樣。但如果不是同一量級,還是會有科技進步彌補不了的,如上面的例子1。

量級的比較

以一台每秒可以進行10億次運算的電腦:

所以好算法、壞算法需要的時間是有很大差距的。

大歐符號 Big-O

記為O(f(n)),f(n)為複雜度量級的”上界”。所以實際結果可能會比他還要小。
常數會省略只寫一個n

  1. 3n^3+5n^2+10n+3 ∈ O(n^3)
  2. f(n) ∈ O(n^2),則f(n) ∈ O(n^3),因為是上界
  3. 1000 ∈ O(1)

ps.最緊上界:如果一個算法同時是O(n^2)算法、O(n^3)算法,通常會取比較小的O(n^2),以避免過度悲觀評估。<<=這句我不太懂

  1. 3n^2+n+20 vs 100n ;O(n^2) vs O(n)
  2. n^2 vs 10nlogn;O(n^2) vs O(nlogn)
  3. 100^n vs n! ;O(100^n) vs O(n!)
  4. 30*2^n vs 3^n ;O(2^n) vs O(3^n)
  5. 100n vs 200n ;O(n) vs O(n)

常見的時間複雜度函式:

如果時間不穩定

如一個人姓名存在資料庫,想找到那個人
如果是排第一個情況,O(1)
如果是排最後一個情況,O(n)

多數關注的是最壞的case,如:插入排序,最好是O(n);最壞是O(n^2)

總結

從參考資料中整理的一些對演算法複雜度的知識。
如果有機會寫到要追求「時間花得少」或「空間花得少」時就能派上用場,但這些通常使用在底層實作上,在應用的層面非常少用到,不過能透過理解複雜度,盡量選擇好的實踐方法。

此外,在應用層面上,相容性較高、開發成本低,我認為比較重要;底層函式庫則是追求時間花得少、空間花得少。所以,看是寫哪個層面的程式,如果兩者反過來會蠻可怕的。

參考資料

https://zh.wikipedia.org/wiki/%E5%A4%A7O%E7%AC%A6%E5%8F%B7
https://www.youtube.com/watch?app=desktop&v=_r7cfVrn28c
https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6

無限階層留言規劃,含資料表設計

這個議題算是只要有留言系統,並且想實作無限層級的留言都會碰到的。
如:Facebook的留言、wordpress的留言。
當然也有簡單類的留言,只有一層的,如:古早的無名小站,但這不是我們今天要探討的。
就單刀直入了,如何實作「無限階層的留言」。

資料表

只要一張comments表加上parent_id即可,如下。
再來就可以利用程式方式(遞迴或迴圈)去印出他。

程式部分

把comments當主表,撈出資料SQL:

1
2
3
4
5
6
7
select
users.name,
comments.id,
comments.comment_parent_id,
comments.title,
comments.content
from comments left join users on comments.user_id = users.id

把上面資料表進行關聯後撈出後的資料:

1
2
3
4
5
6
7
[
{id:1,comment_parent_id:null,title:'留言1',content:'內容',name:'路人A'},
{id:2,comment_parent_id:null,title:'留言2',content:'內容',name:'路人B'},
{id:3,comment_parent_id:2,title:'留言3',content:'內容',name:'路人C'},
{id:4,comment_parent_id:3,title:'留言4',content:'內容',name:'路人D'},
{id:5,comment_parent_id:3,title:'留言5',content:'內容',name:'路人E'}
]

再來就可以實作程式碼了,我們要先把上面SQL撈出來的「打平結構」變成「樹狀結構」。
這會需要使用到遞迴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
</body>
</html>
<script>
const commentList = [
{id:1,comment_parent_id:null,title:'留言1',content:'內容',name:'路人A'},
{id:2,comment_parent_id:null,title:'留言2',content:'內容',name:'路人B'},
{id:3,comment_parent_id:2,title:'留言3',content:'內容',name:'路人C'},
{id:4,comment_parent_id:3,title:'留言4',content:'內容',name:'路人D'},
{id:5,comment_parent_id:3,title:'留言5',content:'內容',name:'路人E'}
]

// 建立樹狀階層
function buildTree(commentList){
// 根節點
const rootNode = commentList.filter(it=> it.comment_parent_id === null)
// 增加子節點
return addNode(rootNode,commentList)
}

// 添加子節點
function addNode(children,commentList){
// 找不到子節點返回
if(children.length === 0){
return []
}
// 添加子節點
const arr = []
// 遍歷當前children節點
for(let i=0;i<children.length;i++){
// 查詢底下的子節點
const childrenNode = commentList.filter(it=>
it.comment_parent_id === children[i].id)
// 把底下子節點添加進陣列
arr.push({
id:children[i].id,
name:children[i].name,
title:children[i].title,
content:children[i].content,
children:addNode(childrenNode,commentList)
})
}
// 返回子節點陣列
return arr
}
// 印出結果
console.log(JSON.stringify(buildTree(commentList)));

</script>

轉成樹狀階層後的資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[
{
"id": 1,
"name": "路人A",
"title": "留言1",
"content": "內容",
"children": []
},
{
"id": 2,
"name": "路人B",
"title": "留言2",
"content": "內容",
"children": [
{
"id": 3,
"name": "路人C",
"title": "留言3",
"content": "內容",
"children": [
{
"id": 4,
"name": "路人D",
"title": "留言4",
"content": "內容",
"children": []
},
{
"id": 5,
"name": "路人E",
"title": "留言5",
"content": "內容",
"children": []
}
]
}
]
}
]

光轉成上面那串,就想了快3小時的遞迴,然後上網找些範例參考才組出來。
我當時的疑問就是:「怎麼讓遞迴去回傳整包陣列」,所幸有找到參考,然後小改自己程式就成功了。

再來就是把上面那階層式資料在網頁上印出來。
還是要用遞迴。

網頁上內容呈現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>

<div id="comment">
</div>

</body>
</html>
<script>
const commentList = [
{ id: 1, comment_parent_id: null, title: '留言1', content: '內容', name: '路人A' },
{ id: 2, comment_parent_id: null, title: '留言2', content: '內容', name: '路人B' },
{ id: 3, comment_parent_id: 2, title: '留言3', content: '內容', name: '路人C' },
{ id: 4, comment_parent_id: 3, title: '留言4', content: '內容', name: '路人D' },
{ id: 5, comment_parent_id: 3, title: '留言5', content: '內容', name: '路人E' }
]

// 建立樹狀階層
function buildTree(commentList) {
// 根節點
const rootNode = commentList.filter(it => it.comment_parent_id === null)
// 增加子節點
return addNode(rootNode, commentList)
}

// 添加子節點
function addNode(children, commentList) {
// 找不到子節點返回
if (children.length === 0) {
return []
}
// 添加子節點
const arr = []
// 遍歷當前children節點
for (let i = 0; i < children.length; i++) {
// 查詢底下的子節點
const childrenNode = commentList.filter(it =>
it.comment_parent_id === children[i].id)
// 把底下子節點添加進陣列
arr.push({
id: children[i].id,
name: children[i].name,
title: children[i].title,
content: children[i].content,
children: addNode(childrenNode, commentList)
})
}
// 返回子節點陣列
return arr
}

// 在網頁上呈現出結果
function generateCommentTemplate(children) {
let template = '<ul>'
for (let i = 0; i < children.length; i++) {
template += '<li>'
template += children[i].title + '<br>'
template += children[i].content + '<br>'
template += children[i].name + '<br>'

// 如果有子節點則遞迴再次呼叫
if (children[i].children.length !== 0) {
template += generateCommentTemplate(children[i].children)
}
template += '</li>'
}
template += '</ul>'
return template
}
// 建立樹狀結構
const tree = buildTree(commentList)
// 產生HTML模板
document.getElementById("comment").innerHTML = generateCommentTemplate(tree)
</script>

輸出結果:

總結

整篇大概花了5小時左右寫完,算是網頁系統裡最難的”遞迴”。
在網路上查,很難有一篇現成完整範例T_T,所以在這篇中就把整套都放上來。
同時練習對於樹的掌握。

流程如下:
1.規劃無限階層的留言 - 約10分鐘
2.樹的組合(遞迴) - 約3小時
3.轉譯成HTML模板(遞迴) - 約30分鐘

總之,有點成就感,因為真的不好寫。
如果後端一開始吐給你的是已經階層化的,那就很輕鬆了,因為那段的遞迴比較簡單。

ps.這留言的實作方法也能用來處理分類、類別、側邊欄,就內文改一下即可。

我有封裝了方法變成一隻.js函式庫,可以直接拿去用。
https://github.com/yuhsiang237/buildTree.js

參考資料
https://blog.51cto.com/dd118/2095671
https://kevintsengtw.blogspot.com/2013/07/aspnet-mvc-jquery-easy-ui-tree-json.html
https://ithelp.ithome.com.tw/articles/10028129
https://itw01.com/UBNZIEU.html

資訊系統的角色權限規劃,含資料表設計

寫後台系統時,有個很複雜的難點在於「角色權限」。
難點在如何變得好擴充,又能讓整個後台系統容易分權。
如:A進到一個頁面,在這頁面只能看,不能編輯。

思考的角度

如果同時思考「頁面權限」和「新刪修查權限」,很容易陷入混亂。
因此,建議先有個 「這個系統主要是要幹嘛」 的概念,說不定根本不用權限,因為使用者就那幾個,或是只到頁面上的分權,先有了使用者需求是什麼,再來確定要用哪種方法。

在此,我想探討的會是「頁面權限」到「新刪修查權限」的作法。
考量到使用者操作的直覺,以「模組>功能>操作權限」這樣排。

如此一來,我們功能只要對到「查看、新增、編輯、刪除」,先解決了細部的部分。
再來就只要把模組與功能分類關聯即可。

資料表規劃

users:使用者,上面綁角色role_id,每個使用者都有一種角色。
roles:角色,用來設定權限用。
module_permissions:模組權限,用來綁定底下功能權限。
func_permissions:功能權限,用來綁底層的權限。
permissions:權限,用來綁func_permissions與permission_types的表。
permission_types:權限分類,可自行定義分類,如:查看、新增、編輯、刪除…等。

ps.資料表上的name欄位也能說是code、key值,就是在網頁上你要抓檢查用的。

以下做的這張表就能進行分權。因為是以功能劃分,所以之後如果有APP或其他前端介面,只要照著「模組>功能」這樣去設計一樣能夠適用。

如果一個A使用者進到「基礎管理>角色管理功能」頁面,只要檢查他的檢視權限有沒有,沒有就返回首頁即可。只要有勾取功能權限,那必有檢視的權限。

如果更新角色時,把module_permissions、func_permissions、permissions有關聯的清空再重新建立即可。

[補充]*代表多,1代表1,可以看出是一對多還多對一
如果想知道更多:[Day7] SQL server資料庫關聯 - SQL Server資料庫入門

更多階層的權限?

如果是巢狀的話也是可以,func_permissions加個func_permissions_parent_id欄位,就能變成巢狀了。
不過以現有系統架構來講,也是能偷吃步直接在name上說是功能A的XXX項目也是可以的。

通常不會有超過5階以上的頁面,不然使用者會迷路、功能分則過細也不好用,即使遇到也是能用上面兩種方法去克服。

然後,建議權限這種不要做成動態的樹,因為哪天要改會很麻煩,建議這種階層還是寫死,變動次數通常不會太多,寫的簡單些未來也好改。

如果是API權限如RESTful API ?

目前我還沒有想到比較好的解法,目前只想到能在送出去前免強撈資料表判斷。
在新、刪、修時都卡,而查則是盡量了,因為RESTful本身就容易查出一些原本不相干的資料,所以不好分權。

總結

花了兩小時左右打完了這篇文跟設計表,實際規劃後覺得權限這部分不算難,只是要想清楚系統到底要做什麼,再來跟PM或SA確定一種可以的結構去開發就行。
如今,網路上也有不同解答,如果有共事的工程師也能一起討論方法,讓這系統能順利產製便可以,千萬別執迷不悟硬幹阿@@…孩子。

然後我是用 https://dbdiagram.io/ 來畫下面那張圖的,很方便。

此外,也可以參考”以角色為基礎的存取控制”(Role-based access control,RBAC)
https://zh.wikipedia.org/wiki/%E4%BB%A5%E8%A7%92%E8%89%B2%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E5%AD%98%E5%8F%96%E6%8E%A7%E5%88%B6

參考資料
http://www.chanpin100.com/article/110136
https://ithelp.ithome.com.tw/questions/10190444
https://zh.wikipedia.org/wiki/%E4%BB%A5%E8%A7%92%E8%89%B2%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E5%AD%98%E5%8F%96%E6%8E%A7%E5%88%B6

自己對自己程式的看法

自己算是一個寫程式的人,做過前端,也實習過後端。

我數學並不強,只會用比較簡單的概念去實作程式。所以在演算法、資料結構上偏向應用居多,我能知道在哪種情況下,可能要搭配的結構,但如果要空手寫出來底層,除非背過,不然要寫很久,有需要才會回去看、參考,然後才實作出來。

因此,我知道以自己的數學能力是沒辦法把程式弄到很頂尖的,可能前20%那種,而後面的80%或許還是有機會。
就是偏向應用、非演算法那塊。

我覺得自己優勢在整合能力,能夠把不同的東西拼起來,可以正常運行、滿足需求。到目前為止,我還沒碰過寫不出來的應用系統。

雖然看網路上很多文人相輕的評論,如:不會寫資結、演算法就不算工程師,但軟體工程師也分很多種,並不是每種都需要很強的資結、演算法。
也許,就只是要穩定的交付可運行的程式而已。

我遇過演算法很強但卻沒辦法在時程內完成案子的工程師,也遇過能力普普但能很穩定能夠完成案子的工程師。

總之,以我觀點來說就是能夠解決問題

而在這片領域,我能解的可能只有80%,而那20%個人天賦影響既然沒辦法改變,那就坦然接受吧!
(就像每人都會寫數學,但又有幾個能變得像高斯或愛因斯坦那樣的開創者)
說來說去,別看得太重而否定自己,不過是要口飯而已。

LeetCode - 35. Search Insert Position

Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.

You must write an algorithm with O(log n) runtime complexity.

 

Example 1:

Input: nums = [1,3,5,6], target = 5
Output: 2

Example 2:

Input: nums = [1,3,5,6], target = 2
Output: 1

Example 3:

Input: nums = [1,3,5,6], target = 7
Output: 4

Example 4:

Input: nums = [1,3,5,6], target = 0
Output: 0

Example 5:

Input: nums = [1], target = 0
Output: 0

 

Constraints:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums contains distinct values sorted in ascending order.
  • -104 <= target <= 104
C# Sol:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Solution {
public int SearchInsert(int[] nums, int target) {
int index = 0;
Boolean isFind = false;
for(int i=0;i<nums.Length;i++){
if(nums[i] == target){
isFind = true;
index = i;
break;
}
}
if(!isFind){
for(int i=0;i<nums.Length;i++){
if(nums[i] > target){
index = i;
break;
}else if(i == nums.Length -1 ){
index = i+1;
}
}
}
return index;
}
}

Runtime: 92 ms
Memory Usage: 24.9 MB

[Day2] 什麼是MVC,有什麼好處 - C#&AspNetCore

在上回 [Day1] 學習C#的計畫與期望 - C#&AspNetCore 中介紹了自己的目的。
在這回中主要介紹MVC的核心概念。

MVC是什麼?

MVC模式(Model–view–controller)是軟體工程中的一種軟體架構模式(software design pattern),把軟體系統分為三個基本部分:模型(Model)、視圖(View)和控制器(Controller)。

模型(Model)

程式設計師編寫程式應有的功能(實現演算法等等)、資料庫專家進行資料管理和資料庫設計(可以實現具體的功能)。
這邊通常放資料存取的屬性、比較複雜的資料處理。
在這個單元裡面,我們通常會定義資料的結構、型態、存取func,必要時會將資料的驗證寫在這個單元中。

視圖(View)

介面設計人員進行圖形介面設計。
簡單來說就是前台,處理HTML/CSS/JS。
將資料視覺化,並呈現給使用者。

控制器(Controller)

負責轉發請求,對請求進行處理。
當使用者進到了前台的視圖(View),對畫面操作所觸發的事件就會跑到控制器。
在控制器裡負責拿到模型(Model)的資料後加上些業務邏輯後返視圖(View)。
負責處理主要商業邏輯處理,資料格式處理,大部分介於Model 與 View 之間,處理資料流的溝通。

我的疑問

Controller與Model很難劃清界限,過去在寫Laravel時就遇到類似問題。
因為在Controller常要對不同的請求,有不同資料回應。
而在Model裡對資料庫操作的存取函式就會滿足不了需求,變成要一直增加。
免不了在Controller操作Model甚至寫SQL,因為這樣比較直覺,不用再去想Model增加可能共用的函式。
如果撇除這問題,MVC架構已經算是不錯了,能夠有架構的避免義大利麵Code的問題。

實現MVC框架

如果選對有Design Pattern的框架在寫的過程比較不會那麼痛苦。因為比較能知道哪邊的東西放在哪邊。
如我上面的疑問,其實不是在Controller就是在Model。好找出問題來源,達成目標才是主要目的。

Java:Spring MVC
C#:ASP.NET MVC
PHP:Laravel

其實還有很多,我這只列出我曾經有碰過或未來可能會碰的。

MVC 優缺點

優點

  1. 好分工,團隊開發能各司其職
  2. 可維護性高(因具備架構性好找錯誤源頭)

缺點

  1. 如果是一頁式簡單網站容易殺雞用牛刀

如果實際開發下去,有寫過MVC跟沒寫過的人應該都會選MVC,因為坑少- -+。
此外,即使要開發前後分離架構,也可以MVC再搭配一套前端框架,作為未來需要特別畫面需求的輔助。

總結

MVC能夠協助一個專案進行有效的分工,讓人比較能各司其職,如:視覺就是專注在View,而工程師專注在Model、Controller。
整體架構上也讓未來維護的人有個底,如果想看這段邏輯寫在哪那就從Controller開始,找不出原因在看Model。
相較於過去,這頁面需要A資料,就把資料查詢、邏輯到回應都寫在一個頁面上難以維護的慘況好很多了@@。

參考資料
https://zh.wikipedia.org/wiki/MVC
https://ithelp.ithome.com.tw/articles/10191216
https://dotblogs.com.tw/dog0416/2016/05/20/131644
https://ithelp.ithome.com.tw/articles/10194428

[Day1] 學習C#的計畫與期望 - C#&AspNetCore

在這個 Challenge中,我想一覽C# .net core MVC的基本做法,如何透過它建立一個小型網頁系統。
並且搭配上一個 Challenge中的SQL Server來作為儲存體。

此外,我想把Laravel的那套做法搬來C#做
https://laravel.com/docs/8.x
如:資料庫交易怎麼rollback、seed、test、orm等。

以及,C#特有的linq寫法。
此外,可能也會加點設計模式的概念作輔助。

為什麼選C#

  1. 因為遊戲開發沒人在用php
  2. Unity是C#
  3. 有比較完整的學習地圖
  4. 在台灣工作相對其他語言多
  5. 強型別語言
  6. 微軟有開源的趨勢
  7. 微軟主打的語言
  8. 因為1、2,所以我回家會想繼續鑽研C#

學習目標

  1. C#與MVC網頁系統、RESTful API
  2. 設計模式淺談
  3. 用C#實作Laravel框架提供的解決方案

而對未來,我想用C#來寫寫看遊戲,如Unity、連線遊戲,因為喜歡像是魔物獵人、undertale那種讓人充滿成就感的感覺。

最後,因為我電腦容量太小,灌不了Visual Studio,所以我買了台二手的筆電T_T,希望能協助我度過這個旅程。
(ps.寫php跟java都沒1G…Visual Studio竟然要10G以上)

好了,等待電腦到來前或許能先聊聊設計模式。