निर्भरता इंजेक्शन के लिए पाठक मोनाद: कई निर्भरता, नेस्टेड कॉल


87

स्काला में डिपेंडेंसी इंजेक्शन के बारे में पूछे जाने पर, रीडर मोनाड का उपयोग करने के लिए बहुत सारे उत्तर इंगित करते हैं, या तो स्कैलाज़ से या सिर्फ अपना खुद का रोल करना। दृष्टिकोण की मूल बातें (जैसे रनर की बात) का वर्णन करने वाले बहुत स्पष्ट लेख हैं , जेसन का ब्लॉग ) का , लेकिन मैंने अधिक पूर्ण उदाहरण खोजने का प्रबंधन नहीं किया, और मैं उस दृष्टिकोण के फायदों को देखने में विफल रहा जैसे उदा। पारंपरिक "मैनुअल" DI ( मैंने लिखा गाइड देखें )। सबसे शायद मुझे कुछ महत्वपूर्ण बिंदु याद आ रहे हैं, इसलिए सवाल।

एक उदाहरण के रूप में, आइए कल्पना करें कि हमारे पास ये वर्ग हैं:

trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }

class FindUsers(datastore: Datastore) {
  def inactive(): Unit = ()
}

class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
  def emailInactive(): Unit = ()
}

class CustomerRelations(userReminder: UserReminder) {
  def retainUsers(): Unit = {}
}

यहां मैं कक्षाओं और कंस्ट्रक्टर मापदंडों का उपयोग करके चीजों को मॉडलिंग कर रहा हूं, जो "पारंपरिक" डीआई दृष्टिकोण के साथ बहुत अच्छी तरह से खेलता है, हालांकि इस डिजाइन में कुछ और अच्छे हैं:

  • प्रत्येक कार्यक्षमता में स्पष्ट रूप से निर्भरताएँ हैं। हम यह मानते हैं कि कार्यक्षमता को ठीक से काम करने के लिए वास्तव में निर्भरता की आवश्यकता होती है
  • निर्भरता कार्यात्मकताओं में छिपी हुई है, उदाहरण के लिए UserReminderकोई विचार नहीं हैFindUsers डेटासटोर की आवश्यकता है। कार्यात्मकताएं अलग-अलग संकलित इकाइयों में भी हो सकती हैं
  • हम केवल शुद्ध स्काला का उपयोग कर रहे हैं; कार्यान्वयन अपरिवर्तनीय वर्गों, उच्च-क्रम के कार्यों का लाभ उठा सकते हैं, "व्यापार तर्क" विधियां IOमानद में लिपटे मूल्यों को वापस कर सकती हैं यदि हम प्रभाव आदि पर कब्जा करना चाहते हैं।

इसे रीडर मोनड के साथ कैसे जोड़ा जा सकता है? उपरोक्त विशेषताओं को बनाए रखना अच्छा होगा, ताकि यह स्पष्ट हो सके कि प्रत्येक कार्यक्षमता को किस प्रकार की निर्भरता चाहिए, और एक कार्यक्षमता की निर्भरता को दूसरे से छिपाएं। ध्यान दें कि classes का उपयोग कार्यान्वयन विस्तार से अधिक है; शायद पाठक का उपयोग करके "सही" समाधान कुछ और होगा।

मुझे कुछ हद तक संबंधित प्रश्न मिला जो या तो सुझाव देता है:

  • सभी निर्भरताओं के साथ एक एकल पर्यावरण वस्तु का उपयोग करना
  • स्थानीय वातावरण का उपयोग करना
  • "पैराफिट" पैटर्न
  • प्रकार-अनुक्रमित नक्शे

हालाँकि, इसके अलावा (लेकिन यह व्यक्तिपरक है) इतनी सरल चीज़ के लिए थोड़ा जटिल है, इन सभी समाधानों में उदाहरण के लिए retainUsersविधि (जो कॉल करता है emailInactive, जो inactiveनिष्क्रिय उपयोगकर्ताओं को खोजने के लिए कॉल करता है) के बारे में जानना होगाDatastore निर्भरता के , नेस्टेड फ़ंक्शन को ठीक से कॉल करने में सक्षम हो - या क्या मैं गलत हूं?

इस तरह के "व्यावसायिक अनुप्रयोग" के लिए पाठक मोनाद का उपयोग करने वाले पहलुओं में केवल निर्माण मापदंडों का उपयोग करने से बेहतर क्या होगा?


1
पाठक मोनद चाँदी की गोली नहीं है। मुझे लगता है, अगर आपको बहुत सारे स्तरों पर निर्भरता की आवश्यकता है, तो आपका डिज़ाइन बहुत अच्छा है।
जेकेकाज़लोव

हालांकि इसे अक्सर डिपेंडेंसी इंजेक्शन के विकल्प के रूप में वर्णित किया जाता है; शायद यह एक पूरक के रूप में वर्णित किया जाना चाहिए? मुझे कभी-कभी यह एहसास होता है कि DI को "सच्चे कार्यात्मक प्रोग्रामर" द्वारा खारिज कर दिया गया है, इसलिए मैं सोच रहा था कि "क्या इसके बजाय" :) किसी भी तरह, मुझे लगता है कि निर्भरता के कई स्तर हैं, या कई बाहरी सेवाएं हैं जिनसे आपको बात करने की आवश्यकता है कि कैसे हर मध्यम-बड़े "व्यावसायिक अनुप्रयोग" की तरह दिखता है (निश्चित रूप से पुस्तकालयों के लिए मामला नहीं)
adamw

2
मुझे हमेशा पाठक मोनाड के बारे में कुछ स्थानीय के रूप में सोचा गया है। उदाहरण के लिए, यदि आपके पास कुछ मॉड्यूल है जो केवल एक DB से बात करता है, तो आप इस मॉड्यूल को रीडर मोनड शैली में लागू कर सकते हैं। हालाँकि, यदि आपके एप्लिकेशन को कई विभिन्न डेटा स्रोतों की आवश्यकता होती है, जिन्हें एक साथ जोड़ा जाना चाहिए, तो मुझे नहीं लगता कि रीडर मोनाड इसके लिए अच्छा है।
झक्काझोजोव

आह, यह एक अच्छा दिशानिर्देश हो सकता है कि दो अवधारणाओं को कैसे संयोजित किया जाए। और फिर वास्तव में ऐसा लगेगा कि DI और RM एक दूसरे के पूरक हैं। मुझे लगता है कि यह वास्तव में ऐसे कार्यों के लिए काफी सामान्य है जो केवल एक निर्भरता पर काम करते हैं, और यहां आरएम का उपयोग करके निर्भरता / डेटा सीमाओं को स्पष्ट करने में मदद मिलेगी।
adamw

जवाबों:


36

इस उदाहरण को कैसे मॉडल करें

इसे रीडर मोनड के साथ कैसे जोड़ा जा सकता है?

मुझे यकीन नहीं है कि यह पाठक के साथ मॉडलिंग की जानी चाहिए, फिर भी यह हो सकता है:

  1. फ़ंक्शंस के रूप में कक्षाओं को एन्कोडिंग करना जो कोड प्ले रीडर के साथ अच्छे बनाता है
  2. रीडर के साथ कार्यों को समझने और उसका उपयोग करने के लिए

शुरुआत से ठीक पहले मुझे आपको छोटे नमूना कोड समायोजन के बारे में बताने की ज़रूरत है जो मुझे इस उत्तर के लिए फायदेमंद लगे। पहला बदलाव FindUsers.inactiveविधि के बारे में है। मैंने इसे वापस List[String]करने दिया ताकि पतों की सूची UserReminder.emailInactiveविधि में उपयोग की जा सके । मैंने विधियों में सरल कार्यान्वयन भी जोड़ा है। अंत में, नमूना रीडर मोनाड के निम्नलिखित हैंड-रोल्ड संस्करण का उपयोग करेगा:

case class Reader[Conf, T](read: Conf => T) { self =>

  def map[U](convert: T => U): Reader[Conf, U] =
    Reader(self.read andThen convert)

  def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
    Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))

  def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
    Reader[BiggerConf, T](extractFrom andThen self.read)
}

object Reader {
  def pure[C, A](a: A): Reader[C, A] =
    Reader(_ => a)

  implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
    Reader(read)
}

मॉडलिंग चरण 1. कार्यों के रूप में कक्षाओं को एन्कोडिंग

हो सकता है कि यह वैकल्पिक हो, मुझे यकीन नहीं है, लेकिन बाद में यह समझ के लिए बेहतर बनाता है। ध्यान दें, कि परिणामी फ़ंक्शन करी है। यह पूर्व निर्माता तर्क (ओं) को उनके पहले पैरामीटर (पैरामीटर सूची) के रूप में भी लेता है। उस तरफ

class Foo(dep: Dep) {
  def bar(arg: Arg): Res = ???
}
// usage: val result = new Foo(dependency).bar(arg)

हो जाता है

object Foo {
  def bar: Dep => Arg => Res = ???
}
// usage: val result = Foo.bar(dependency)(arg)

ध्यान रखें की प्रत्येक कि Dep, Arg,Res प्रकार पूरी तरह से मनमाने ढंग से किया जा सकता है: एक टपल, एक समारोह या एक सरल प्रकार।

प्रारंभिक समायोजन के बाद यहां का नमूना कोड, कार्यों में तब्दील हो गया है:

trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }

object FindUsers {
  def inactive: Datastore => () => List[String] =
    dataStore => () => dataStore.runQuery("select inactive")
}

object UserReminder {
  def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
    emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}

object CustomerRelations {
  def retainUsers(emailInactive: () => Unit): () => Unit =
    () => {
      println("emailing inactive users")
      emailInactive()
    }
}

यहां एक बात गौर करने वाली है कि विशेष कार्य पूरी वस्तुओं पर निर्भर नहीं करते हैं, बल्कि केवल सीधे इस्तेमाल किए जाने वाले भागों पर निर्भर करते हैं। कहां OOP संस्करण में UserReminder.emailInactive()उदाहरण userFinder.inactive()यहाँ कॉल यह सिर्फ कॉल करेगा inactive() - एक कार्य पहले पैरामीटर में इसे पारित कर दिया।

कृपया ध्यान दें, कि कोड प्रश्न से तीन वांछनीय गुणों को प्रदर्शित करता है:

  1. यह स्पष्ट है कि प्रत्येक कार्यक्षमता को किस प्रकार की निर्भरता चाहिए
  2. एक कार्यक्षमता की निर्भरता दूसरे से छुपाता है
  3. retainUsers डेटाटस्टोर निर्भरता के बारे में जानने के लिए विधि की आवश्यकता नहीं होनी चाहिए

स्टेपिंग स्टेप 2. रीडर्स का उपयोग करके फंक्शन्स को कंपोज़ करना और उन्हें रन करना

रीडर मोनाड आपको केवल उन कार्यों की रचना करने देता है जो सभी एक ही प्रकार पर निर्भर करते हैं। यह अक्सर एक मामला नहीं है। हमारे उदाहरण FindUsers.inactiveमें Datastoreऔर UserReminder.emailInactiveपर निर्भर करता हैEmailServer । उस समस्या को हल करने के लिए एक नया प्रकार (जिसे अक्सर कॉन्फिगरेशन कहा जाता है) को प्रस्तुत किया जा सकता है, जिसमें सभी निर्भरताएँ होती हैं, फिर फ़ंक्शंस को बदलते हैं ताकि वे सभी इस पर निर्भर रहें और केवल इससे संबंधित डेटा ले सकें। यह स्पष्ट रूप से निर्भरता प्रबंधन के दृष्टिकोण से गलत है क्योंकि इस तरह से आप इन कार्यों को उन प्रकारों पर भी निर्भर करते हैं जिनके बारे में उन्हें पहले से पता नहीं होना चाहिए।

सौभाग्य से यह पता चला है, कि फ़ंक्शन को काम करने का एक तरीका मौजूद है Configभले ही यह एक पैरामीटर के रूप में इसके कुछ हिस्से को स्वीकार करता है। यह एक विधि है जिसे localरीडर में परिभाषित किया गया है। इसे संबंधित भाग को निकालने के लिए एक तरीका प्रदान करना होगा Config

उदाहरण के लिए हाथ पर लागू यह ज्ञान इस तरह दिखेगा:

object Main extends App {

  case class Config(dataStore: Datastore, emailServer: EmailServer)

  val config = Config(
    new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
    new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
  )

  import Reader._

  val reader = for {
    getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
    emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
    retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
  } yield retainUsers

  reader.read(config)()

}

कंस्ट्रक्टर मापदंडों का उपयोग करने पर लाभ

इस तरह के "व्यावसायिक अनुप्रयोग" के लिए पाठक मोनाद का उपयोग करने वाले पहलुओं में केवल निर्माण मापदंडों का उपयोग करने से बेहतर क्या होगा?

मुझे उम्मीद है कि इस उत्तर को तैयार करके मैंने अपने लिए खुद को आंकना आसान बना दिया कि किन पहलुओं में यह सादे रचनाकारों को हरा देगा। फिर भी अगर मैं इनकी गणना करता, तो यहां मेरी सूची है। डिस्क्लेमर: मेरे पास OOP बैकग्राउंड है और मैं रीडर और क्लेस्ली की पूरी तरह से सराहना नहीं कर सकता क्योंकि मैं उनका इस्तेमाल नहीं करता।

  1. एकरूपता - कोई भी मैटर, संक्षिप्तता के लिए कितना छोटा / लंबा है, यह सिर्फ एक पाठक है और आप इसे आसानी से किसी अन्य उदाहरण के साथ रचना कर सकते हैं, शायद केवल एक और कॉन्फिगर प्रकार का परिचय दे रहे हैं और localइसके ऊपर कुछ कॉल छिड़क रहे हैं। यह बिंदु IMO है, बल्कि स्वाद का विषय है, क्योंकि जब आप कंस्ट्रक्टरों का उपयोग करते हैं, तो कोई भी आपको अपनी पसंद की चीज़ों की रचना करने से रोकता है, जब तक कि कोई व्यक्ति कुछ बेवकूफी न करे, जैसे कि कंस्ट्रक्टर में काम करना जो ओओपी में एक बुरा अभ्यास माना जाता है।
  2. रीडर, एक इकाई है, इसलिए यह है कि से संबंधित सभी लाभ हो जाता है - sequence, traverseतरीकों मुक्त करने के लिए लागू किया।
  3. कुछ मामलों में आपको पाठक को केवल एक बार निर्माण करना और इसे विस्तृत रूप से कन्फ़िगर्स के लिए उपयोग करना बेहतर लग सकता है। कंस्ट्रक्टर्स के साथ कोई भी आपको ऐसा करने से रोकता है, आपको बस आने वाले हर कॉन्फिग के लिए पूरी ऑब्जेक्ट ग्राफ नए सिरे से बनाने की जरूरत है। जबकि मुझे इससे कोई समस्या नहीं है (मैं आवेदन करने के लिए हर अनुरोध पर भी ऐसा करना पसंद करता हूं), यह कई लोगों के लिए एक स्पष्ट विचार नहीं है कि जिन कारणों से मैं केवल अटकलें लगा सकता हूं।
  4. रीडर आपको फ़ंक्शंस का अधिक उपयोग करने की ओर धकेलता है, जो मुख्यतः FP शैली में लिखे गए एप्लिकेशन के साथ बेहतर तरीके से निभाएगा।
  5. पाठक चिंताओं को अलग करता है; आप सब कुछ के साथ बातचीत कर सकते हैं, निर्भरता प्रदान किए बिना तर्क को परिभाषित कर सकते हैं। दरअसल बाद में, अलग से आपूर्ति। (इस बिंदु के लिए धन्यवाद केन स्क्रैम्बलर)। यह अक्सर पाठक का लाभ सुना जाता है, फिर भी सादे निर्माणकर्ताओं के साथ भी संभव है।

मैं यह भी बताना चाहूंगा कि मुझे रीडर में क्या पसंद नहीं है।

  1. विपणन। कभी-कभी मुझे आभास हो जाता है, कि रीडर को सभी तरह की निर्भरता के लिए विपणन किया जाता है, बिना भेद के अगर वह सत्र कुकी या डेटाबेस है। मेरे लिए व्यावहारिक रूप से निरंतर वस्तुओं के लिए रीडर का उपयोग करने में बहुत कम समझ है, जैसे ईमेल सर्वर या इस उदाहरण से रिपॉजिटरी। ऐसी निर्भरता के लिए मुझे सादे निर्माणकर्ता और / या आंशिक रूप से लागू किए गए फ़ंक्शन बेहतर तरीके से मिलते हैं। अनिवार्य रूप से रीडर आपको लचीलापन देता है ताकि आप हर कॉल पर अपनी निर्भरता को निर्दिष्ट कर सकें, लेकिन अगर आपको वास्तव में इसकी आवश्यकता नहीं है, तो आप केवल इसके कर का भुगतान करते हैं।
  2. अस्पष्ट भारीपन - पाठक को बिना प्रभाव के उपयोग करने से उदाहरण को पढ़ने में मुश्किल होगी। दूसरी ओर, जब आप नपुंसकता का उपयोग करके शोर वाले हिस्सों को छिपाते हैं और कुछ त्रुटि करते हैं, तो संकलक कभी-कभी आपको संदेशों को समझने में मुश्किल देगा।
  3. के साथ समारोह pure, localऔर खुद के विन्यास कक्षाएं बनाने / उस के लिए tuples का उपयोग कर। रीडर आपको कुछ कोड जोड़ने के लिए मजबूर करता है जो समस्या डोमेन के बारे में नहीं है, इसलिए कोड में कुछ शोर का परिचय दे रहा है। दूसरी ओर, एक एप्लिकेशन जो कंस्ट्रक्टर का उपयोग करता है, अक्सर फ़ैक्टरी पैटर्न का उपयोग करता है, जो समस्या डोमेन के बाहर से भी है, इसलिए यह कमजोरी इतनी गंभीर नहीं है।

यदि मैं अपनी कक्षाओं को कार्यों के साथ वस्तुओं में बदलना नहीं चाहता तो क्या होगा?

तुम्हें चाहिए। आप तकनीकी रूप से इससे बच सकते हैं , लेकिन अगर मैं FindUsersकक्षा को ऑब्जेक्ट में परिवर्तित नहीं करता तो क्या होगा । समझ के लिए संबंधित लाइन की तरह दिखेगा:

getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)

जो पठनीय नहीं है, वह क्या है? मुद्दा यह है कि रीडर फ़ंक्शंस पर काम करता है, इसलिए यदि आपके पास पहले से नहीं है, तो आपको उन्हें इनलाइन बनाने की आवश्यकता है, जो अक्सर उतना सुंदर नहीं होता है।


विस्तृत जवाब के लिए :) एक बात है कि मेरे पास स्पष्ट नहीं है, यही वजह है कि धन्यवाद Datastoreऔर EmailServerलक्षण के रूप में छोड़ दिया जाता है, और दूसरों को हो गया objectरों? क्या इन सेवाओं / निर्भरताओं / (हालांकि आप उन्हें कॉल करते हैं) में एक बुनियादी अंतर है जिसके कारण उन्हें अलग तरह से व्यवहार किया जाता है?
adamw

अच्छी तरह से ... मैं उदाहरण के EmailSenderलिए एक वस्तु के रूप में अच्छी तरह से परिवर्तित नहीं कर सकते , है ना? मैं तब टाइप किए बिना निर्भरता व्यक्त करने में सक्षम नहीं होगा ...
adamw

आह, निर्भरता तब एक उपयुक्त प्रकार के साथ एक फ़ंक्शन का रूप लेगी - इसलिए टाइप नामों का उपयोग करने के बजाय, सब कुछ फ़ंक्शन हस्ताक्षर (केवल आकस्मिक होने वाला नाम) में जाना होगा। हो सकता है, लेकिन मैं आश्वस्त हूँ नहीं;)
adamw

सही बात। इसके बजाय EmailSenderआप पर निर्भर करेगा (String, String) => Unit। यह सुनिश्चित करना कि कोई और मुद्दा है या नहीं :) निश्चित रूप से, यह कम से कम सामान्य है, क्योंकि हर कोई पहले से ही निर्भर है Function2
प्रेज़ेमेक पोक्रीवका

वैसे आप निश्चित रूप से नाम देना चाहते हैं, (String, String) => Unitताकि यह कुछ अर्थ
बताए

3

मुझे लगता है कि मुख्य अंतर यह है कि आपके उदाहरण में आप सभी निर्भरता को इंजेक्ट कर रहे हैं जब ऑब्जेक्ट्स को तत्काल किया जाता है। रीडर मोनाड मूल रूप से निर्भरता को देखते हुए अधिक से अधिक जटिल कार्यों का निर्माण करता है, जो तब उच्चतम परतों में वापस आ जाते हैं। इस मामले में, इंजेक्शन तब होता है जब फ़ंक्शन अंततः कहा जाता है।

एक तत्काल लाभ लचीलापन है, खासकर यदि आप एक बार अपने मोनाड का निर्माण कर सकते हैं और फिर इसे अलग-अलग इंजेक्शन निर्भरता के साथ उपयोग करना चाहते हैं। एक नुकसान यह है, जैसा कि आप कहते हैं, संभावित रूप से कम स्पष्टता। दोनों मामलों में, मध्यवर्ती परत को केवल उनकी तात्कालिक निर्भरता के बारे में जानने की आवश्यकता होती है, इसलिए वे दोनों डीआई के लिए विज्ञापन के रूप में काम करते हैं।


मध्यवर्ती परत को केवल उनके मध्यवर्ती निर्भरता के बारे में कैसे पता चलेगा, और उन सभी को नहीं? क्या आप यह दिखाते हुए एक कोड उदाहरण दे सकते हैं कि पाठक मोनड का उपयोग करके कैसे उदाहरण को लागू किया जा सकता है?
adamw

मैं शायद इसे Json के ब्लॉग से बेहतर नहीं समझा सकता (जिसे आपने पोस्ट किया है) फ़ॉर्म को उद्धृत करने के लिए "निहितार्थ उदाहरण के विपरीत, हमारे पास उपयोगकर्ता नाम और userInfo के हस्ताक्षर में कहीं भी UserRepository नहीं है"। उस उदाहरण को ध्यान से देखें।
डेनियल लैंगडन

1
हां, लेकिन यह मानता है कि पाठक जिस मोनड का उपयोग कर रहे हैं, वह पैराट्राइज़्ड है Configजिसके साथ एक संदर्भ है UserRepository। इतना सच है, यह सीधे हस्ताक्षर में दिखाई नहीं दे रहा है, लेकिन मैं कहूंगा कि यह और भी बुरा है, आपको वास्तव में कोई पता नहीं है कि आपका कोड पहली नज़र में किस निर्भरता का उपयोग कर रहा है। Configसभी निर्भरताओं के साथ निर्भर नहीं होने का मतलब यह है कि प्रत्येक विधि उन सभी पर निर्भर करती है?
adamw

यह उन पर निर्भर करता है, लेकिन इसे जानने की जरूरत नहीं है। कक्षाओं के साथ आपके उदाहरण में भी ऐसा ही है। मैं उन्हें काफी समकक्ष :-) के रूप में देखता हूं
डैनियल लैंगडन

वर्गों के साथ उदाहरण में आप केवल इस बात पर निर्भर करते हैं कि आपको वास्तव में क्या चाहिए, न कि सभी निर्भरताओं के साथ एक वैश्विक वस्तु। और आपको एक समस्या मिलती है कि कैसे तय किया जाए कि वैश्विक की "निर्भरता" के अंदर क्या है config, और "बस एक फ़ंक्शन" क्या है। संभवत: आप बहुत अधिक आत्म निर्भरता के साथ ही समाप्त हो जाएंगे। वैसे भी, यह Q & A :) की तुलना में अधिक बात करने वाली प्राथमिकता है
adamw
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.