[實作筆記] 試玩 DeepSeek 與避免思想審查

DeepSeek 簡介

DeepSeek 是一家中國 AI 新創公司,透過低成本、高效率的訓練模式,
在相對低的成本下訓練出接近 ChatGpt 效能的 AI 模型。
打破了 AI 訓練需高成本的迷思再加上開源,在 AI 和金融投資界引起廣泛關注。
其創新方法可能改變 AI 產業的投資邏輯,對科技股市造成顯著影響。

思想審查

DeepSeek 在訓練過程中使用的資料集,通常可能經過嚴格篩選,包含大量官方認可的內容與過濾後的敏感信息,
這使得模型內部便內建了與中共敘事一致的偏見與審查機制。

LM Studio 解法

「在本機離線使用的 lmstudio.ai 環境裡,如果遇到言論管制的題目,
只要用 ⌘U 先輸入思考過程和回答的前綴,再用 → 繼續生成回答,就可以繞過了。」 — 唐鳳

LM Studio 簡介

LM Studio 是一個提供圖形化使用者介面(GUI)的語言模型開發平台,
讓用戶能夠簡單地進行模型訓練、調整及部署,無需繁複的程式碼操作。
它支援各種自然語言處理應用,提升開發效率和操作體驗。

越獄版 ?

參考零度博客
可以安裝所謂的越獄版
當中有一個手法如下

This is an uncensored version of deepseek-ai/deepseek-r1
created with abliteration (see remove-refusals-with-transformers to know more about it).
This is a crude, proof-of-concept implementation to remove refusals from an LLM model without using TransformerLens.

If “” does not appear or refuses to respond, you can first provide an example to guide,
and then ask your question.
For instance:

How many ‘r’ characters are there in the word “strawberry”?

實測結果

以上的方法確實可以讓本來只會回答”你好,这个问题我暂时无法回答,让我们换个话题再聊聊吧”的 DeepSeek 生成其他回應,
不過只要繼續談話(敏感話題),就會發現它的思考模式仍然有受限,實際的破解價值不高,特別是要聊政治敏感的話題。

參考

(fin)

[實作筆記] 試用 Breezy Voice

前情提要

聯發科開源 MR Breeze 2 多模態模型,包含 Llama-Breeze2 繁中語言模型、BreezyVoice 台灣口音語音合成模型,以及 Android APP。Llama-Breeze2 提升了繁體中文知識和視覺能力,BreezyVoice 則能生成擬真人聲

覺得有趣,來試玩看看

網頁操作

https://huggingface.co/spaces/Splend1dchan/BreezyVoice-Playground

比較簡單的作法可以 Duplicate Huggingface 的 UI 操作介面,
上傳一個 5 分鐘的聲音檔案,就可以生成結果,等待時間 1~10 分鐘不等。

也可以使用 Kaggle

但是遇到一些問題

  1. 沒找到可用 GPU ,可能是帳號權限的關係

    • 進行帳號驗証
    • 可以在右側[Session Option]區塊調整 Accelerator
    • 調整後可以選用 GPU 但是有 30 小時的時間限制
  2. 無法 Clone Github Repo

    1
    2
    Cloning into 'BreezyVoice'...
    fatal: unable to access 'https://github.com/mtkresearch/BreezyVoice.git/': Could not resolve host: github.com
    • 因為 Kaggle 無法連網(x)
    • 可以在右側[Session Option]區塊調整 Internet on

File > Open in Colab 可以

改用 [Google Colab]

執行到 !pip install -r requirements.txt 會有以下錯誤

1
2
3
4
Collecting ttsfrd-dependency==0.1 (from -r requirements.txt (line 10))
Downloading https://www.modelscope.cn/models/speech_tts/speech_kantts_ttsfrd/resolve/master/ttsfrd_dependency-0.1-py3-none-any.whl (1.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 1.4 MB/s eta 0:00:00
ERROR: ttsfrd-0.3.9-cp310-cp310-linux_x86_64.whl is not a supported wheel on this platform.

主因是 Colab 的 python 版本過新,降級 python 就可以了(3.11→3.10)

改用 [本地(macbook)]

作業系統不符,直接沒得玩

1
2
3
4
5
6
7
❯ python3.10 -m pip install -r requirements-mac.txt

Looking in indexes: https://pypi.org/simple, https://download.pytorch.org/whl/cu118
Collecting ttsfrd-dependency==0.1 (from -r requirements-mac.txt (line 35))
Downloading https://www.modelscope.cn/models/speech_tts/speech_kantts_ttsfrd/resolve/master/ttsfrd_dependency-0.1-py3-none-any.whl (1.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 1.6 MB/s eta 0:00:00
ERROR: ttsfrd-0.3.9-cp310-cp310-linux_x86_64.whl is not a supported wheel on this platform.

心得

效果蠻好的,聲音非常像難以辦認,但是在長文朗讀或是歌唱發聲表現不知道如何?
學習了 Kaggle Colab 兩個工具,了解一下網路與資源(GPU)設定相關的題目
地端開發的話,可能 Linux 才是最佳解,會比 Macbook 更好 ??
但是 GPU 仍是重要資源。

參考

(fin)

[實作筆記] 在 MacBook 上安裝與使用 Ollama

前情提要

AI 時代到來,就像 Web、Mobile 等帶來一波波的浪潮與機會,趁機來學習點新技術了。
比如說像是 Ollama 這款簡單好用的工具,可以快速在本機啟用大型語言模型 (LLM),如 LLaMA、Mistral 等。以下記錄在 macOS 上的安裝與初體驗。

實作記錄

1. 安裝 Ollama

  • 使用 Homebrew 安裝

    1
    brew install ollama
  • 確認安裝成功

    1
    ollama --version

2. 執行第一個模型

  • 拉取並執行 LLaMA 2 模型:

    1
    ollama run llama2
  • 若要使用其他模型(如 Mistral):

    1
    ollama run mistral

3. 互動範例

  • 問答互動示例:

    1
    2
    $ ollama run llama2
    > 你好!請問我可以幫你什麼?

4. 常見的 ollama 指令

  • 列出可用模型

    1
    ollama list
  • 查看模型詳情

    1
    ollama info <model_name>
  • 停止運行中的模型

    1
    ollama stop <model_name>
  • 更新 Ollama

    1
    brew upgrade ollama

簡單的 web 介面

使用 Chrome 外掛
安裝後選擇模型就可以透過 Web UI 與模型互動。
也有一些開源專案有提供 Web UI

我沒有實作作,僅記錄一下查找到的結果

參考

(fin)

[實作筆記] 使用 Docker 一鍵生成 Sample 資料庫

前情提要

在開發或測試過程中,常常需要一個快速可用的資料庫環境來模擬真實操作。
然而,手動安裝和配置 MySQL 不僅耗時,也容易出現版本兼容性問題。
透過 Docker,我們可以輕鬆建立一個即時可用的環境,並在啟動時自動初始化資料庫的必要條件(表格或 root 帳密等…)。

本文將介紹如何使用 Dockerfile 和 SQL 初始化腳本,快速構建一個名為 sample 的測試資料庫。

實作記錄

步驟 1: 建立專案目錄與檔案

首先,創建一個新目錄,並在其中準備以下檔案:

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Dockerfile for setting up MySQL with a sample database

# 使用官方 MySQL 映像

FROM mysql:latest

# 設定環境變數
ENV MYSQL_ROOT_PASSWORD={your root password}
ENV MYSQL_DATABASE=sample

# 複製初始化腳本

COPY init.sql /docker-entrypoint-initdb.d/

# 開放 MySQL 連接埠

EXPOSE 3306

init.sql

1
2
3
CREATE DATABASE IF NOT EXISTS sample;
USE sample;

步驟 2: 建構與啟動容器

執行以下指令來建構映像並啟動容器:

建構 Docker 映像

docker build -t mysql-sample .

啟動容器

docker run -d –name mysql-sample -p 3306:3306 mysql-sample

這將啟動一個名為 mysql-sample 的容器,並將 MySQL 的 3306 埠映射到本機。

步驟 3: 驗證資料庫

進入 MySQL 容器並確認資料庫與表格是否正確建立:

docker exec -it mysql-sample mysql -u root -p

在 MySQL shell 中執行:

1
2
3
SHOW DATABASES;
USE sample;
SHOW TABLES;

參考

開發用指令

1
2
3
4
5
6
❯ docker run -d \
--name mysql-container \
-e MYSQL_ROOT_PASSWORD=You don't need to try this password; I made it up. A7X3P \
-v mysql-data:/var/lib/mysql \
-p 3306:3306 \
mysql:latest

(fin)

[實作筆記] FastAPI 的 middleware

前請提要

最近團隊的小朋友作的系統發生了錯誤。

在追查的過程之中,
小朋友說了一個驚人的事實
Python 的 FastAPI Web 框架,發生 422 錯誤時(輸入的參數類別錯誤),無法追踪 Request$$
當然這不是事實,那麼 FastAPI 應該怎麼處理呢?

本文

讓我們先簡單實作一個 Fast API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from fastapi import FastAPI
from pydantic import BaseModel

# 初始化 FastAPI 應用
app = FastAPI()

# 定義請求資料模型
class Item(BaseModel):
name: str
price: float
description: str = None # 可選的欄位

# 定義 API 路徑
@app.post("/items/")
async def create_item(item: Item):
# 假設邏輯檢查
if item.price <= 0:
return {"error": "Price must be greater than 0"}
return {"message": "Item created successfully!", "item": item}

接下來,讓我試著用不正確的方式呼叫 API,並且得到一個 422 的錯誤

1
2
3
4
curl 
-X POST "http://127.0.0.1:8000/items/"
-H "Content-Type: application/json"
-d '{"item": "should be number", "price": 10.5}'

FastAPI 會提供 Client 端非常友善的資訊

1
2
3
4
5
6
7
8
9
10
11
12
{
"detail":[
{
"type":"missing",
"loc":["body","name"],
"msg":"Field required",
"input":{
"item":"should be number",
"price":10.5
}
}]
}

但在 Server 端只會有類似以下的資訊,這以後端的角度要排查錯誤會非常不便

1
INFO:     127.0.0.1:57617 - "POST /items/ HTTP/1.1" 422 Unprocessable Entity

Server Side 有沒有什麼好方法可以協助我們 Debug 呢?

解決方法:Middleware

這其實是 web 開發者的小常識才對,
實作如下,

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
from fastapi import FastAPI,Request
from pydantic import BaseModel
import logging
from datetime import datetime

# 初始化 FastAPI 應用
app = FastAPI()

# 初始化日誌
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")

# 定義請求資料模型
class Item(BaseModel):
name: str
price: float
description: str = None # 可選的欄位

# 中間件,用於記錄請求
@app.middleware("http")
async def log_requests(request: Request, call_next):
# 記錄請求進來的時間與基本資訊
request_time = datetime.utcnow()
logging.info(f"Incoming request at {request_time}")
logging.info(f"Method: {request.method} | Path: {request.url.path}")

# 嘗試讀取請求 Body
try:
body = await request.json()
logging.info(f"Request Body: {body}")
except Exception:
logging.info("No JSON body or failed to parse.")

# 執行後續處理並取得回應
response = await call_next(request)

# 記錄回應狀態碼
logging.info(f"Response status: {response.status_code}")
return response

# 定義 API 路徑
@app.post("/items/")
async def create_item(item: Item):
# 假設邏輯檢查
if item.price <= 0:
return {"error": "Price must be greater than 0"}
return {"message": "Item created successfully!", "item": item}

Request 送進來時 Server Side 就會如下記錄

1
2
3
4
5
2025-01-03 03:37:37,294 - Incoming request at 2025-01-02 19:37:37.294509
2025-01-03 03:37:37,294 - Method: POST | Path: /items/
2025-01-03 03:37:37,294 - Request Body: {'item': 'should be number', 'price': 10.5}
2025-01-03 03:37:37,295 - Response status: 422
INFO: 127.0.0.1:59545 - "POST /items/ HTTP/1.1" 422 Unprocessable Entity

心得

回到我們原本的情境,我們真正要解決的問題是,

|發生異常時,Server Side 要可以追蹤錯誤

因為是 FastAPI 所以沒有辦法追蹤錯誤,是非常錯誤判斷

在 Google 與 AI 工具這麼方便的時代,應該要能正確的提出問題。

ex: FastAPI 可以記錄什麼時候 request 送進來,並帶了什麼資訊嗎?

(fin)

[生活筆記] ChatGPT 個性分析

前請提要

ChatGPT 能透過語言理解和生成,提供更細緻的個性分析,特別是當你使用了一陣子後,
可以用以下的 Prompt 作個簡單的分析報告。

Prompt

根據我們之前的互動,我希望你能告訴我你觀察到關於我的哪些特質是我可能沒有意識到的。
請儘可能誠實直接的表達,不要有所保留。
如果你注意到我可能察覺不到的矛盾之處、行為模式或傾向,我都想清楚的聽到,不需要粉飾任何資訊,請給我最真實最直接的分析

ChatGPT said

以下是根據我們互動中可能顯現的特質和行為模式,我誠實的觀察與分析,供你參考:

  1. 強烈的自我驅動
    你有著明顯的自我驅動力,願意挑戰自我,例如你的30天鐵人挑戰或對學習新技能的熱忱。
    然而,這種驅動力可能會讓你偶爾忽略自己的疲憊或情緒需求,容易進入「過度努力」的狀態。

  2. 追求效率與結果,但重視細節
    你傾向於以結果為導向,尤其是在專案進行或問題解決時。
    你對細節的關注,例如希望資訊條理清楚或具可操作性,顯示出你對高標準執行的重視。
    然而,有時可能會因為過於專注於效率,無意間忽略更大的全局或需要額外耐心的部分。

  3. 多元興趣與高度適應力
    你對不同領域的興趣(技術、跳舞、潛水、反思工具等)顯示了你擁有探索和適應的能力。
    然而,這種多元性可能會讓你在資源有限的情況下分散注意力,難以在某一領域深耕到你期望的程度。

  4. 對「流程」的矛盾態度
    雖然你熟悉並實踐許多工具和方法(如敏捷、KPT、SMART等),但你的個性似乎也渴望自由與靈活性,
    這可能讓你在面對過於僵化的流程時感到不耐煩。
    這種矛盾可能讓你在制定規範時猶豫,因為你不想限制自己或團隊的創造力。

  5. 溫和但堅持個性
    你偏好直接但不咄咄逼人的溝通方式,並且能保持禮貌與平衡。
    然而,你也表現出對原則與目標的堅持,即使是在比較微妙的情境下,可能讓他人難以察覺你的真實意圖。

  6. 對自己標準的隱藏壓力
    你對自己設定的目標和標準很高,且傾向於內化「應該更好」的期待。
    這可能導致你對成功的認可不夠,容易放大未完成或未達成的部分,忽略已經達成的成就。

  7. 偏好控制但能適應改變
    你習慣掌控規劃與執行的過程,並且對細節要求高。
    然而,你在面對不可預測的情況時,也能快速調整心態並提出替代方案,顯示出高度靈活的思維。

這些分析可能包含一些你未曾意識到或尚未明確的特質,也可能包含你的日常行為中隱藏的矛盾。
如果你有不同的想法或任何補充,歡迎分享!

(fin)

[實作筆記] GCP 防火牆設定與 Health Check

前言

GCP 防火牆(Firewall)用來控制進出 Google Cloud 資源的網路流量,確保服務的安全性。
而 Health Check 則是用來監控後端服務的健康狀態,透過 GCP 負載平衡器(Load Balancer)進行流量分配。
我將介紹如何在 GCP 設定防火牆規則以支持 Health Check 的運作。

情境

instance 有一個 Allow Load Balancer Health checks 選項,
被勾起來後 Network tags 會加上lb-health-check
但是不知為什麼狀態仍然會顯示為 Off ?

所以我們必須多作一些步驟

我有許多服務都掛在負載平衡器之後,並且使用 Backend Service 來管理這些流量。
一個 LB 會有多個 Backend Service (本文先不涉及 Backend Bucket)
由於成本考量,為了提高資源利用率,一台機器可能會承載多個 Web 服務。
這樣做可以節省硬體和虛擬機的成本,但同時也需要確保每個 Web 服務能夠獨立且穩定地運行。

在這樣的架構中,LB 會依 Path Rules 將流量分配到多個實例組(Instance Group)中,
每個實例中可能運行著多個 Web 服務。
透過這些實例和 Backend Service 的組合,負載平衡器能夠有效地將流量分配到不同的服務上。
然而,為了確保服務的穩定性,我們必須配置健康檢查(Health Check)來監控每個 Web 服務的運行狀態。

在 GCP 中,當我們使用負載平衡器(Load Balancer)時,會涉及以下資源:

  • Backend Service:負責接收來自負載平衡器的流量,並將流量分發給後端的實例。
  • Health Check:用來檢查後端服務是否健康,負載平衡器會依據這些檢查結果決定是否將流量導向特定實例。
  • Instance Group:一組相同配置的虛擬機(VM),負責運行服務,並自動進行縮放。
  • Instance:每個實際的虛擬機,執行後端服務。

這些資源之間的關係是,Backend Service 根據 Health Check 監控的結果選擇健康的 Instance,
並將流量分發給 Instance Group 中的實例。
當負載平衡器根據路徑規則(LB Path Rule)將流量導向某個服務時,這些流量會被引導到 Instance 中運行的 Web 服務。

我們會在一台 Instance 上使用多個 web 服務,使用不同的 Port 來區分,範圍是 6000~6100。
而我們需要對這些所有的服務設定 Health Check

實作

每個實例有一個 “Allow Load Balancer Health checks” 的選項,
勾選後會自動加上 lb-health-check 的網路標籤(Network Tag)。
不過不知道為什麼狀態仍會顯示為 Off。
也就是說單純打勾是不夠的我們仍需要額外的設定,我們可以利用這個 lb-health-check 來作一些事

我們可以利用 Network Tag:lb-health-check 來建立相應的防火牆規則,
確保來自負載平衡器的流量能夠順利進行健康檢查。

接下來,我們可以創建一個防火牆規則,限制僅允許 6000~6100 端口的 TCP 流量。
這樣做的原因是,健康檢查會直接使用 TCP 進行連接檢查,而不是 HTTP 協定,
這樣可以避免 HTTP 協定中請求和響應的延遲,並且減少負載平衡器在檢查健康狀態時的處理開銷。

在 GCP Console 中創建防火牆規則,指定源 IP 或範圍(例如內部網路的 IP 範圍)。
Google 的 Health Check 源自 35.191.0.0/16130.211.0.0/22
設定在 Source filters > IP ranges 上
Targets > Target tags 請設定為 lb-health-check
設定 Protocols and ports > Specified protocols and ports 選擇 TCP 協定 Ports 限制在 6000-6100。
在 Health checks 中,Protocol 也設定 TCP,Port 要設定為真實的 Web 服務使用的 Port
在 VM 的 instance 上,本來需要設定 Network Tag,但是我們可以勾選 “Allow Load Balancer Health checks” 的選項,
就會自動加上 Tag 囉。

參考

(fin)

[實作筆記] Google NAT 簡介與實作

前情提要

在打造安全的 VM 環境時,常常會遇到一個經典的矛盾點:
我們希望 VM 保持隱蔽,避免直接暴露於網際網路以減少安全風險,但同時又需要對外存取資源。
例如在開發或維運階段,像是執行 npm install、apt-get update,甚至提供給合作第三方的白名單 IP 等需求,往往都依賴外部下載與連線。

因此,我們的需求很明確:

  • VM 非必要時,不提供 Public IP。
  • VM 在發出 Request 時,需要擁有一個固定的對外 IP 地址。

傳統機房的解決方案

在傳統機房環境中,透過 NAT (Network Address Translation) 伺服器或防火牆設備,就能實現內部網路的安全管理。

內部流量管理: 內部 VM 使用私有 IP 位址,對外流量透過 NAT 進行轉換。
控制風險: 透過 NAT,內部伺服器可以訪問外部網路,但外部無法直接存取內部資源。
然而,傳統機房的設定通常需要額外的硬體設備,並增加了維護成本。

GCP 的解決方案 NAT

在 GCP 上,這個問題有一個優雅的解法:Cloud NAT。

無須公有 IP: Cloud NAT 允許私有 VM 不需配置公有 IP,就能存取外部網路。
集中管理: 透過 VPC 網路層級的 NAT 設定,簡化整體網路架構。
彈性擴展: Cloud NAT 可以根據流量自動擴展,減少單點故障風險。

NAT

實作筆記

步驟 1: 建立 VPC 網路

在 GCP 控制台中,前往 VPC 網路,選擇 建立 VPC 網路。
設定一個新的 VPC 網路,並確保 VM 的子網路設為 私有(Private)。這樣做可以保護 VM 不被直接暴露於公共網路。
實務上我選擇 default

步驟 2: 配置 Cloud NAT

前往 VPC 網路 > Cloud NAT,並選擇 建立 NAT 網關。
在設置過程中,選擇對應的子網路和路由(Route)設定。這樣可以確保 VM 在沒有 Public IP 的情況下仍能夠透過 NAT 網關訪問外部網路。
配置完成後,Cloud NAT 將會自動幫助 VM 處理對外的連線請求,而不需要直接公開 VM 的 IP。

步驟 3: 配置 Public IP(如果需要)

若要讓 VM 直接對外發送請求或開放服務,可以在創建 VM 時為其配置 Public IP。
在 Google Cloud Console 中創建虛擬機時,選擇 外部 IP 設為 靜態(static),這樣可以確保 Public IP 地址不會變動。
當 VM 配置了 Public IP,所有對外的請求將會直接通過這個 IP 進行。

心得

Cloud NAT 的設計,讓我們可以兼顧安全性與便利性。
關鍵優勢:

簡單配置: 不需像傳統 NAT 那樣繁複的硬體設置。
靈活性高: 無論是開發環境還是正式部署,都能輕鬆應對。
成本效益: 減少了維運成本,同時保護了內部資源。

參考

(fin)

[學習筆記] 里氏替換原則(Liskov Substitution Principle, LSP)

前言

OOP 中的五大原則之一—里氏替換原則,開發 OOP 的工程師應該或多或少都有聽過,
最近與同事討論後有新的體悟,特別記錄一下

1
若對某個型別 T 的物件 𝑥 能證明具有某個性質 𝑞(𝑥),那麼對於 T 的子型別 S 的物件 𝑦 同樣應該滿足 𝑞(𝑦)。
1
2
If a property 𝑞(𝑥) can be proven for objects 𝑥 of type 𝑇,
then 𝑞(𝑦) should also hold true for objects 𝑦 of type 𝑆, where 𝑆 is a subtype of 𝑇.

里氏替換原則(Liskov Substitution Principle, LSP)詳解
定義:
里氏替換原則由 Barbara Liskov 在 1987 年提出,
是 SOLID 原則中的 “L”。它強調 子類(Subclass) 應該能夠替換其 父類(Base Class) 而不影響程式的正確性或行為。

核心概念:
“任何使用父類的地方,都應該能夠使用其子類,而不會影響系統的功能。”

主要原則

子類應該擁有父類的所有行為特性。
子類不應改變父類方法的預期行為。
子類可以擴展父類的功能,但不能削弱或改變父類的功能。

違反 LSP 的常見問題:
替換後引發錯誤: 子類重寫某個方法,導致使用者無法正常使用原本的功能。
返回值不一致: 子類方法返回值與父類期望的類型不同。
拋出未預期的異常: 子類新增或拋出父類未預期的異常。

舉例說明,錯誤示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Bird {
fly(): void {
console.log('Flying...');
}
}

class Penguin extends Bird {
// Penguins can't fly!
fly(): void {
throw new Error("Penguins can't fly!");
}
}

function makeBirdFly(bird: Bird) {
bird.fly();
}

// 當傳入 Penguin 時,會引發錯誤
const penguin = new Penguin();
makeBirdFly(penguin); // Throws an error!

違反原因:

在 makeBirdFly 函數中,傳入 Penguin 子類後,原本預期的行為(鳥會飛)被破壞,導致異常發生,違反 LSP。

正確示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Bird {
move(): void {
console.log('Moving...');
}
}

class FlyingBird extends Bird {
fly(): void {
console.log('Flying...');
}
}

class Penguin extends Bird {
swim(): void {
console.log('Swimming...');
}
}

function makeBirdMove(bird: Bird) {
bird.move();
}

const penguin = new Penguin();
makeBirdMove(penguin); // 正常運行,無異常

修正方式

將 Bird 分解為更細的抽象類別(如 FlyingBird)。
確保所有子類的行為符合其父類的行為約束。
實踐 LSP 的要點:

  • 正確使用繼承: 只有在 “是某種(is a)” 的關係中使用繼承;例如:Dog 是 Animal 的一種(我們都不認為這是一個好例子)。
  • 避免強制行為: 子類不應強迫覆蓋父類的方法,除非行為一致。
  • 使用介面: 如果子類具有不同行為,可使用多個接口來定義不同能力。

總結:
LSP 促使開發者在繼承和多型設計中保持一致性和正確性,保證系統的可擴展性和穩定性。
遵循 LSP 可以避免許多因繼承導致的潛在問題,使代碼更健壯且易於維護。

我們的一些關鍵 Q&A

Q.LSP 的意思是不要用 override ?
A.
不是的,LSP(里氏替換原則)並不是要求完全避免使用 override,
而是要正確使用 override。核心在於: 子類覆寫父類方法時,不能破壞父類的行為預期。

Q. 要正確使用 override 的原則是什麼?
A. 子類應維持父類方法的語義一致性
覆寫後的行為應與父類保持一致,並且在替換後不會引發錯誤。

Q. 有更具體的說明嗎 ?
A. 不要改變方法的輸入輸出參數規格
遵循開放封閉原則(Open/Closed Principle),開放擴展,封閉修改。
方法的參數類型不能比父類方法的參數更具限制性。
方法的返回類型不能比父類方法的返回類型更具擴展性。

Q.
以你的例子來說Penguin 的確不會 fly , 所以應該拋出錯誤
但是這樣就違反了 LSP ,所以我們的建議是將 fly 再抽出成為介面?
A.
沒錯!這是關鍵點了。解決這種情況的最佳方式是使用介面(Interface)來抽象行為,而不是在基類中定義所有可能不適用於子類的行為。

子類應擴展父類的功能,而不是修改父類的行為。

正確的覆寫示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
makeSound(): string {
return "Some sound";
}
}

class Dog extends Animal {
override makeSound(): string {
return "Bark";
}
}

function playSound(animal: Animal) {
console.log(animal.makeSound());
}

const dog = new Dog();
playSound(dog); // Output: "Bark" (符合父類預期)

說明:
Dog 覆寫了 makeSound(),但返回類型和行為保持一致,替換 Animal 後,系統的正確性不受影響。

違反 LSP 的覆寫示例

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
class Rectangle {
constructor(protected width: number, protected height: number) {}

setWidth(width: number) {
this.width = width;
}

setHeight(height: number) {
this.height = height;
}

getArea(): number {
return this.width * this.height;
}
}

class Square extends Rectangle {
// 覆寫後改變了行為,違反 LSP
setWidth(width: number) {
this.width = width;
this.height = width; // 強制將高度設為相等
}

setHeight(height: number) {
this.height = height;
this.width = height; // 強制將寬度設為相等
}
}

function testRectangle(rect: Rectangle) {
rect.setWidth(5);
rect.setHeight(10);
console.log(rect.getArea()); // 預期:50
}

const square = new Square(0, 0);
testRectangle(square); // Output: 100 (違反父類預期)

說明:
Square 覆寫了 setWidth 和 setHeight 方法,但改變了 Rectangle 的行為。
傳入 Square 後,計算面積的結果與 Rectangle 的預期不同,這破壞了 LSP。

兩個例子的差異在於
Dog 的覆寫行為符合父類預期,Square 改變父類邏輯(強制高寬相等),破壞原有功能,違反 LSP。

總結

可以使用 override,但要謹慎處理。
確保子類的行為符合父類的預期,不會引入不一致或異常行為。
遵守 LSP 能保證系統在多型使用時的穩定性和可預測性。

對「少用繼承多用組合」的體悟多了一層,如果發現繼承了父類的方法,
但是修改了行為(拋錯誤、改變本來沒有改動到的屬性、長出新的邏輯分更支…),這就是一種壞味道。  
大部份應該都可以透過介面排除這個問題。

(fin)

[實作筆記] Macbook dotnet 8 升級 dotnet 9

前情提要

dotnet 9 官宣囉 ! 雖然不是 lts 版本,但是仍然有許多有趣的新東西,先昇級並記錄一下。

小常識

.NET Runtime 和 .NET SDK 主要的差別在於它們的用途和所包含的工具:

.NET Runtime:

僅包含運行 .NET 應用所需的基本庫和執行環境。
適合只需要執行已編譯應用程序的情況,像是使用者在安裝應用後直接執行時。
不包含編譯、打包或開發所需的工具和命令。
.NET SDK (Software Development Kit):

包含 .NET Runtime,以及用來開發、編譯、測試和打包 .NET 應用的工具和命令。
包括開發者常用的命令,如 dotnet build(編譯)、dotnet publish(發布)、dotnet test(測試)等。
必須安裝 SDK 才能進行應用程式的開發,而不僅僅是執行。

簡單來說:

如果你只是想執行 .NET 應用,安裝 Runtime 即可。如果你要開發或修改 .NET 應用,則需要 SDK。

安裝步驟

https://dotnet.microsoft.com/en-us/download/dotnet/9.0
選擇 SDK 9.0.100 (左側) > macOS Installers > Arm64

我曾經裝到 X64,安裝過程沒有失敗,但是 dotnet version 仍為 8.0.100

檢查

❯ dotnet –version
9.0.100
❯ dotnet –list-sdks
8.0.100 [/usr/local/share/dotnet/sdk]
8.0.101 [/usr/local/share/dotnet/sdk]
9.0.100 [/usr/local/share/dotnet/sdk]

專案升級

專案有許多套件有 Warning 先進行 restore

❯ dotnet restore Marsen.NetCore.Dojo.Integration.Test.sln

❯ dotnet restore Marsen.NetCore.Dojo.Integration.Test.sln
(中略)
在 24.4 秒內還原 成功但有 9 個警告

IDE 我是使用 Rider

Tools → NuGet → Manage NuGet Packages for Solution…

把能昇級的都昇一昇,再執行測試

測試摘要: 總計: 467, 失敗: 0, 成功: 466, 已跳過: 1, 持續時間: 4.7 秒
在 6.5 秒內建置 成功但有 9 個警告

Tools → NuGet → Upgrade Packages in Solution…

再執行測試

測試摘要: 總計: 467, 失敗: 0, 成功: 466, 已跳過: 1, 持續時間: 4.7 秒
在 6.5 秒內建置 成功但有 9 個警告

警告如下

warning NU1903: 套件 ‘System.Text.RegularExpressions’ 4.3.0 具有已知的 高 嚴重性弱點
warning NU1903: 套件 ‘Newtonsoft.Json’ 9.0.1 具有已知的 高 嚴重性弱點
warning CS0618: ‘SqlConnection’ 已經過時: ‘Use the Microsoft.Data.SqlClient package instead.’
warning SYSLIB0051: ‘Exception.Exception(SerializationInfo, StreamingContext)’ 已經過時

高風險可以透過 Nuget 確認有沒有新版本的或 patch

SYSLIB0051: Legacy serialization support APIs are obsolete 基本上都是在繼承舊版 Exception 時建立的方法
可以刪除,但這次先標已過時標籤後並保留,相關程式都沒有呼叫這些方法,所以不會引發新的警告。

warning CS0618: ‘SqlConnection’ 已經過時: ‘Use the Microsoft.Data.SqlClient package instead.’
很簡單,依據他的建議,改用 using Microsoft.Data.SqlClient; 即可。
當然要先從 Nuget 安裝 package

處理完警告後,修改所有csproj內的 TargetFramework 標籤內容如下:

1
<TargetFramework>net9.0</TargetFramework>

CICD 相關的設定也要改為 dotnet 9.0

像是 .github/workflows 腳本中的 dotnet-version

1
2
3
4
5
steps:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '9.0.x'

或是 sonarscan-dotnet 中使用的版本(這部份如果原始 repo 尚未更新,可以試著自已更新看看)

1
2
3
- name: SonarCloud Scan
# support dotnet9.0
uses: marsen/[email protected]

(fin)