本文同步更新於blog
情境:让我们试着作一个摩斯电码机,它会将一般句子转成摩斯电码的表示
<?php
namespace App\InterpreterPattern\MorseCode;
class Context
{
/**
* @var string
*/
public $text;
/**
* @param string $text
*/
public function __construct(string $text)
{
$this->text = $text;
}
}
主要是承载要解译的词句,
会随着解译进度,改变其内容。
<?php
namespace App\InterpreterPattern\MorseCode\Contracts;
use App\InterpreterPattern\MorseCode\Context;
interface Expression
{
/**
* 找出要解析的字串执行,并回传剩余字串
*
* @param Context $context
* @return Context
*/
public function interpret(Context $context): Context;
/**
* 解析字串後,印在控制台
*
* @param string $message
*/
public function execute(string $message);
}
这边说明一下,所谓的摩斯电码,
是利用滴答两种不同长短讯号的排列组合,
来表达每一个字母符号。
例如:A的表示为 (.-)。
而在此处的范例中,
同个单字的字母会用空格 ( ) 分开,
不同单字的字母则会用斜杠 (/) 分开。
例如:Good Morning的表示会是 (--. --- --- -.. / -- --- .-. -. .. -. --.)。
字母间不区分大小写。
按照上述规则,我想区分出两种表达式 (Expression)。
解译字母符号的为终端表达式 (Terminal Expression),
其他情况为非终端表达式 (NonTerminal Expression)。
想法是使用非终端表达式时,表示还有字需要解译。
<?php
namespace App\InterpreterPattern\MorseCode;
use App\InterpreterPattern\MorseCode\Contracts\Expression;
use App\InterpreterPattern\MorseCode\Context;
class NonTerminalExpression implements Expression
{
public function interpret(Context $context): Context
{
$head = ' ';
$context->text = trim($context->text);
$this->execute($head);
return $context;
}
/**
* @param string $message
*/
public function execute(string $message)
{
echo ' / ';
}
/**
* @param string $character
* @return boolean
*/
public function isSpace($character)
{
return $character == ' ';
}
}
此处interpret()方法会将目前解译到的词句,去除前後空白。
execute()方法则会印出斜杠 (/)。
而isSpace()方法,会在待会的客户端程序码用到。
<?php
namespace App\InterpreterPattern\MorseCode;
use App\InterpreterPattern\MorseCode\Contracts\Expression;
use App\InterpreterPattern\MorseCode\Context;
use App\InterpreterPattern\MorseCode\Exceptions\UndefinedTextException;
class TerminalExpression implements Expression
{
protected $mapping = [
'a' => '.-',
'b' => '-...',
'c' => '-.-.',
'd' => '-..',
'e' => '.',
'f' => '..-.',
'g' => '--.',
'h' => '....',
'i' => '..',
'j' => '.---',
'k' => '-.-',
'l' => '.-..',
'm' => '--',
'n' => '-.',
'o' => '---',
'p' => '.--.',
'q' => '--.-',
'r' => '.-.',
's' => '...',
't' => '-',
'u' => '..-',
'v' => '...-',
'w' => '.--',
'x' => '-..-',
'y' => '-.--',
'z' => '--..',
'0' => '-----',
'1' => '.----',
'2' => '..---',
'3' => '...--',
'4' => '....-',
'5' => '.....',
'6' => '-....',
'7' => '--...',
'8' => '---..',
'9' => '----.',
'.' => '.-.-.-',
',' => '--..--',
'?' => '..--..',
'/' => '-..-.',
"'" => '.----.',
'!' => '-.-.--',
];
public function interpret(Context $context): Context
{
$firstSpacePos = strpos($context->text, ' ');
if ($firstSpacePos) {
$head = substr($context->text, 0, $firstSpacePos);
$context->text = substr($context->text, $firstSpacePos);
} else {
$head = $context->text;
$context->text = '';
}
$this->execute($head);
return $context;
}
/**
* @param string $message
*/
public function execute(string $message)
{
$characters = str_split($message);
$lastKey = array_key_last($characters);
foreach ($characters as $key => $character) {
$this->encode($character);
if ($key == $lastKey) {
break;
}
$this->typeSpace();
}
}
/**
* @param string $character
*/
private function encode(string $character)
{
$character = strtolower($character);
if (!array_key_exists($character, $this->mapping)) {
throw new UndefinedTextException();
}
echo $this->mapping[$character];
}
private function typeSpace()
{
echo ' ';
}
}
此处interpret()方法会找出要解译的单字,并截断它。
execute()方法则会逐步印出单字中的每一个字母符号,彼此间以空格隔开。
<?php
namespace App\InterpreterPattern\MorseCode;
use App\InterpreterPattern\MorseCode\NonTerminalExpression;
use App\InterpreterPattern\MorseCode\TerminalExpression;
use App\InterpreterPattern\MorseCode\Context;
class Program
{
/**
* @var TerminalExpression
*/
protected $terminalExpression;
/**
* @var NonTerminalExpression
*/
protected $nonTerminalExpression;
public function __construct()
{
$this->terminalExpression = new TerminalExpression();
$this->nonTerminalExpression = new NonTerminalExpression();
}
/**
* @param string $text
*/
public function encode(string $text)
{
try {
$context = new Context(trim($text));
while (strlen($context->text) > 0) {
$firstCharacter = substr($context->text, 0, 1);
if ($this->nonTerminalExpression->isSpace($firstCharacter)) {
$context = $this->nonTerminalExpression->interpret($context);
continue;
}
$context = $this->terminalExpression->interpret($context);
}
} catch (\Throwable $th) {
throw $th;
}
}
}
最後让我们来看客户端程序码怎麽跑吧!
以Hello World为例:
[单一职责原则]
语境类别 (Context):负责乘载要解译的词句。
非终端表达式 (NonTerminal Expression):负责连结解译单字间的文法。
终端表达式 (Terminal Expression):负责解译每一个字母符号。
[开放封闭原则]
增加要转译的字母符号时,仅需修改终端表达式 (Terminal Expression)。
[依赖反转原则]
透过表达式 (Expression) 接口,
确保各个表达式都有interpret()方法与execute()方法。
最後附上类别图:
(注:若不熟悉 UML 类别图,可参考UML类别图说明。)
现实中几乎没有机会使用到的设计模式,
范例想了很多天,希望这样有传达出这个模式的精神!
另外这个范例还没有完成decode()方法,
也就是从摩斯电码转回一般句子。
之後有时间会试着实作看看。
ʕ •ᴥ•ʔ:目前心目中前三难的设计模式。
<<: Kerckhoffs的原则-开源(Open source)
>>: 【如何设计软件 ? 】领域驱动设计 | 4 层架构 + 3 类物件
这个暑假就像开头第一篇说的,应该是大部分人度过最长的一个暑假,我原本也没什麽目标,打算好好休养生息,...
Day 11 - Kotlin的函式(2) 昨天我们讲了list集合,以及如何取得数值,今天我们要继...
使用终端机搜寻特定字串时,大家一定用过 grep 这个指令吧~ 但你有想过 grep 为什麽叫 gr...
昨天已经用PostgreSQL做了范例,今天要轮到PHP当主角了,从DockerHub下载下来最原始...
题目 Given a string, find the first non-repeating ch...