সি ++ সিরিয়ালাইজেশন ডিজাইন পর্যালোচনা


9

আমি একটি সি ++ অ্যাপ্লিকেশন লিখছি। বেশিরভাগ অ্যাপ্লিকেশনগুলি ডেটা উদ্ধৃতি পড়তে এবং লেখার প্রয়োজন এবং এটি কোনও ব্যতিক্রম নয়। আমি ডেটা মডেল এবং সিরিয়ালাইজেশন লজিকের জন্য একটি উচ্চ স্তরের নকশা তৈরি করেছি। এই প্রশ্নটি এই নির্দিষ্ট লক্ষ্যগুলি মাথায় রেখে আমার নকশাটির পর্যালোচনা করার অনুরোধ করছে :

  • স্বেচ্ছাসেবী বিন্যাসে ডেটা মডেলগুলি পড়ার এবং লেখার একটি সহজ এবং নমনীয় উপায় হ'ল: কাঁচা বাইনারি, এক্সএমএল, জেএসএন, ইত্যাদি। অল। ডেটা ফর্ম্যাটটি নিজেই ডেটা থেকে কোড কোড হিসাবে সিরিয়ালাইজেশনের জন্য অনুরোধ করছে dec

  • নিশ্চিত করার জন্য যে সিরিয়ালাইজেশন যথাসম্ভব ত্রুটিমুক্ত। I / O বিভিন্ন কারণে স্বভাবগতভাবে ঝুঁকিপূর্ণ: আমার ডিজাইনটি এটি ব্যর্থ হওয়ার জন্য আরও কীভাবে প্রবর্তন করে? যদি তা হয় তবে কীভাবে আমি এই ঝুঁকিগুলি হ্রাস করার জন্য ডিজাইনের রিফ্যাক্টর করতে পারি?

  • এই প্রকল্পটি সি ++ ব্যবহার করে। আপনি এটি পছন্দ করুন বা ঘৃণা করুন, ভাষার জিনিসগুলি করার নিজস্ব নিজস্ব পদ্ধতি রয়েছে এবং ডিজাইনের লক্ষ্য এই ভাষাটির সাথে কাজ করা নয়, এর বিপরীতে নয়

  • শেষ অবধি , প্রকল্পটি ডাব্লুএক্সউজেডসের শীর্ষে নির্মিত । আমি আরও সাধারণ ক্ষেত্রে প্রযোজ্য সমাধানের সন্ধান করছি, এই নির্দিষ্ট প্রয়োগটি সেই সরঞ্জামকিটটির সাথে সুন্দরভাবে কাজ করা উচিত।

এরপরে সি ++ তে লিখিত ক্লাসগুলির একটি খুব সাধারণ সেট যা নকশাটি চিত্রিত করে। এগুলি আমি এখনও অবধি আংশিকভাবে লিখেছি এমন প্রকৃত ক্লাস নয়, এই কোডটি আমি যে নকশাটি ব্যবহার করছি তা কেবল চিত্রিত করে।


প্রথমে কিছু নমুনা ডিএও:

#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>

// One widget represents one record in the application.
class Widget {
public:
  using id_type = int;
private:
  id_type id;
};

// Container for widgets. Much more than a dumb container,
// it will also have indexes and other metadata. This represents
// one data file the user may open in the application.
class WidgetDatabase {
  ::std::map<Widget::id_type, ::std::shared_ptr<Widget>> widgets;
};

এরপরে, আমি ডিএও পড়তে এবং লেখার জন্য খাঁটি ভার্চুয়াল ক্লাস (ইন্টারফেস) সংজ্ঞায়িত করি। ধারণাটি হ'ল ডেটা ( এসআরপি ) থেকে ডেটা সিরিয়ালাইজেশন বিমূর্ত করা ।

class WidgetReader {
public:
  virtual Widget read(::std::istream &in) const abstract;
};

class WidgetWriter {
public:
  virtual void write(::std::ostream &out, const Widget &widget) const abstract;
};

class WidgetDatabaseReader {
public:
  virtual WidgetDatabase read(::std::istream &in) const abstract;
};

class WidgetDatabaseWriter {
public:
  virtual void write(::std::ostream &out, const WidgetDatabase &widgetDb) const abstract;
};

শেষ অবধি, এখানে কোডটি পছন্দসই I / O প্রকারের জন্য উপযুক্ত পাঠক / লেখক পায়। পাঠক / লেখকের উপশ্রেণীও সংজ্ঞায়িত হবে তবে এগুলি নকশা পর্যালোচনায় কোনও যোগ করে না:

enum class WidgetIoType {
  BINARY,
  JSON,
  XML
  // Other types TBD.
};

WidgetIoType forFilename(::std::string &name) { return ...; }

class WidgetIoFactory {
public:
  static ::std::unique_ptr<WidgetReader> getWidgetReader(const WidgetIoType &type) {
    return ::std::unique_ptr<WidgetReader>(/* TODO */);
  }

  static ::std::unique_ptr<WidgetWriter> getWidgetWriter(const WidgetIoType &type) {
    return ::std::unique_ptr<WidgetWriter>(/* TODO */);
  }

  static ::std::unique_ptr<WidgetDatabaseReader> getWidgetDatabaseReader(const WidgetIoType &type) {
    return ::std::unique_ptr<WidgetDatabaseReader>(/* TODO */);
  }

  static ::std::unique_ptr<WidgetDatabaseWriter> getWidgetDatabaseWriter(const WidgetIoType &type) {
    return ::std::unique_ptr<WidgetDatabaseWriter>(/* TODO */);
  }
};

আমার নকশার বর্ণিত লক্ষ্য অনুযায়ী, আমার একটি নির্দিষ্ট উদ্বেগ রয়েছে have সি ++ স্ট্রিমগুলি পাঠ্য বা বাইনারি মোডে খোলা যেতে পারে, তবে ইতিমধ্যে খোলার স্ট্রিমটি চেক করার কোনও উপায় নেই। কোনও এক্সএমএল বা জেএসএন পাঠক / লেখককে বাইনারি স্ট্রিম সরবরাহ করা প্রোগ্রামার ত্রুটির মাধ্যমে এটি সম্ভব হয়েছিল। এটি সূক্ষ্ম (বা এত সূক্ষ্ম নয়) ত্রুটিগুলির কারণ হতে পারে। আমি কোডটি দ্রুত ব্যর্থ হওয়ার পক্ষে পছন্দ করব, তবে আমি নিশ্চিত নই যে এই নকশাটি এটি করবে।

এর চারপাশের একটি উপায় হ'ল পাঠক বা লেখকের কাছে স্ট্রিমটি খোলার দায়িত্বটি অফলোড করা যেতে পারে তবে আমি বিশ্বাস করি যে এসআরপি লঙ্ঘন করে কোডটি আরও জটিল করে তুলবে। ডিএও লেখার সময় লেখকের স্ট্রিমটি কোথায় চলছে সেদিকে খেয়াল করা উচিত নয়: এটি কোনও ফাইল, স্ট্যান্ডার্ড আউট, এইচটিটিপি প্রতিক্রিয়া, একটি সকেট, যে কোনও কিছু হতে পারে। সিরিয়ালাইজেশনের যুক্তিতে এই উদ্বেগটি একবারে সংশ্লেষিত হয়ে গেলে এটি আরও জটিল হয়ে ওঠে: নির্দিষ্ট ধরণের প্রবাহ এবং কোন নির্মাণকারীকে কল করতে হবে তা অবশ্যই তা জানতে হবে।

এই বিকল্পটি বাদ দিয়ে, আমি নিশ্চিত নই যে এই বিষয়গুলি সহজ, নমনীয় এবং মডেলগুলি ব্যবহার করে এমন কোডগুলিতে যুক্তি ত্রুটিগুলি রোধ করতে সাহায্য করার জন্য এর চেয়ে ভাল উপায় কী হতে পারে।


ব্যবহারের ক্ষেত্রে যার সমাধানটি একীভূত করতে হবে তা হল একটি সরল ফাইল নির্বাচন ডায়ালগ বাক্স । ব্যবহারকারী ফাইল মেনু থেকে "ওপেন ..." বা "হিসাবে সংরক্ষণ করুন ..." নির্বাচন করে এবং প্রোগ্রামটি উইজেটডাটাবেস খুলে বা সংরক্ষণ করে। স্বতন্ত্র উইজেটগুলির জন্য "আমদানি ..." এবং "রফতানি ..." বিকল্পগুলিও থাকবে।

ব্যবহারকারী খোলার বা সংরক্ষণের জন্য কোনও ফাইল নির্বাচন করেন, ডাব্লুএক্সউজেডস কোনও ফাইলের নাম ফিরিয়ে দেবে। এই ইভেন্টটির প্রতিক্রিয়া জানানো হ্যান্ডলারটি অবশ্যই সাধারণ উদ্দেশ্য কোড হতে হবে যা ফাইলের নাম নেয়, একটি সিরিয়ালাইজার অর্জন করে এবং ভারী উত্তোলন করার জন্য একটি ফাংশনকে কল করে। আদর্শভাবে এই নকশাটিও কাজ করবে যদি অন্য কোনও টুকরো কোডটি নন-ফাইল I / O সম্পাদন করে, যেমন সকেটের উপর দিয়ে কোনও মোবাইল ডিভাইসে উইজেটডাটাবেস পাঠানো।


একটি উইজেট তার নিজস্ব ফর্ম্যাট সংরক্ষণ করতে পারে? এটি বিদ্যমান ফর্ম্যাটগুলির সাথে হস্তক্ষেপ করতে পারে? হ্যাঁ! উপরের সবগুলো. ফাইল ডায়লগে ফিরে গিয়ে মাইক্রোসফ্ট ওয়ার্ড সম্পর্কে চিন্তা করুন। মাইক্রোসফ্ট তারা নির্দিষ্ট বাধা থাকা সত্ত্বেও ডওএক্সএক্স ফর্ম্যাটটি বিকাশ করতে মুক্ত ছিল। একই সাথে ওয়ার্ডটি উত্তরাধিকার এবং তৃতীয় পক্ষের ফর্ম্যাটগুলি (যেমন পিডিএফ) পড়ে বা লিখতে পারে। এই প্রোগ্রামটি আলাদা নয়: আমি যে "বাইনারি" ফর্ম্যাটটির কথা বলি তা গতির জন্য নকশাকৃত একটি এখনও নির্ধারিত অভ্যন্তরীণ বিন্যাস। একই সময়ে, এটির ডোমেনে (প্রশ্নের অপ্রাসঙ্গিক) ওপেন স্ট্যান্ডার্ড ফর্ম্যাটগুলি পড়তে এবং লিখতে সক্ষম হতে হবে যাতে এটি অন্যান্য সফ্টওয়্যার দিয়ে কাজ করতে সক্ষম হয়।

শেষ অবধি, উইজেটের এক প্রকার রয়েছে। এটিতে চাইল্ড অবজেক্ট থাকবে তবে সেগুলি এই সিরিয়ালাইজেশন যুক্তি দ্বারা পরিচালিত হবে। প্রোগ্রামটি কখনই উইজেট এবং স্প্রোকেট লোড করবে না । এই নকশাটি কেবল উইজেট এবং উইজেট ডেটাবেসগুলির সাথে সম্পর্কিত হতে হবে।


1
আপনি কি এর জন্য বুস্ট সিরিয়ালাইজেশন লাইব্রেরিটি ব্যবহার করার বিষয়টি বিবেচনা করেছেন ? এটি আপনার কাছে থাকা সমস্ত ডিজাইনের লক্ষ্য অন্তর্ভুক্ত করে।
বার্ট ভ্যান ইনজেন শেহেনো

1
@ বার্টওয়ানআইংজেনচেনাও আমার ছিল না, মূলত বোস্টের সাথে আমার যে ভালবাসা / ঘৃণার সম্পর্ক ছিল তার কারণেই। আমি মনে করি এই ক্ষেত্রে আমার সমর্থিত কিছু ফর্ম্যাটগুলি বুস্ট সিরিয়ালাইজেশনের চেয়ে বেশি জটিল হতে পারে যাতে যথেষ্ট জটিলতা যোগ না করে হ্যান্ডেল করতে পারে যে এটি ব্যবহার করে আমার বেশি কেনা হয় না।

আহ! সুতরাং আপনি (ডি-) উইজেট উদাহরণগুলি সিরিয়ালাইজ করছেন না (এটি অদ্ভুত হবে ...), তবে এই উইজেটগুলিকে কেবল কাঠামোগত ডেটা পড়তে এবং লিখতে হবে? আপনার কি বিদ্যমান ফাইল ফর্ম্যাটগুলি প্রয়োগ করতে হবে, বা আপনি কোনও অ্যাড-হক বিন্যাস নির্ধারণ করতে মুক্ত? বিভিন্ন উইজেটগুলি কি সাধারণ বা অনুরূপ ফর্ম্যাটগুলি ব্যবহার করে যা সাধারণ মডেল হিসাবে প্রয়োগ করা যেতে পারে? তারপরে আপনি WxWidget idশ্বরের অবজেক্ট হিসাবে সবকিছু একসাথে munging না করে আপনি একটি ইউজার ইন্টারফেস – ডোমেন লজিক – মডেল – ডাল বিভক্ত করতে পারেন। আসলে, উইজেটগুলি এখানে প্রাসঙ্গিক বলে আমি দেখতে পাচ্ছি না।
আমন

@amon আমি আবার প্রশ্নটি সম্পাদনা করেছি। ডাব্লুএক্স উইজেটগুলি কেবলমাত্র ব্যবহারকারীর সাথে ইন্টারফেসের মতোই প্রাসঙ্গিক: আমি যে উইজেটগুলির সাথে কথা বলি তার সাথে ডাব্লুএক্সউজেটস কাঠামো (যেমন কোনও দেবতা অবজেক্ট) নেই। আমি কেবল এই শব্দটি এক ধরণের ডিএওর জেনেরিক নাম হিসাবে ব্যবহার করি।

1
@ লার্স ভিক্লুন্ড আপনি একটি জোরালো যুক্তি দিয়েছেন এবং আপনি এই বিষয়ে আমার মতামত পরিবর্তন করেছেন। আমি উদাহরণ কোড আপডেট করেছি।

উত্তর:


7

আমি ভুল হতে পারি, তবে আপনার নকশাটি ভয়াবহভাবে ওভাররেঞ্জাইনার্ড বলে মনে হচ্ছে। মাত্র এক ধারাবাহিকভাবে করতে Widget, আপনি সংজ্ঞায়িত করতে চান WidgetReader, WidgetWriter, WidgetDatabaseReader, WidgetDatabaseWriterইন্টারফেসগুলি যা প্রতিটি এক্সএমএল, তাদেরকে JSON এবং বাইনারি এনকোডিং, এবং একটি কারখানা সমস্ত শ্রেণীর একসঙ্গে গিঁট জন্য বাস্তবায়নের আছে। এটি নিম্নলিখিত কারণে সমস্যাযুক্ত:

  • আমি একটি অ ধারাবাহিকভাবে করতে চান, Widgetবর্গ, এটা কল দিন Fooআমি ক্লাস এই পুরো চিড়িয়াখানা reimplement করতে হবে, এবং তৈরি করুন FooReader, FooWriter, FooDatabaseReader, FooDatabaseWriterইন্টারফেস, বার প্রতিটি ধারাবাহিকতাতে বিন্যাস, প্লাস একটি কারখানা তিন এটা এমনকি দূরবর্তী অবস্থান থেকে ব্যবহারযোগ্য না। আমাকে বলবেন না যে সেখানে কোনও অনুলিপি এবং পেস্ট চলবে না! এই সংযুক্তি বিস্ফোরণটি মোটামুটি অনির্বচনীয় বলে মনে হচ্ছে, এমনকি যদি এই শ্রেণীর প্রতিটিটিতে কেবলমাত্র একটি পদ্ধতি থাকে।

  • Widgetযুক্তিসঙ্গতভাবে আবদ্ধ করা যাবে না। হয় আপনি গেটর পদ্ধতির সাহায্যে উন্মুক্ত জগতে ক্রমিক হওয়া উচিত এমন সমস্ত কিছু খুলুন বা আপনার friendপ্রতিটি WidgetWriter(এবং সম্ভবত সমস্ত WidgetReader) বাস্তবায়ন করতে হবে । উভয় ক্ষেত্রেই, আপনি সিরিয়ালাইজেশন বাস্তবায়ন এবং এর মধ্যে যথেষ্ট সংযোগ স্থাপন করবেন Widget

  • পাঠক / লেখক চিড়িয়াখানাটি অসঙ্গতিগুলিকে আমন্ত্রণ জানায়। আপনি যখনই কোনও সদস্যকে যুক্ত করবেন Widget, সেই সদস্যটিকে সঞ্চয় / পুনরুদ্ধার করতে আপনাকে সম্পর্কিত সমস্ত ক্রমিক ক্রিয়াকলাপ আপডেট করতে হবে। এটি এমন একটি বিষয় যা স্থিরভাবে সঠিকতার জন্য যাচাই করা যায় না, সুতরাং আপনাকে প্রতিটি পাঠক এবং লেখকের জন্য পৃথক পরীক্ষাও লিখতে হবে। আপনার বর্তমান ডিজাইনে, আপনি ক্রমিক করতে চান এমন প্রতি ক্লাসে 4 * 3 = 12 টি পরীক্ষা।

    অন্য দিকে, YAML এর মতো একটি নতুন সিরিয়ালাইজেশন ফর্ম্যাট যুক্ত করাও সমস্যাযুক্ত। আপনি ক্রমিক করতে চান এমন প্রতিটি শ্রেণীর জন্য আপনাকে একটি ওয়াইএএমএল পাঠক এবং লেখক যুক্ত করতে হবে এবং সেই কেসটি এনাম এবং কারখানায় যুক্ত করতে হবে। আবার, এটি এমন কিছু যা স্থিতিশীলভাবে পরীক্ষা করা যায় না, যদি না আপনি (খুব) চালাক হন এবং কারখানার জন্য স্বতন্ত্র Widgetযে কোনও স্বতন্ত্র ইন্টারফেস আঁকেন এবং নিশ্চিত করেন না যে প্রতিটি ক্রিয়াকলাপের জন্য প্রতিটি ক্ষেত্রে / আউট অপারেশনের জন্য একটি প্রয়োগকরণ সরবরাহ করা হয়েছে।

  • হয়তো Widgetএখন সন্তুষ্ট SRP এটা ধারাবাহিকতাতে জন্য দায়ী নয় যেহেতু। তবে পাঠক এবং লেখকের বাস্তবায়ন স্পষ্টভাবে নয়, "এসআরপি = প্রতিটি বস্তুর পরিবর্তনের একটি কারণ রয়েছে" ব্যাখ্যার সাথে: ক্রমিকায়িতকরণের ফর্ম্যাট পরিবর্তিত হলে বা পরিবর্তনগুলি যখন বাস্তবায়িত হয় তখন অবশ্যই পরিবর্তন করতে হবে Widget

আপনি যদি আগে ন্যূনতম সময় বিনিয়োগ করতে সক্ষম হন তবে অনুগ্রহ করে ক্লাসের এই অ্যাড-হক জঞ্জালের চেয়ে আরও জেনেরিক সিরিয়ালাইজেশন কাঠামো আঁকতে চেষ্টা করুন। উদাহরণস্বরূপ, আপনি একটি সাধারণ ইন্টারচেঞ্জ উপস্থাপনা সংজ্ঞায়িত করতে পারেন, আসুন একে SerializationInfoজাভাস্ক্রিপ্ট-এর মতো বস্তুর মডেল সহ কল করুন : বেশিরভাগ বস্তুগুলিকে একটি std::map<std::string, SerializationInfo>, বা হিসাবে std::vector<SerializationInfo>বা কোনও আদিম হিসাবে দেখা যায় int

প্রতিটি সিরিয়ালাইজেশন ফর্ম্যাটের জন্য, আপনার তখন একটি ক্লাস থাকবে যা সেই স্ট্রিমের ক্রমিক উপস্থাপনাটি পড়তে এবং লিখতে পরিচালিত করে। এবং প্রতিটি শ্রেণীর জন্য যা আপনি সিরিয়ালাইজ করতে চান, আপনার কাছে এমন কিছু ব্যবস্থা আছে যা উদাহরণগুলি থেকে / সিরিয়ালের উপস্থাপনায় রূপান্তর করে।

আমি সিএক্সএক্সটিউলস ( হোমপেজ , গিটহাব , সিরিয়ালাইজেশন ডেমো ) এর সাথে এমন নকশার অভিজ্ঞতা পেয়েছি এবং এটি বেশিরভাগ ক্ষেত্রেই স্বজ্ঞাত, বিস্তৃতভাবে প্রযোজ্য এবং আমার ব্যবহারের ক্ষেত্রে সন্তোষজনক - কেবলমাত্র সিরিয়ালাইজেশন প্রতিনিধিত্বের মোটামুটি দুর্বল অবজেক্ট মডেল যা আপনার প্রয়োজন problems deserialization সময় আপনি সঠিকভাবে কোন ধরণের প্রত্যাশা করছেন তা সুনির্দিষ্টভাবে জানতে এবং সেই deserialization ডিফল্ট-গঠনমূলক অবজেক্টগুলি বোঝায় যা পরে আরম্ভ করা যেতে পারে। এখানে একটি স্বীকৃত ব্যবহারের উদাহরণ:

class Point {
  int _x;
  int _y;
public:
  Point(x, y) : _x(x), _y(y) {}
  int x() const { return _x; }
  int y() const { return _y; }
};

void operator <<= (SerializationInfo& si, const Point& p) {
  si.addMember("x") <<= p.x();
  si.addMember("y") <<= p.y();
}

void operator >>= (const SerializationInfo& si, Point& p) {
  int x;
  si.getMember("x") >>= x;  // will throw if x entry not found
  int y;
  si.getMember("y") >>= y;
  p = Point(x, y);
}

int main() {
  // cxxtools::Json<T>(T&) wrapper sets up a SerializationInfo and manages Json I/O
  // wrappers for other formats also exist, e.g. cxxtools::Xml<T>(T&)

  Point a(42, -15);
  std::cout << cxxtools::Json(a);
  ...
  Point b(0, 0);
  std::cin >> cxxtools::Json(p);
}

আমি বলছি না যে আপনি সিএক্সএক্সটুলগুলি ব্যবহার করুন বা সেই নকশাটি হুবহু অনুলিপি করা উচিত, তবে আমার অভিজ্ঞতার সাথে এর নকশাটি ছোট, এক-অফ ক্লাসের জন্য এমনকি সিরিয়ালিয়করণ যুক্ত করা তুচ্ছ করে তোলে তবে শর্ত থাকে যে আপনি সিরিয়ালাইজেশন ফর্ম্যাটটি সম্পর্কে খুব ঘনিষ্ঠভাবে যত্ন না রাখেন ( উদাহরণস্বরূপ, ডিফল্ট এক্সএমএল আউটপুট সদস্যের নাম উপাদান নাম হিসাবে ব্যবহার করবে এবং আপনার ডেটার জন্য বৈশিষ্ট্যগুলি কখনই ব্যবহার করবে না)।

স্ট্রিমগুলির জন্য বাইনারি / পাঠ্য মোডের সমস্যাটি সমাধানযোগ্য বলে মনে হয় না, তবে এটি এতটা খারাপ নয়। একটি জিনিসের জন্য, এটি কেবল বাইনারি ফর্ম্যাটগুলির জন্য গুরুত্বপূর্ণ, প্ল্যাটফর্মে আমি ;-) এর জন্য প্রোগ্রাম করার প্রবণতা রাখি না আরও গুরুতরভাবে, এটি আপনার সিরিয়ালাইজেশন অবকাঠামোর একটি বিধিনিষেধ আপনাকে কেবল ডকুমেন্ট করতে হবে এবং আশা করি প্রত্যেকে সঠিকভাবে ব্যবহার করবে। আপনার পাঠক বা লেখকদের মধ্যে স্ট্রিমগুলি খোলার উপায়টি খুব জটিল নয় এবং বাইনারি ডেটা থেকে পাঠ্যকে আলাদা করার জন্য সি ++ তে কোনও বিল্ট-ইন টাইপ-লেভেল প্রক্রিয়া নেই।


আপনার পরামর্শ কীভাবে পরিবর্তিত হবে যে এই ডিএওগুলি মূলত ইতিমধ্যে একটি "সিরিয়ালাইজেশন তথ্য" শ্রেণি রয়েছে? এগুলি POJOs এর সি ++ সমতুল্য । এই বিষয়গুলি কীভাবে ব্যবহৃত হবে সে সম্পর্কে আমি আরও কিছু তথ্য দিয়ে আমার প্রশ্নটিও সম্পাদনা করতে যাচ্ছি।
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.