用最簡單的語言,解釋設計模式。
雖然示例程式碼是用 PHP7 實現的,但因為概念是一樣的,所以語言並不會阻礙大家理解設計模式。
概述
行為型設計模式關心物件之間的責任分配。與結構型設計模式不同的是,行為型設計模式不僅僅指定結構,而且還概述了它們之間的訊息傳遞/通訊的模式。或者換句話說,行為型模式幫助回答了“軟體元件是如何執行的?”
維基百科
在軟體工程中,行為型設計模式為設計模式的一種型別,用來識別物件之間的常用交流模式並加以實現。如此,可以在交流時增強靈活性。
分類
- 責任鏈模式
- 命令模式
- 迭代器模式
- 中介者模式
- 備忘錄模式
- 觀察者模式
- 訪問者模式
- 策略模式
- 狀態模式
- 模板方法模式
? 責任鏈模式
現實生活示例
例如,你的帳戶中有三種付款方式(A,B 和 C); 每種方式付款額不同。 A 可支付 100 美元,B 可支付 300 美元,C 可支付 1000 美元,支付的優先順序為 A->B->C。現在想要購買價值 210 美元的東西。使用責任鏈模式,首先將檢查帳戶 A 是否可以進行購買,如果可以購買,鏈條將被破壞。如果不能購買,將繼續檢查賬號 B 是否可以購買,如果可以購買,鏈條將被破壞,否則請求將繼續轉發,直到找到合適的處理程式。這裡的 A、B 和 C 就是責任鏈的鏈條,整個現象就是責任鏈模式。
概述
責任鏈模式有助於建立一個物件鏈。請求從一端進入,在物件之間轉發,直到找到合適的處理程式。
維基百科
責任鏈模式是物件導向程式設計的一種軟體設計模式,它包含了一些命令物件和一系列的處理物件。每一個處理物件決定它能處理哪些命令物件,不能處理的命令物件傳遞給該鏈中的下一個處理物件。
程式示例
以上面的支付賬號為例,首先給出賬戶基類,包含連結賬號的邏輯以及一些不同型別的賬戶
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 |
abstract class Account { protected $successor; protected $balance; public function setNext(Account $account) { $this->successor = $account; } public function pay(float $amountToPay) { if ($this->canPay($amountToPay)) { echo sprintf('Paid %s using %s' . PHP_EOL, $amountToPay, get_called_class()); } elseif ($this->successor) { echo sprintf('Cannot pay using %s. Proceeding ..' . PHP_EOL, get_called_class()); $this->successor->pay($amountToPay); } else { throw new Exception('None of the accounts have enough balance'); } } public function canPay($amount): bool { return $this->balance >= $amount; } } class Bank extends Account { protected $balance; public function __construct(float $balance) { $this->balance = $balance; } } class Paypal extends Account { protected $balance; public function __construct(float $balance) { $this->balance = $balance; } } class Bitcoin extends Account { protected $balance; public function __construct(float $balance) { $this->balance = $balance; } } |
然後通過上面定義的連結(即 Bank, Paypal, Bitcoin)形成責任鏈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Let's prepare a chain like below // $bank->$paypal->$bitcoin // // First priority bank // If bank can't pay then paypal // If paypal can't pay then bit coin $bank = new Bank(100); // Bank with balance 100 $paypal = new Paypal(200); // Paypal with balance 200 $bitcoin = new Bitcoin(300); // Bitcoin with balance 300 $bank->setNext($paypal); $paypal->setNext($bitcoin); // Let's try to pay using the first priority i.e. bank $bank->pay(259); // Output will be // ============== // Cannot pay using bank. Proceeding .. // Cannot pay using paypal. Proceeding ..: // Paid 259 using Bitcoin! |
? 命令模式
現實生活示例
一個典型的例子是你在餐廳點菜,你(即客戶)向服務員(即 Invoker)點餐(即命令),服務員只需將需求轉達給會烹飪的廚師。 另外一個例子是你(即客戶端)使用遙控器(Invoker)開啟(即命令)電視機(即接收器)。
概述
命令模式允許將操作封裝在物件中,其背後的關鍵思想是提供客戶端與接收器分離的方法。
維基百科
在物件導向程式設計的範疇中,命令模式是一種行為型設計模式。將所有需要的資訊封裝到物件中,用於之後的動作(action)或者事件觸發。被封裝的資訊包括方法名以及擁有方法及引數的物件。
程式示例
首先給出一個接收器,實現了可能會執行的動作:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Receiver class Bulb { public function turnOn() { echo "Bulb has been lit"; } public function turnOff() { echo "Darkness!"; } } |
然後給出一個介面,(Bulb)中的每個命令都要實現這個介面,得到一組命令集:
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 |
interface Command { public function execute(); public function undo(); public function redo(); } // Command class TurnOn implements Command { protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOn(); } public function undo() { $this->bulb->turnOff(); } public function redo() { $this->execute(); } } class TurnOff implements Command { protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOff(); } public function undo() { $this->bulb->turnOn(); } public function redo() { $this->execute(); } } |
然後是Invoker,客戶端將與之互動以處理各種命令:
1 2 3 4 5 6 7 8 |
// Invoker class RemoteControl { public function submit(Command $command) { $command->execute(); } } |
最後來看一下如何在客戶端中使用:
1 2 3 4 5 6 7 8 |
$bulb = new Bulb(); $turnOn = new TurnOn($bulb); $turnOff = new TurnOff($bulb); $remote = new RemoteControl(); $remote->submit($turnOn); // Bulb has been lit! $remote->submit($turnOff); // Darkness! |
命令模式也可用於實現基於事務的系統。在執行命令時,需要持續儲存命令的歷史,如果最後一條命令成功執行,皆大歡喜,否則遍歷歷史記錄,並對所有執行過的命執行撤銷
。
➿ 迭代器模式
現實生活示例
老式的無線電裝置將是一個很好的迭代器示例,使用者可以在某個頻道上啟動,然後使用下一個或上一個按鈕來切換頻道。或者以 MP3 播放器或電視機為例,你可以按下一個按鈕和上一個按鈕進行連續的頻道切換。換句話說,它們都提供了一個介面來遍歷相應的頻道,歌曲或廣播電臺。
概述
迭代器模式提供了一種方法,可以訪問物件的元素而不暴露底層實現。
維基百科
在物件導向程式設計中,迭代器模式是一種設計模式,其中迭代器用於遍歷容器並訪問容器的元素。迭代器模式將演算法與容器解耦; 在某些情況下,演算法是特定容器必需的,因此不能解耦。
程式示例
通過PHP,使用 SPL(PHP標準庫)可以輕鬆實現迭代器模式,以上述收音機為例,首先給出 RadioStation
類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class RadioStation { protected $frequency; public function __construct(float $frequency) { $this->frequency = $frequency; } public function getFrequency(): float { return $this->frequency; } } |
然後是 迭代器
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 |
use Countable; use Iterator; class StationList implements Countable, Iterator { /** [@var](http://www.jobbole.com/members/variable) RadioStation[] $stations */ protected $stations = []; /** [@var](http://www.jobbole.com/members/variable) int $counter */ protected $counter; public function addStation(RadioStation $station) { $this->stations[] = $station; } public function removeStation(RadioStation $toRemove) { $toRemoveFrequency = $toRemove->getFrequency(); $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) { return $station->getFrequency() !== $toRemoveFrequency; }); } public function count(): int { return count($this->stations); } public function current(): RadioStation { return $this->stations[$this->counter]; } public function key() { return $this->counter; } public function next() { $this->counter++; } public function rewind() { $this->counter = 0; } public function valid(): bool { return isset($this->stations[$this->counter]); } } |
可以這樣使用
1 2 3 4 5 6 7 8 9 10 11 12 |
$stationList = new StationList(); $stationList->addStation(new RadioStation(89)); $stationList->addStation(new RadioStation(101)); $stationList->addStation(new RadioStation(102)); $stationList->addStation(new RadioStation(103.2)); foreach($stationList as $station) { echo $station->getFrequency() . PHP_EOL; } $stationList->removeStation(new RadioStation(89)); // Will remove station 89 |
? 中介者模式
現實生活示例
典型的例子是你通過手機與他人通話,你與通話者之間有一個網路供應商,對話將通過供應商傳遞而非直接傳遞。這種情況下網路供應商就是中介者。
概述
中介者模式新增了第三方物件(稱為中介者)來控制兩個物件(稱為 colleague)之間的互動。中介者模式有助於減少通訊類之間的耦合,因為類之間無需知道對方的實現。
維基百科
在軟體工程中,中介者模式包裝了一系列物件相互作用的方式。這種模式被認為是一種行為模式,因為它可以改變程式的執行時行為。
程式示例
下面是一個最簡單的使用者(即 colleague)在聊天室(中介者)中互相傳送訊息的示例
首先給出中介者及聊天室
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
interface ChatRoomMediator { public function showMessage(User $user, string $message); } // Mediator class ChatRoom implements ChatRoomMediator { public function showMessage(User $user, string $message) { $time = date('M d, y H:i'); $sender = $user->getName(); echo $time . '[' . $sender . ']:' . $message; } } |
然後是使用者即 colleague
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class User { protected $name; protected $chatMediator; public function __construct(string $name, ChatRoomMediator $chatMediator) { $this->name = $name; $this->chatMediator = $chatMediator; } public function getName() { return $this->name; } public function send($message) { $this->chatMediator->showMessage($this, $message); } } |
用法
1 2 3 4 5 6 7 8 9 10 11 |
$mediator = new ChatRoom(); $john = new User('John Doe', $mediator); $jane = new User('Jane Doe', $mediator); $john->send('Hi there!'); $jane->send('Hey!'); // Output will be // Feb 14, 10:58 [John]: Hi there! // Feb 14, 10:58 [Jane]: Hey! |
? 備忘錄模式
現實生活示例
以計算器(即發起者)為例,每當執行一些計算時,最後一次計算結果將儲存在記憶體中(即備忘錄),以便資料可以恢復,也可以使用某些操作按鈕(即臨時代理)來恢復資料。
概述
備忘錄模式以一種稍後可平滑恢復的方式捕捉並儲存物件的當前狀態。
維基百科
備忘錄模式是一種軟體設計模式,可以將物件恢復到之前的狀態(通過回滾來撤銷)
需要提供撤銷操作時,備忘錄模式通常很有用。
程式示例
首先給出可以儲存編輯器狀態的備忘錄物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class EditorMemento { protected $content; public function __construct(string $content) { $this->content = $content; } public function getContent() { return $this->content; } } |
然後是使用備忘錄物件的編輯器及發起者
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 Editor { protected $content = ''; public function type(string $words) { $this->content = $this->content . ' ' . $words; } public function getContent() { return $this->content; } public function save() { return new EditorMemento($this->content); } public function restore(EditorMemento $memento) { $this->content = $memento->getContent(); } } |
這樣使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$editor = new Editor(); // Type some stuff $editor->type('This is the first sentence.'); $editor->type('This is second.'); // Save the state to restore to : This is the first sentence. This is second. $saved = $editor->save(); // Type some more $editor->type('And this is third.'); // Output: Content before Saving echo $editor->getContent(); // This is the first sentence. This is second. And this is third. // Restoring to last saved state $editor->restore($saved); $editor->getContent(); // This is the first sentence. This is second. |
? 觀察者模式
現實生活示例
一個很好的例子是,求職者訂閱了一些招聘網站,每當有匹配的工作機會時,求職者就會收到通知。
概述
定義了物件之間的依賴,一旦其中一個物件的狀態發生改變,依賴它的物件都會收到通知。
維基百科
觀察者模式是軟體設計模式的一種。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。通常通過呼叫目標物件所提供的方法來實現。
程式示例
以上述求職訂閱為例,首先給出求職者,有職位釋出時會收到通知
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 |
class JobPost { protected $title; public function __construct(string $title) { $this->title = $title; } public function getTitle() { return $this->title; } } class JobSeeker implements Observer { protected $name; public function __construct(string $name) { $this->name = $name; } public function onJobPosted(JobPost $job) { // Do something with the job posting echo 'Hi ' . $this->name . '! New job posted: '. $job->getTitle(); } } |
然後是求職者訂閱的職位釋出類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class JobPostings implements Observable { protected $observers = []; protected function notify(JobPost $jobPosting) { foreach ($this->observers as $observer) { $observer->onJobPosted($jobPosting); } } public function attach(Observer $observer) { $this->observers[] = $observer; } public function addJob(JobPost $jobPosting) { $this->notify($jobPosting); } } |
這樣使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Create subscribers $johnDoe = new JobSeeker('John Doe'); $janeDoe = new JobSeeker('Jane Doe'); // Create publisher and attach subscribers $jobPostings = new JobPostings(); $jobPostings->attach($johnDoe); $jobPostings->attach($janeDoe); // Add a new job and see if subscribers get notified $jobPostings->addJob(new JobPost('Software Engineer')); // Output // Hi John Doe! New job posted: Software Engineer // Hi Jane Doe! New job posted: Software Engineer |
? 訪問者模式
現實生活示例
假如有人前往杜拜,他們需要有證件(比如簽證)就可進入杜拜。到達後,無需獲得許可或做一些跑腿工作,他們便可以自由前往杜拜的任何地方; 只要是知道的地方,就能遊覽。訪問者模式可以做到這一點,它可以幫助你新增訪問地點,以便在無需跑腿的情況下,可以儘可能多地訪問。
概述
訪問者模式可以在無需修改物件的情況下增加一些額外操作。
維基百科
在物件導向程式設計和軟體工程中,訪問者設計模式是一種從物件結構中分離演算法的方式。這種分離的實際結果是能夠向現有的物件結構新增新的操作,而無需修改這些結構。這是遵循開放/封閉原則的一種方式。
程式設計示例
以模擬動物園為例,動物園裡有幾種不同種類的動物,我們需要讓它們發出叫聲,下面使用訪問者模式實現
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Visitee interface Animal { public function accept(AnimalOperation $operation); } // Visitor interface AnimalOperation { public function visitMonkey(Monkey $monkey); public function visitLion(Lion $lion); public function visitDolphin(Dolphin $dolphin); } |
然後實現各種動物
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 |
class Monkey implements Animal { public function shout() { echo 'Ooh oo aa aa!'; } public function accept(AnimalOperation $operation) { $operation->visitMonkey($this); } } class Lion implements Animal { public function roar() { echo 'Roaaar!'; } public function accept(AnimalOperation $operation) { $operation->visitLion($this); } } class Dolphin implements Animal { public function speak() { echo 'Tuut tuttu tuutt!'; } public function accept(AnimalOperation $operation) { $operation->visitDolphin($this); } } |
接下來實現訪問者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Speak implements AnimalOperation { public function visitMonkey(Monkey $monkey) { $monkey->shout(); } public function visitLion(Lion $lion) { $lion->roar(); } public function visitDolphin(Dolphin $dolphin) { $dolphin->speak(); } } |
可以這樣使用
1 2 3 4 5 6 7 8 9 |
$monkey = new Monkey(); $lion = new Lion(); $dolphin = new Dolphin(); $speak = new Speak(); $monkey->accept($speak); // Ooh oo aa aa! $lion->accept($speak); // Roaaar! $dolphin->accept($speak); // Tuut tutt tuutt! |
當需要為動物新增新動作時,我們本可以通過動物支援繼承來實現,但是需要修改動物類。但現在就不必修改動物類了。例如,假設需要向動物新增跳躍行為,我們可以通過建立一個新的訪問者來簡單地新增此行為,即:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Jump implements AnimalOperation { public function visitMonkey(Monkey $monkey) { echo 'Jumped 20 feet high! on to the tree!'; } public function visitLion(Lion $lion) { echo 'Jumped 7 feet! Back on the ground!'; } public function visitDolphin(Dolphin $dolphin) { echo 'Walked on water a little and disappeared'; } } |
這樣使用
1 2 3 4 5 6 7 8 9 10 |
$jump = new Jump(); $monkey->accept($speak); // Ooh oo aa aa! $monkey->accept($jump); // Jumped 20 feet high! on to the tree! $lion->accept($speak); // Roaaar! $lion->accept($jump); // Jumped 7 feet! Back on the ground! $dolphin->accept($speak); // Tuut tutt tuutt! $dolphin->accept($jump); // Walked on water a little and disappeared |
? 策略模式
現實生活示例
考慮排序的例子,我們實現了氣泡排序,但資料開始增長,氣泡排序開始變得非常慢。為了解決這個問題,我們實現了快速排序。儘管快速排序演算法對於大型資料集來說效果很好,但對於較小的資料集卻非常慢。為了解決這個問題,我們實施了一個策略,小資料集使用氣泡排序,大資料集使用快速排序。
概述
策略模式允許你基於場景轉換演算法或策略。
維基百科
在計算機程式設計中,策略模式是一種行為設計模式,可以在執行時選擇演算法的行為。
程式設計示例
以上述排序為例,首先給出策略介面及不同的策略實現
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 |
interface SortStrategy { public function sort(array $dataset): array; } class BubbleSortStrategy implements SortStrategy { public function sort(array $dataset): array { echo "Sorting using bubble sort"; // Do sorting return $dataset; } } class QuickSortStrategy implements SortStrategy { public function sort(array $dataset): array { echo "Sorting using quick sort"; // Do sorting return $dataset; } } |
客戶端可以使用任意策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Sorter { protected $sorter; public function __construct(SortStrategy $sorter) { $this->sorter = $sorter; } public function sort(array $dataset): array { return $this->sorter->sort($dataset); } } |
用法
1 2 3 4 5 6 7 |
$dataset = [1, 5, 4, 3, 2, 8]; $sorter = new Sorter(new BubbleSortStrategy()); $sorter->sort($dataset); // Output : Sorting using bubble sort $sorter = new Sorter(new QuickSortStrategy()); $sorter->sort($dataset); // Output : Sorting using quick sort |
? 狀態模式
現實生活示例
想象一下,你正在使用一些繪圖應用程式,你可以選擇筆刷來繪畫,刷子根據所選顏色改變其行為,即如果選擇紅色,它將繪製為紅色,如果選擇藍色,那麼它將繪製藍色等。
概述
當狀態改變時,類的行為也發生改變。
維基百科
狀態模式是以物件導向的方式實現狀態機的行為設計模式。對於狀態模式,通過將每個單獨狀態實現為派生類的狀態模式介面, 來實現一個狀態機,並通過呼叫模式超類的方法來實現狀態轉換。狀態模式可以被解釋為一種策略模式,它能夠通過呼叫模式介面定義的方法來切換當前策略。
程式示例
以文字編輯器為例,編輯器可以改變文字的狀態如選中粗體,就會以粗體輸入文字,選中斜體便以斜體輸入。
首先是狀態介面和一些狀態實現
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 |
interface WritingState { public function write(string $words); } class UpperCase implements WritingState { public function write(string $words) { echo strtoupper($words); } } class LowerCase implements WritingState { public function write(string $words) { echo strtolower($words); } } class Default implements WritingState { public function write(string $words) { echo $words; } } |
然後是文字編輯器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class TextEditor { protected $state; public function __construct(WritingState $state) { $this->state = $state; } public function setState(WritingState $state) { $this->state = $state; } public function type(string $words) { $this->state->write($words); } } |
用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$editor = new TextEditor(new Default()); $editor->type('First line'); $editor->setState(new UpperCase()); $editor->type('Second line'); $editor->type('Third line'); $editor->setState(new LowerCase()); $editor->type('Fourth line'); $editor->type('Fifth line'); // Output: // First line // SECOND LINE // THIRD LINE // fourth line // fifth line |
? 模板方法模式
現實生活示例
假設我們要造一座房子,建造的大體步驟如下:
- 打地基
- 壘牆
- 封頂
- 鋪地板
這些步驟的順序不能被打亂,比如說,你不能在壘牆之前先封頂。但是其中的每一步可以定製,比如牆的材料可以使用木頭、聚酯纖維或者石頭。
概述
模板方法模式定義瞭如何執行某種演算法的框架,但是將這些步驟的實現推遲到子類中。
維基百科
在軟體工程中,模板方法模式是一種行為設計模式,用於定義操作中演算法的程式框架,將一些步驟推遲到子類實現。它允許在不改變演算法結構的情況下重新定義演算法的某些步驟。
程式示例
假如我們有一個構建工具,可以幫助我們測試,構建並生成構建報告(即程式碼覆蓋報告,linting報告等),並將應用程式部署到測試伺服器上。
首先是用於確定構建演算法框架的基類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
abstract class Builder { // Template method final public function build() { $this->test(); $this->lint(); $this->assemble(); $this->deploy(); } abstract public function test(); abstract public function lint(); abstract public function assemble(); abstract public function deploy(); } |
然後提供一些基類的實現
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 |
class AndroidBuilder extends Builder { public function test() { echo 'Running android tests'; } public function lint() { echo 'Linting the android code'; } public function assemble() { echo 'Assembling the android build'; } public function deploy() { echo 'Deploying android build to server'; } } class IosBuilder extends Builder { public function test() { echo 'Running ios tests'; } public function lint() { echo 'Linting the ios code'; } public function assemble() { echo 'Assembling the ios build'; } public function deploy() { echo 'Deploying ios build to server'; } } |
用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$androidBuilder = new AndroidBuilder(); $androidBuilder->build(); // Output: // Running android tests // Linting the android code // Assembling the android build // Deploying android build to server $iosBuilder = new IosBuilder(); $iosBuilder->build(); // Output: // Running ios tests // Linting the ios code // Assembling the ios build // Deploying ios build to server |
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!