pos機代理找對象難,面向對象編程已死

 新聞資訊  |   2023-04-22 09:41  |  投稿人:pos機之家

網上有很多關于pos機代理找對象難,面向對象編程已死的知識,也有很多人為大家解答關于pos機代理找對象難的問題,今天pos機之家(m.fqd168.com)為大家整理了關于這方面的知識,讓我們一起來看下吧!

本文目錄一覽:

1、pos機代理找對象難

pos機代理找對象難

【CSDN編者按】ECS(ECS,Entity–component–system,實體組件系統,是一種主要用于游戲開發的架構模式),是在游戲開發社區廣為流傳的偽模式,它基本上是關系模型的翻版,其中“實體”是ID,表示無形的對象,“組件”是特定表中的行,該行引用一個ID,而“系統”是更改組件的過程性的代碼。這種“模式”經常會導致繼承的過度使用,而不會提及過度使用繼承,其實違反了OOP(OOP,Object Oriented Programming,面向對象編程,是一種計算機編程架構)原則。那么如何避免這種情況呢?本文作者,會給大家介紹下真正的設計指南。1.靈感

這篇文章的靈感,來自最近Unity的知名工程師Aras Pranckevi?ius一次面向初級開發者的公開演講,演講的目的是讓他們熟悉新的“ECS”架構的一些術語。

Aras使用了非常典型的模式,他展示了一些非常糟糕的OOP代碼,然后表示關系模型是個更好的方案(不過這里的關系模型稱為“ECS”)。我并不是要批評Aras,實際上我很喜歡他的作品,也非常贊賞他的演講!

我選擇他的演講而不是網上幾百篇關于ECS的其他帖子的原因是,他的演講給出了代碼,里面有個非常簡單的小“游戲”用來演示各種不同的架構。這個小項目節省了我很多精力,可以方便我闡述自己的觀點,所以,謝謝Aras!

Aras幻燈片的鏈接:

http://aras-p.info/texts/files/2018Academy - ECS-DoD.pdf

代碼鏈接:

https://github.com/aras-p/dod-playground

我不想分析他的演講最后提出的ECS架構,只想就他批判的“壞的OOP”代碼來說說我的看法。我想論述的是,如果我們能改正所有違反OOD(面向對象設計)原則的地方,會變成什么樣子。

劇透警告:改正違反OOD的代碼,能得到與Aras的ECS版本相似的性能改進,而且還能比ECS版本占用更少的內存,代碼量也更少!

概括為一句話:如果你認為OOP是垃圾、而ECS才是王道,那么先去了解一下OOD(即怎樣正確使用OOP),再學學關系模型(了解怎樣正確使用ECS)。

我一直很反感論壇上的許多關于ECS的帖子,部分原因是我覺得ECS夠不上單獨弄個術語的程度(劇透:它只不過是關系模型的專用版本),另一部分原因是所有宣揚ECS模式的帖子、幻燈片或文章都有著同樣的結構:

展示一些很糟糕的OOP代碼,其設計很垃圾,通常是過度使用繼承(這一條就違反了許多OOD原則)。證明組合要比繼承更好(其實OOD早就這么說過)。證明關系模型很適合游戲開發(只不過改名叫ECS)。

這種結構的文章很讓我惱火,因為:

偷換概念。它對比的對象風馬牛不相及,這一點很難讓人信服,雖然可能是出于無意,卻也并不能證明它提出的新架構更好。它會產生副作用,貶低知識,并且無意間打擊讀者去學習該領域長達五十多年的研究結果。關系模型第一次是在上世紀六十年代提出的。七八十年代深入研究了該模型的各個方面。新手經常提出的問題是“這個數據應該放到哪個類里?”而該問題的答案通常很模糊,“等你有了更多經驗以后自然而然就知道了”。但在七十年代,這個問題深入地研究,并用通用的、正式的方式解決了,即數據庫的正規化(https://en.wikipedia.org/wiki/Database_normalization#Normal_forms)。忽略已有的研究成果把ECS當作全新的方案來展示,就等于把這些知識藏起來不告訴新手程序員。

面向對象編程的歷史也同樣悠久(實際上比關系模型還要久,它的概念從上世紀五十年代就出現了)!但是,直到九十年代,OO才得到人們的關注,成了主流的編程范式。各種各樣的OO語言雨后春筍般地出現,其中就包括Java和(標準版本的)C++。

但由于它是被炒作起來的,所以每個人只是把這個詞寫到自己的簡歷上,真正懂得它的人少之又少。這些新語言引入了許多關鍵字來實現OO的功能,如CLASS、Virtual、extends、implements,我認為自此OO分成了兩派。

后面我把擁有OO思想的編程語言稱為“OOP”,使用OO思想的設計和架構技術稱為“OOD”。每個人學習OOP都很快,學校里也說OO類非常高效,很適合新手程序員……但是,OOD的知識卻被拋在了后面。

我認為,使用OOP的語言特性卻不遵循OOD設計規則的代碼,不是OO代碼。大多數反對OO的文章所攻擊的代碼都不是真正的OO代碼。

OOP代碼的名聲很差,其中部分原因就是大多數OOP代碼沒有遵循OOD原則,所以其實不是真正的OO代碼。

2.背景

前面說過,上世紀九十年代是OO的大爆炸時代,那個時期的“壞OOP代碼”可能是最糟糕的。如果你在那個時期學習了OOP,那么你很可能學過下面的“OOP四大支柱”:

抽象封裝多態繼承

我更傾向于稱他們為“OOP的四大工具”而不是四大支柱。這些工具可以用來解決問題。但是,只學習工具的用法是不夠的,你必須知道什么時候應該使用它們。

教育者只傳授工具的用法而不傳授工具的使用場景,是不負責任的表現。在二十一世紀初,第二波OOD思潮出現,工具的濫用得到了一定的抑制。

當時提出了SOLID(https://en.wikipedia.org/wiki/SOLID)思想體系來快速評價設計的質量。注意其中的許多建議其實在上世紀九十年代就廣為流傳了,但當時并沒有像“SOLID”這種簡單好記的詞語將其提煉成五條核心原則……

單一職責原則(Single Responsibility Principle)。每個類應該只有一個目的。如果類A有兩個目的,那么分別創建類B和類C來處理每個目的,再從B和C中提煉出A。開放/封閉原則(Open / Closed Principle)。軟件隨時都在變化(即維護很重要)。把可能會變化的部分放到實現(即具體的類)中,給不太可能會變化的東西建立接口(比如抽象基類)。里氏替換原則(Liskov Substitution Principle)。每個接口的實現都應該100%遵循接口的要求,即任何能在接口上運行的算法都應該能在具體的實現上運行。接口隔離原則(Interface Segregation Principle )。接口應當盡量小,保證每一部分代碼都“只需了解”最小量的代碼,也就是說避免不必要的依賴。這一條建議對C++也很好用,因為不遵循這條原則會讓編譯時間大幅增長。依賴倒置原則(Dependency Inversion Principle)。兩個具體的實現直接通信并且互相依賴的模式,可以通過將兩者之間的通信接口正規化成第三個類,將這個類作為兩者之間的接口的方式解耦合。這第三個類可以是個抽象積累,定義兩者之間需要的調用,甚至可以只是個定義兩者間傳遞數據的簡單數據結構。這一條不在SOLID中,但我認為這一條同樣重要:組合重用原則(Composite Reuse Principle)。默認情況下應當使用組合,只有在必須時才使用繼承。

這才是我們的SOLID C++。

接下來我用三字母的簡稱來代表這些原則:SRP、OCP、LSP、ISP、DIP、CRP。

一點其他看法:

在OOD中,接口和實現并不對應任何具體的OOP關鍵字。在C++中,接口通常用抽象類和虛函數建立,然后實現從基類繼承……但那只是實現接口的概念的一種方式而已。C++中能使用PIMPL(https://en.cppreference.com/w/cpp/language/pimpl)、不透明指針(https://en.wikipedia.org/wiki/Opaque_pointer)、鴨子類型(https://en.wikipedia.org/wiki/Duck_typing)、typedef等……你甚至可以創建OOD的設計,然后用完全不支持OOP關鍵字的C語言實現!所以我這里說的接口指的并不一定是虛函數,而是隱藏實現的思想(https://en.wikipedia.org/wiki/Information_hiding)。接口可以是多態的(https://en.wikipedia.org/wiki/Polymorphism_(computer_science)),但大多數情況下并不是!好的多態非常罕見,但任何軟件都會用到接口。上面說過,如果建立一個簡單的數據結構負責從一個類傳遞數據到另一個類,那么該結構就起到了接口的作用——用正式的語言來說,這叫數據定義(https://en.wikipedia.org/wiki/Data_definition_language)。即使只是將一個類分成了公有和私有兩部分,那么所有公有部分中的東西都是接口,而私有部分的都是實現。繼承實際上(至少)有兩種類型:接口繼承,實現繼承。在C++中,接口繼承包括:利用純虛函數實現的抽象基類、PIMPL、條件typedef。在Java中,接口繼承用implements關鍵字表示。在C++中,實現繼承發生在一切基類包含純虛函數以外的內容的情況。在Java中,實現繼承用Extends關鍵字表示。OOD定義了許多關于接口繼承的規則,但實現繼承通常是不祥的預兆(https://en.wikipedia.org/wiki/Code_smell)。

最后,我也許應該給出一些糟糕的OOP教育的例子,以及這種教育導致的糟糕代碼(以及OOP的壞名聲)。

在學習層次結構和繼承時,你很可能學習過以下類似的例子:

假設我們有個學校的應用,其中包括學生和教職工的名錄。于是我們可以用Person作為基類,然后從Person繼承出Student和Staff兩個類。這完全錯了。先等一下。LSP(里氏替換原則)指出,類的層次結構和操作它們的算法是共生(symbiotic)的。它們是一個完整程序的兩個部分。OOP是過程式編程的擴展,它的主要結構依然是過程。所以,如果不知道Student和Staff上的算法(以及哪些算法可以用多態來簡化),那么設計類層次結構是不負責任的。必須首先有算法和數據才能繼續。

在學習層次結構和繼承時,你很可能學習過以下類似的例子:

假設你有個形狀的類。它的子類可以有正方形和矩形。那么,應該是正方形is-a矩形,還是矩形is-a正方形?

這個例子其實很好地演示了實現繼承和接口繼承之間的區別。

如果你考慮的是實現繼承,那么你完全沒有考慮LSP,只不過是把繼承當做復用代碼的工具而已。從這個觀點來看,下面的定義是完全合理的: struct Square { int width="360px",height="auto" />

你一定猜到了,OOD認為這種設計(很可能)錯了。我說可能的原因是你還可以爭論其中暗含的接口……不過這無關緊要。

正方形的寬度和高度永遠相同,所以從正方形的接口的角度來看,我們完全可以認為它的面積是“寬度×寬度”。

如果矩形從正方形繼承,那么根據LSP,矩形必須遵守正方形接口的規則。所有能在正方形上正確工作的算法必須能在矩形上正確工作。

比如下面的算法:std::vector<Square*> shapes; int area = 0; for (auto s: shapes) area += s->width="360px",height="auto" />

如果用接口繼承的方式來思考,那么無論是正方形還是矩形,都不應該從對方繼承。正方形和矩形的接口實際上是不同的,誰都不是誰的超集。所以,OOD實際上并不鼓勵實現繼承。前面說過,如果你要復用代碼,OOD認為應該使用組合!所以,上面實現繼承的層次結構代碼的正確版本,用C++來寫應該是這樣:

struct Shape { virtual int area() const = 0; };struct Square : public virtual Shape { virtual int area() const { return width="360px",height="auto" />

public virtual相當于Java中的implements,在實現一個接口時使用。private可以讓你從基類繼承,而無需繼承它的接口。在本例中,Rectangle is-not-a Square,雖然它繼承了Square。我不推薦這樣寫代碼,但如果你真想使用實現繼承,那么這才是正確的寫法!

總之一句話,OOP課程教給你什么是繼承,而你沒有學習的OOD課程本應教給你在99%的情況下不要使用繼承!

3.實體 / 組件框架

有了這些背景之后,我們來看看Aras開頭提出的那些所謂的“常見的OOP”。

實際上我還要說一句,Aras稱這些代碼為“傳統的OOP”,而我并不這樣認為。這些代碼也許是人們常用的OOP,但如上所述,這些代碼破壞了所有核心的OO規則,所以它們完全不是傳統的OOP。

我們從最早的提交開始——當時他還沒有把設計修改成ECS:"Make it work on Windows again"(https://github.com/aras-p/dod-playground/blob/3529f232510c95f53112bbfff87df6bbc6aa1fae/source/game.cpp):

class GameObject;class Component;typedef std::vector<Component*> ComponentVector;typedef std::vector<GameObject*> GameObjectVector;class Component{public: Component() : m_GameObject(nullptr) {} virtual ~Component() {} virtual void Start() {} virtual void Update(double time, float deltaTime) {} const GameObject& GetGameObject() const { return *m_GameObject; } GameObject& GetGameObject() { return *m_GameObject; } void SetGameObject(GameObject& go) { m_GameObject = &go; } bool HasGameObject() const { return m_GameObject != nullptr; }private: GameObject* m_GameObject;};class GameObject{public: GameObject(const std::string&& name) : m_Name(name) { } ~GameObject() { for (auto c : m_Components) delete c; } template<typename T> T* GetComponent() { for (auto i : m_Components) { T* c = dynamic_cast<T*>(i); if (c != nullptr) return c; } return nullptr; } void AddComponent(Component* c) { assert(!c->HasGameObject()); c->SetGameObject(*this); m_Components.emplace_back(c); } void Start() { for (auto c : m_Components) c->Start(); } void Update(double time, float deltaTime) { for (auto c : m_Components) c->Update(time, deltaTime); } private: std::string m_Name; ComponentVector m_Components;};static GameObjectVector s_Objects;template<typename T>static ComponentVector FindAllComponentsOfType(){ ComponentVector res; for (auto go : s_Objects) { T* c = go->GetComponent<T>(); if (c != nullptr) res.emplace_back(c); } return res;}template<typename T>static T* FindOfType(){ for (auto go : s_Objects) { T* c = go->GetComponent<T>(); if (c != nullptr) return c; } return nullptr;}

OK,代碼很難一下子看懂,所以我們來分析一下……不過還需要另一個背景:在上世紀九十年代,使用繼承解決所有代碼重用問題,這在游戲界是通用的做法。首先有個Entity,然后擴展成Character,再擴展成Player和Monster等等……

如前所述,這是實現繼承,盡管一開始看起來不錯,但最后會導致極其不靈活的代碼。因此,OOD才有“使用組合而不是繼承”的規則。因此,在本世紀初“使用組合而不是繼承”的規則變得流行后,游戲開發才開始寫這種代碼。

這段代碼實現了什么?總的來說都不好,呵呵。

簡單來說,這段代碼通過運行時函數庫重新實現了組合的功能,而不是利用語言特性來實現。

你可以認為,這段代碼在C++之上構建了一種新的語言,以及運行這種語言的編譯器。Aras的示例游戲并沒有用到這段代碼(我們一會兒就會把它都刪掉了!),它唯一的用途是將游戲的性能降低10倍。

它實際上做了什么?這是個“實體/組件”(Entity/Component)框架(有時候會被誤稱為“實體/組件系統”),但它跟“實體組件系統”(Entity Component System)框架完全沒關系(后者很顯然不會被稱為“實體組件系統”)。

游戲從一個無功能的“實體”開始(本例中稱為GameObject),這些實體自身由“組件”(Component)構成。GameObject實現了服務定位器模式(Service Locator Pattern,https://en.wikipedia.org/wiki/Service_locator_pattern),這種模式可以通過類型查詢子組件。Component知道自己屬于哪個GameObject,它們可以通過查詢父GameObject來定位兄弟組件。組合僅限于單層(Component不能擁有子組件,GameObject也不能擁有子GameObject)。GameObject只能有各種類型的組件各一個(有些框架要求這一點,有些不要求)。所有組件(可能)都會以未知的方式改變,因此接口定義為“virtual void Update”。GameObject屬于場景,場景可以查詢所有GameObject(因此可以繼續查詢所有Component)。

這種框架在本世紀初非常流行,盡管它很嚴格,但提供了足夠的靈活性來支持無數的游戲,直到今天依然如此。

但是,這種框架并不是必須的。編程語言的特性中已經提供了組合,不需要再用框架實現一遍……那為什么還需要這些框架?那是因為框架可以實現動態的、運行時的組合。

GameObject無須硬編碼,可以從數據文件中加載。這樣游戲設計師和關卡設計師就可以創建自己的對象……但是,在大多數游戲項目中,項目的設計師都很少,而程序員很多,所以我認為這并不是關鍵的功能。何況,還有許多其他方式來實現運行時組合!

例如,Unity使用C#作為其“腳本語言”,許多其他游戲使用Lua等替代品,所以面向設計師的工具可以生成C#/Lua代碼來定義新的游戲對象,而不需要這些框架!

我們會在以后的文章里重新加入運行時組合的“功能”,但要同時避免10倍的性能開銷……

如果我們用OOD的觀點評價這段代碼:

GameObject:GetComponent使用了dynamic_cast。大多數人都會告訴你,dynamic_cast是一種代碼異味——它強烈地暗示著代碼什么地方有問題。我認為,它預示著你的代碼違反了LSP——某個算法在操作基類的解耦,但它要求了解不同實現的細節。這正是代碼異味的原因。GameObject還算可以,如果認為它實現了服務定位器模式的話……但是從OOD的觀點來看,這種模式在項目的不同部分之間建立了隱含的聯系,而且我認為(我找不到能用計算機科學的知識支持我的維基鏈接)這種隱含的通信通道是一種反面模式(https://en.wikipedia.org/wiki/Anti-pattern),應當使用明示的通信通道。這種觀點同樣適用于一些游戲中使用的“事件框架”……我認為,Component違反了SRP(單一責任原則),因為它的接口( virtual void Update(time))太寬泛了。“virtual void Update”在游戲開發中非常普遍,但我還是要說這是個反面模式。好的軟件應該可以很容易地論證其控制流和數據流。將一切游戲代碼放在“virtual void Update”調用后面完全混淆了控制流和數據流。在我看來,不可見的副作用(https://en.wikipedia.org/wiki/Side_effect_(computer_science))——也稱為“遠隔作用”(https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming)——是最常見的Bug來源,而“virtual void Update”使得一切都擁有不可見的副作用。盡管Component類的目的是實現組合,但它是通過繼承實現的,這違反了CRP(組合重用原則)。

這段代碼好的一方面在于,它滿足了SRP和ISP(接口隔離原則),分割出了大量的簡單組件,每個組件的責任非常小,這一點非常適合代碼重用。

但是,它在DIP(依賴反轉原則)方面做得不好,許多組件都互相了解對方。

所以,我上面貼出的所有代碼實際上都可以刪掉了。整個框架都可以刪掉。刪掉GameObject(即其他框架中的Entity),刪掉Component,刪掉Find Of Type。這些都是無用的VM中的一部分,破壞了OOD的規則,使得游戲變得非常慢。

4.無框架組合(即使用編程語言的功能實現組合)

如果刪掉整個組合框架,并且沒有Component基類,我們怎樣才能使用組合來管理GameObject呢?

我們不需要寫VM再在我們自己的奇怪的語言之上實現GameObject,我們可以使用C++自身的功能來實現,因為這就是我們游戲程序員的工作。

下面的提交中刪除了整個實體/組件框架:

https://github.com/hodgman/dod-playground/commit/f42290d0217d700dea2ed002f2f3b1dc45e8c27c

下面是原始版本的代碼:

https://github.com/hodgman/dod-playground/blob/3529f232510c95f53112bbfff87df6bbc6aa1fae/source/game.cpp

下面是改進后的代碼:

https://github.com/hodgman/dod-playground/blob/f42290d0217d700dea2ed002f2f3b1dc45e8c27c/source/game.cpp

這段改動包括:

從每個組件類型中刪掉了“: public Component”。給每個組件類型添加了構造函數。OOD的主旨是封裝類的狀態,但這些類非常小、非常簡單,所以沒有太多東西需要隱藏,它的接口只是數據描述而已。但是,封裝成為面向對象支柱的主要原因之一是,它可以讓類不變量(class invariant,https://en.wikipedia.org/wiki/Class_invariant)永遠為真……或者說,在違反某個不變量時,你只需要檢查封裝的實現代碼就能找到Bug。在這段示例代碼中,我們值得添加一個構造函數來確保一個簡單的不變量,即所有值必須被初始化。我將過于通用的“Update”方法改名,使之能夠反映出實際功能,比如MoveComponent的叫做Update Position,Avoid Component的叫做Resolve Collisions。我刪掉了三段有關模板和預制組件(Prefab)硬編碼的代碼,即創建包含特定Component類型的GameObject代碼,并用三個C++類來代替。修正了“virtual void Update”反面模式。不再讓組件通過服務定位器模式互相查找,而是讓GameObject在構造過程中直接鏈接組件。5.對象

這樣,我們不再使用下面的“VM”代碼:

for (auto i = 0; i < kObjectCount; ++i) { GameObject* go = new GameObject("object"); PositionComponent* pos = new PositionComponent(); pos->x = RandomFloat(bounds->xMin, bounds->xMax); pos->y = RandomFloat(bounds->yMin, bounds->yMax); go->AddComponent(pos); SpriteComponent* sprite = new SpriteComponent(); sprite->colorR = 1.0f; sprite->colorG = 1.0f; sprite->colorB = 1.0f; sprite->spriteIndex = rand() % 5; sprite->scale = 1.0f; go->AddComponent(sprite); MoveComponent* move = new MoveComponent(0.5f, 0.7f); go->AddComponent(move); AvoidComponent* avoid = new AvoidComponent(); go->AddComponent(avoid); s_Objects.emplace_back(go); }

而是使用正常的C++實現:

struct RegularObject{ PositionComponent pos; SpriteComponent sprite; MoveComponent move; AvoidComponent avoid; RegularObject(const WorldBoundsComponent& bounds) : move(0.5f, 0.7f) , pos(RandomFloat(bounds.xMin, bounds.xMax), RandomFloat(bounds.yMin, bounds.yMax)) , sprite(1.0f, 1.0f, 1.0f, rand() % 5, 1.0f) { }};... regularObject.reserve(kObjectCount);for (auto i = 0; i < kObjectCount; ++i) regularObject.emplace_back(bounds);6.算法

現在另一個難題是算法。還記得開始時我說過,接口和算法是共生(Symbotic)的,兩者應該互相影響對方的設計嗎?“virtual void Update”反面模式也不適合這種情況。原始的代碼有個主循環算法,它的結構如下:

for (auto go : s_Objects) { go->Update(time, deltaTime);

你可能會認為這段代碼很簡潔,但我認為這段代碼很糟糕。它完全混淆了游戲中的控制流和數據流。

如果我們想理解軟件,維護軟件,給軟件添加新功能,優化軟件,甚至想讓它能在多個CPU核心上運行得更快,那么我們必須理解控制流和數據流。所以,“virtual void Update”不應該出現。

相反,我們應該使用更明確的主循環,才能讓論證控制流更容易(這里數據流依然被混淆了,我們會在稍后的提交中解決)。

for (auto& go : s_game->regularObject) { UpdatePosition(deltaTime, go, s_game->bounds.wb); } for (auto& go : s_game->avoidThis) { UpdatePosition(deltaTime, go, s_game->bounds.wb); } for (auto& go : s_game->regularObject) { ResolveCollisions(deltaTime, go, s_game->avoidThis); }

這種風格的缺點是,每加入一個新類型的對象,就要在主循環中添加幾行。我會在以后的文章中解決這個問題。

7.性能

現在代碼中仍然有違反OOD的地方,有一些不好的設計抉擇,還有許多可以優化的地方,但這些問題我會在以后的文章中解決。

至少在目前來看,這個“改正后的OOD”版本的性能不弱于Aras演講中最后的ECS版本,甚至可能超過它……

而我們所做的只是將偽OOP代碼刪除,并使用真正遵守OOP規則的代碼而已(并且刪除了100多行代碼!)。

9.下一步

我還想談更多的問題,包括解決殘余的OOD問題、不可更改的對象(函數式風格編程,https://en.wikipedia.org/wiki/Functional_programming),以及對數據流、消息傳遞的論證能帶來的好處。

并給我們的OOD代碼添加一些DOD論證,給OOD代碼添加一些關系型技巧,刪掉那些“實體”類并得到純粹由組件組成的、以不同風格互相鏈接的組件(指針 VS 事件處理),真實世界的組件容器,加入更多優化以跟上ECS版本,以及更多Aras的演講中都沒有提到的優化(如線程和SIMD)。所以,敬請期待我后續的文章……

原文:https://www.gamedev.net/blogs/entry/2265481-oop-is-dead-long-live-oop/作者:Brooke Hodgman,獨立游戲、圖形和引擎程序員,現居墨爾本,在GOATi Enterainment的22series.com工作譯者:彎月,責編:胡巍巍

以上就是關于pos機代理找對象難,面向對象編程已死的知識,后面我們會繼續為大家整理關于pos機代理找對象難的知識,希望能夠幫助到大家!

轉發請帶上網址:http://m.fqd168.com/news/22070.html

你可能會喜歡:

版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 babsan@163.com 舉報,一經查實,本站將立刻刪除。