{"id":20619712,"url":"https://github.com/twtrubiks/line-bot-oop","last_synced_at":"2025-08-04T01:37:20.665Z","repository":{"id":84518966,"uuid":"163133263","full_name":"twtrubiks/line-bot-oop","owner":"twtrubiks","description":" line-bot refactor use oop (design pattern)","archived":false,"fork":false,"pushed_at":"2022-06-25T04:24:34.000Z","size":15,"stargazers_count":11,"open_issues_count":0,"forks_count":6,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-15T11:55:09.517Z","etag":null,"topics":["design-patterns","oop","refactor","singleton","strategy"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/twtrubiks.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-26T03:44:39.000Z","updated_at":"2024-11-24T04:30:07.000Z","dependencies_parsed_at":"2023-03-02T04:30:27.323Z","dependency_job_id":null,"html_url":"https://github.com/twtrubiks/line-bot-oop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fline-bot-oop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fline-bot-oop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fline-bot-oop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twtrubiks%2Fline-bot-oop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/twtrubiks","download_url":"https://codeload.github.com/twtrubiks/line-bot-oop/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249067775,"owners_count":21207395,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["design-patterns","oop","refactor","singleton","strategy"],"created_at":"2024-11-16T12:12:20.377Z","updated_at":"2025-04-15T11:55:14.451Z","avatar_url":"https://github.com/twtrubiks.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# line-bot-oop\n\n[Youtube Tutorial - 使用 oop 重構 ( refactor )-封裝 繼承 Singleton-PART 1](https://youtu.be/6j_IVQUH8yk)\n\n[Youtube Tutorial - 使用 oop 重構 ( refactor )-Strategy-PART 2](https://youtu.be/fdPkZ3sqfI8)\n\n本篇文章主要是將 [line-bot-tutorial](https://github.com/twtrubiks/line-bot-tutorial) repo refactor 成 oop 📝\n\noop 全名為 Object-oriented programming ( 物件導向 )，如不了解請自行 google :smile:\n\n我會使用 code 說明一些我 refactor 的重點 ( design pattern )。\n\n## 說明\n\n### Singleton\n\n首先，來看 [config.py](config.py)，\n\n```python\nclass Singleton(type):\n    _instances = {}\n\n    def __call__(cls, *args, **kwargs):\n        if cls not in cls._instances:\n            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)\n        return cls._instances[cls]\n\n\nclass Config(metaclass=Singleton):\n    def __init__(self, file='config.ini'):\n        ......\n```\n\n這邊我主要是使用了 design pattern 中的 singleton ( 單例模式 )，可參考 [creating-a-singleton-in-python](https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python)，\n\n什麼是 singleton，簡單說就是假如你希望一個系統中，某一個 class **只能出現一個** instance 時，就能使用它。\n\n像這邊使用在 `Config` 就很適合，因為整個系統，我只需要一個 `Config` 的 instance，我不需要很多個，多個\n\n除了浪費資源外，沒有什麼好處，看下面的例子，\n\n```python\n\u003e\u003e\u003e from config import Config\n\u003e\u003e\u003e c1 = Config()\n\u003e\u003e\u003e c2 = Config()\n\u003e\u003e\u003e print( id(c1) == id(c2) ) # \u003c1\u003e\nTrue\n```\n\n\u003c1\u003e 的部分為 `True`，代表它是同一個 instance ( 如果沒有使用 singleton，c1 和 c2 的 id 一定不一樣)。\n\n### 封裝 和 繼承\n\n接著看 [task.py](task.py)，\n\n這裡主要是將原本寫的一堆 function programming 改成 oop，把每個功能 **封裝** 成 class，\n\n然後使用到 **繼承** 的概念，說明如下，\n\n```python\nclass Crawler:\n    rs = requests.session()\n\n    def __init__(self, target_url, method='get'):\n        print('Start Crawler....{}'.format(self.__class__.__name__))\n        self.url = target_url\n        self.content = self.analyze(method)\n\n    def analyze(self, method):\n        .......\n        return soup\n\nclass EynyMovie(Crawler):\n    def parser(self):\n        ......\n        return result\n\n    @staticmethod\n    def pattern_mega(text):\n        ......\n        return match\n```\n\n我先定義 `Crawler` class，然後其他的功能 ( 像是 `EynyMovie` class ) 都去繼承這個 `Crawler`，\n\n依照自己的需求再去實作 `parser` 這個 method。\n\n這邊有使用到 `staticmethod`，如果你不了解，可參考 [What is the classmethod and staticmethod](https://github.com/twtrubiks/python-notes/tree/master/what_is_classmethod_and_staticmethod)。\n\n以下再說明一個 `staticmethod` 的例子，\n\n```python\nclass PttBeauty(Crawler):\n    parser_page = 2  # crawler count\n    push_rate = 10  # 推文\n\n    def parser(self):\n        url = 'https://www.ptt.cc/bbs/Beauty/index{}.html'\n        index_seqs = PttBeauty.get_all_index(self.content, url, self.parser_page)\n        ......\n\n    def crawler_info(self, res):\n        ......\n\n    @staticmethod\n    def get_all_index(content, url, parser_page): # \u003c1\u003e\n        max_page = PttBeauty.get_max_page(content.select('.btn.wide')[1]['href'])\n        ......\n        return queue\n\n    @staticmethod\n    def get_max_page(content): # \u003c2\u003e\n        ......\n        return int(page_number) + 1\n\n\nclass PttGossiping(Crawler):\n    parser_page = 2  # crawler count\n\n    def parser(self):\n        url = 'https://www.ptt.cc/bbs/Gossiping/index{}.html'\n        index_seqs = PttBeauty.get_all_index(self.content, url, self.parser_page) # \u003c3\u003e\n        ......\n```\n\n因為 `PttBeauty` class 以及 `PttGossiping` class 都會使用到 `get_all_index` 以及 `get_max_page`\n\n這兩個 function，所以我將它們加上 `staticmethod` (\u003c1\u003e 和 \u003c2\u003e )，然後看 \u003c3\u003e 的部分，這裡\n\n直接使用 `PttBeauty.get_all_index()` 去得到我們需要的資訊。\n\n雖然這邊也可以將 `get_all_index` 以及 `get_max_page` 這兩個 function 單獨抽出去，但為了\n\n方便管理以及維護，統一寫在 `PttBeauty` class 中。\n\n### Strategy\n\n再來是 [strategy.py](strategy.py)，\n\n這邊使用了 design pattern 中的 strategy ( 策略模式 )，\n\n先來說明一下策略模式，主要是利用 python 是動態語言的關係，動態去抽換 function，\n\n可參考 [python-patterns-strategy.py](https://github.com/faif/python-patterns/blob/master/behavioral/strategy.py)\n\n```python\nimport types\nclass StrategyExample:\n    def __init__(self, func=None):\n        self.name = 'Strategy Example 0'\n        if func is not None:\n            self.execute = types.MethodType(func, self) # \u003c1\u003e\n\n    def execute(self):\n        print(self.name)\n\ndef execute_replacement1(self):\n    print(self.name + ' from execute 1')\n\ndef execute_replacement2(self):\n    print(self.name + ' from execute 2')\n\nif __name__ == '__main__':\n    strat0 = StrategyExample()\n\n    strat1 = StrategyExample(execute_replacement1)\n    strat1.name = 'Strategy Example 1'\n\n    strat2 = StrategyExample(execute_replacement2)\n    strat2.name = 'Strategy Example 2'\n\n    strat0.execute()\n    strat1.execute()\n    strat2.execute()\n```\n\n\u003c1\u003e 的部分就是去抽換 function，有點 Monkey Patch 的概念，\n\n`types.MethodType(func, self)` 的用法之前也介紹過了，\n\n可參考 [What is the Monkey Patch](https://github.com/twtrubiks/fluent-python-notes/tree/master/what_is_the_Monkey_Patch)。\n\n了解完 strategy 之後，接著來看如何應用，\n\n這邊建立 3 個 strategy，然後主要繼承 `TaskStrategy` class，\n\n程式碼請看 [strategy.py](strategy.py)，\n\n```python\nclass TaskStrategy:\n    def __init__(self, func=None, event=None):\n        self.name = func.__name__ if func else \"default\"\n        self.event = event\n        if func:\n            self.execute = types.MethodType(func, self)\n        print('{} class , task {}'.format(self.__class__.__name__, self.name))\n\n    def execute(self):\n        pass\n\n    def reply_message(self, obj):\n        line_bot_api.reply_message(self.event.reply_token, obj)\n\nclass TemplateStrategy(TaskStrategy):\n    def execute(self):\n        ......\n        self.reply_message(carousel_template_message)\n\nclass ImageStrategy(TaskStrategy):\n    def execute(self):\n        ......\n        self.reply_message(sticker_message)\n```\n\n`TaskStrategy` class，主要是給個別的 task ( 功能 ) 使用。\n\n在 [task.py](task.py) 中，我們已經依照功能建立很多 class，\n\n所以在這階段使用就很簡單，像是要呼叫新聞的爬蟲，\n\n直接寫這樣即可，如下，\n\n```python\ndef apple_news(self):\n    task = AppleNews('https://tw.appledaily.com/new/realtime')\n    self.reply_message(TextSendMessage(text=task.parser()))\n```\n\n依照 class 建立 instance，然後都去執行 parser 這個 method。\n\n`TemplateStrategy` class，處理 template ( 清單顯示 )，所以獨立出來。\n\n`ImageStrategy` class，專門處理圖片 ( 雖然目前只有一個 )。\n\n最後是 [app.py](app.py)，\n\n首先是 import 的部分，盡量不要使用 `from xx import *` 這種方法，\n\n需要什麼再 import 就好，像是 `from xx import a,b,c` 這樣，另外\n\n還要小心 **Circular Imports** 的問題，我之前也介紹過了，\n\n可參考 [circular import](https://github.com/twtrubiks/python-notes/tree/master/python_circular_import)。\n\n來看 `Bot` 這個 class，\n\n```python\nclass Bot:\n    # \u003c1\u003e\n    task_map = {\n        MyDict.eyny_movie: eyny_movie,\n        .....\n    }\n\n    # \u003c2\u003e\n    template_map = {\n        MyDict.start_template: start_template,\n        .....\n    }\n\n    def __init__(self, val):\n        self.val = val\n        self.special_handle()\n\n    def strategy_action(self): # \u003c3\u003e\n        strategy_class = None\n        action_fun = None\n        if self.val in self.task_map:\n            strategy_class = TaskStrategy\n            action_fun = self.task_map.get(self.val)\n        elif self.val in self.template_map:\n            strategy_class = TemplateStrategy\n            action_fun = self.template_map.get(self.val)\n        return strategy_class, action_fun\n\n    def special_handle(self):\n        if self.val.lower() == MyDict.eyny_movie:\n            self.val = self.val.lower()\n```\n\n\u003c1\u003e 和 \u003c2\u003e 的部分主要是將 message 和 function 名稱 mapping 起來，\n\n\u003c3\u003e 的部分則是 mapping Strategy ( strategy_class ) 以及 action ( action_fun )，\n\n需要 \u003c1\u003e 和 \u003c2\u003e 的部分，主要是可以避免很多的 `if` `else`。\n\n最後看 `handle_message` 的部分，\n\n這邊和當初未 refactor ([app.py](https://github.com/twtrubiks/line-bot-tutorial/blob/master/app.py)) 的相比，明顯簡潔有力多了，\n\n```python\n@handler.add(MessageEvent, message=TextMessage)\ndef handle_message(event):\n    message = event.message.text\n    bot = Bot(message)\n    strategy_class, action_fun = bot.strategy_action() # \u003c1\u003e\n    if strategy_class:\n        # \u003c2\u003e\n        task = strategy_class(action_fun, event)\n        task.name = str(action_fun)\n        task.execute()\n        return 0\n    default_task = TemplateStrategy(event=event) # \u003c3\u003e\n    default_task.execute()\n```\n\n\u003c1\u003e 的部分得到 strategy_class 和 action_fun，\n\n接著在 \u003c2\u003e 的部分直接將 strategy_class 和 action_fun 丟進去 ( 依照 strategy ) 就可以了。\n\n最後 \u003c3\u003e 的部分則是 default 的 template 顯示 ( message 完全沒有 mapping )。\n\n## 結論\n\n功能和之前未 refactor ( [app.py](https://github.com/twtrubiks/line-bot-tutorial/blob/master/app.py) ) 的完全一模一樣，\n\n主要是修改成 oop，然後應用一些 design patterns，方便後續的維護。\n\n程式碼也都部署到 heroku 上了，有興趣可掃下面的 QRCODE 玩玩看:smile:\n\n## 執行結果\n\nline 的 QRCODE\n\n![alt tag](http://i.imgur.com/Kkpzt4p.jpg)\n\n或是手機直接點選 [https://line.me/R/ti/p/%40vbi2716y](https://line.me/R/ti/p/%40vbi2716y)\n\n![alt tag](http://i.imgur.com/oAgR5nr.jpg)\n\n## 執行環境\n\n* Python 3.9\n\n## Donation\n\n文章都是我自己研究內化後原創，如果有幫助到您，也想鼓勵我的話，歡迎請我喝一杯咖啡:laughing:\n\n![alt tag](https://i.imgur.com/LRct9xa.png)\n\n[贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8)\n\n## License\n\nMIT license","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwtrubiks%2Fline-bot-oop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftwtrubiks%2Fline-bot-oop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwtrubiks%2Fline-bot-oop/lists"}