एक वर्ग को अपने उपयोगकर्ताओं से कैसे संवाद करना चाहिए जो विधियों को लागू करते हैं?


12

परिदृश्य

एक वेब एप्लिकेशन IUserBackendविधियों के साथ एक उपयोगकर्ता बैकएंड इंटरफ़ेस को परिभाषित करता है

  • getUser (यूआईडी)
  • CreateUser (यूआईडी)
  • deleteUser (यूआईडी)
  • सेटपासवर्ड (यूआईडी, पासवर्ड)
  • ...

अलग-अलग उपयोगकर्ता बैकएंड (जैसे LDAP, SQL, ...) इस इंटरफ़ेस को लागू करते हैं लेकिन हर बैकएंड सब कुछ नहीं कर सकता है। उदाहरण के लिए एक ठोस LDAP सर्वर इस वेब एप्लिकेशन को उपयोगकर्ताओं को हटाने की अनुमति नहीं देता है। तो LdapUserBackendलागू करने वाला वर्ग लागू IUserBackendनहीं करेगा deleteUser(uid)

ठोस वर्ग को वेब एप्लिकेशन से संवाद करने की आवश्यकता है जो बैकएंड के उपयोगकर्ताओं के साथ वेब एप्लिकेशन को करने की अनुमति है।

ज्ञात समाधान

मैंने एक समाधान देखा है जहां IUserInterfaceएक implementedActionsविधि है जो एक पूर्णांक लौटाती है जो कि बिटवाइज़ के कार्यों का परिणाम है बिटवॉन्ड और अनुरोधित कार्यों के साथ एंडेड:

function implementedActions(requestedActions) {
    return (bool)(
        ACTION_GET_USER
        | ACTION_CREATE_USER
        | ACTION_DELTE_USER
        | ACTION_SET_PASSWORD
        ) & requestedActions)
}

कहाँ पे

  • ACTION_GET_USER = 1
  • ACTION_CREATE_USER = 2
  • ACTION_DELETE_USER = 4
  • ACTION_SET_PASSWORD = 8
  • .... = 16
  • .... = 32

आदि।

इसलिए वेब एप्लिकेशन एक बिटमास्क सेट करता है जिसकी उसे जरूरत है और implementedActions()बूलियन के साथ उत्तर देता है कि क्या यह उनका समर्थन करता है।

राय

मेरे लिए ये बिट ऑपरेशन सी उम्र से अवशेष की तरह दिखते हैं, जरूरी नहीं कि इसे स्वच्छ कोड के रूप में समझना आसान हो।

सवाल

क्लास के लिए एक आधुनिक (बेहतर?) पैटर्न क्या है, जिससे यह लागू होने वाले इंटरफेस के सबसेट को संप्रेषित करता है? या ऊपर से "बिट ऑपरेशन विधि" अभी भी सबसे अच्छा अभ्यास है?

( यदि यह मायने रखता है: PHP, हालांकि मैं OO भाषाओं के लिए एक सामान्य समाधान की तलाश में हूं )


5
सामान्य समाधान इंटरफ़ेस को विभाजित करना है। IUserBackendशामिल नहीं होना चाहिए deleteUserसब पर विधि। इसका हिस्सा होना चाहिए IUserDeleteBackend(या जिसे आप इसे कॉल करना चाहते हैं)। कोड जिसे उपयोगकर्ताओं को हटाने की आवश्यकता होगी, के पास तर्क होंगे IUserDeleteBackend, कोड की आवश्यकता नहीं है जो कार्यक्षमता का उपयोग करेगा IUserBackendऔर अभेद्य तरीकों से कोई परेशानी नहीं होगी ।
बकुरीउ

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

@SebastianRedl हाँ, यह कुछ ऐसा है जिसे मैंने ध्यान में नहीं लिया। मुझे वास्तव में रनटाइम के लिए एक समाधान की आवश्यकता है। चूँकि मैं बहुत अच्छे उत्तरों को अमान्य नहीं करना चाहता था, इसलिए मैंने एक नया प्रश्न खोला जो रनटाइम पर केंद्रित है
समस्या

जवाबों:


24

मोटे तौर पर, दो दृष्टिकोण हैं जिन्हें आप यहां ले जा सकते हैं: बहुरूपता के माध्यम से परीक्षण और फेंकना या रचना।

परीक्षण और फेंक

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

फिर, यदि supportsDelete()रिटर्न false, कॉलिंग deleteUser()एक NotImplementedExeptionफेंक दिया जा सकता है, या विधि कुछ भी नहीं कर रही है।

यह सरल के रूप में कुछ के बीच एक लोकप्रिय समाधान है। हालांकि, कई - खुद शामिल थे - तर्क देते हैं कि यह लिस्कोव के प्रतिस्थापन सिद्धांत (एलआईडी में एलआईडी) का उल्लंघन है और इसलिए एक अच्छा समाधान नहीं है।

बहुरूपता के माध्यम से रचना

यहां तक ​​पहुंचने का तरीका यह है कि IUserBackendएक उपकरण को भी कुंद कर दें। यदि कक्षाएं हमेशा उस इंटरफ़ेस में सभी विधियों को लागू नहीं कर सकती हैं, तो इंटरफ़ेस को अधिक केंद्रित भागों में तोड़ दें। तो आपके पास हो सकता है: IGeneralUser IDeletableUser IRenamableUser ... दूसरे शब्दों में, आपके सभी बैकएंड को लागू करने वाले सभी तरीकों में जा सकते हैं IGeneralUserऔर आप प्रत्येक क्रिया के लिए एक अलग इंटरफ़ेस बना सकते हैं जो केवल कुछ ही प्रदर्शन कर सकते हैं।

इस तरह, LdapUserBackendलागू नहीं होता है IDeletableUserऔर आप उस परीक्षण का उपयोग करते हैं जैसे कि (C # सिंटैक्स का उपयोग करके):

if (backend is IDeletableUser deletableUser)
{
    deletableUser.deleteUser(id);
}

(यदि कोई उदाहरण इंटरफ़ेस को लागू करता है और फिर आप उस इंटरफ़ेस पर कैसे आते हैं, यह निर्धारित करने के लिए PHP में तंत्र के बारे में निश्चित नहीं हूं, लेकिन मुझे यकीन है कि उस भाषा में एक समतुल्य है)

इस पद्धति का लाभ यह है कि यह आपके कोड को SOLID सिद्धांतों का अनुपालन करने में सक्षम बनाने के लिए बहुरूपता का अच्छा उपयोग करता है और मेरे विचार से सिर्फ अधिक सुरुचिपूर्ण है।

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


4
SOLID डिजाइन विचार के लिए +1। हमेशा अलग-अलग तरीकों से जवाब दिखाने के लिए अच्छा है जो कोड क्लीनर को आगे बढ़ाएगा!
कालेब

2
PHP में यह होगाif (backend instanceof IDelatableUser) {...}
Rad80

आप पहले से ही एलएसपी के उल्लंघन का उल्लेख करते हैं। मैं सहमत हूं, लेकिन इसे थोड़ा जोड़ना चाहता था: टेस्ट और थ्रो वैध है यदि इनपुट मान क्रिया को निष्पादित करना असंभव बनाता है, जैसे 0 को एक Divide(float,float)विधि में विभाजक के रूप में पारित करना । इनपुट मान परिवर्तनशील है, और अपवाद संभावित निष्पादन के एक छोटे सबसेट को कवर करता है। लेकिन अगर आप अपने कार्यान्वयन प्रकार के आधार पर फेंकते हैं, तो निष्पादित करने में इसकी अक्षमता एक दिया गया तथ्य है। अपवाद सभी संभव आदानों को शामिल करता है , न कि उनमें से एक सबसेट। यह दुनिया के हर गीले फर्श पर "गीला फर्श" का चिन्ह लगाने जैसा है जहाँ हर मंजिल हमेशा गीली रहती है।
Flater

एक प्रकार पर नहीं फेंकने के सिद्धांत के लिए एक अपवाद (उद्देश्य नहीं है)। C # के लिए, वह है NotImplementedException। यह अपवाद अस्थायी आउटेज के लिए अभिप्रेत है , अर्थात ऐसा कोड जो अभी विकसित नहीं हुआ है लेकिन विकसित किया जाएगा । यह निश्चित रूप से निश्चित नहीं है कि एक दिया गया वर्ग किसी भी विधि के साथ कभी भी कुछ नहीं करेगा , भले ही विकास पूरा हो जाए।
Flater

जवाब के लिए धन्यवाद। मुझे वास्तव में एक रनटाइम सॉल्यूशन की आवश्यकता थी लेकिन अपने प्रश्न में इस पर जोर देने में विफल रहा। चूँकि मैं आपके उत्तर का जवाब नहीं देना चाहता था, इसलिए मैंने एक नया प्रश्न बनाने का फैसला किया ।
problemofficer

5

वर्तमान स्थिति

वर्तमान सेटअप इंटरफ़ेस अलगाव सिद्धांत (SOLID में I) का उल्लंघन करता है।

संदर्भ

विकिपीडिया के अनुसार इंटरफ़ेस अलगाव सिद्धांत (आईएसपी) कहता है कि किसी भी ग्राहक को उन तरीकों पर निर्भर होने के लिए मजबूर नहीं किया जाना चाहिए जो इसका उपयोग नहीं करते हैं । इंटरफ़ेस अलगाव का सिद्धांत रॉबर्ट मार्टिन द्वारा 1990 के दशक के मध्य में तैयार किया गया था।

दूसरे शब्दों में, अगर यह आपका इंटरफ़ेस है:

public interface IUserBackend
{
    User getUser(int uid);
    User createUser(int uid);
    void deleteUser(int uid);
    void setPassword(int uid, string password);
}

फिर इस इंटरफ़ेस को लागू करने वाले प्रत्येक वर्ग को इंटरफ़ेस के प्रत्येक सूचीबद्ध तरीके का उपयोग करना चाहिए । कोई अपवाद नहीं।

कल्पना करें कि क्या कोई सामान्यीकृत विधि है:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     backendService.deleteUser(user.Uid);
}

यदि आप वास्तव में इसे बना रहे थे ताकि केवल कुछ कार्यान्वयन कक्षाएं वास्तव में एक उपयोगकर्ता को हटाने में सक्षम हों, तो यह विधि कभी-कभी आपके चेहरे पर उड़ जाएगी (या कुछ भी नहीं करें)। यह अच्छा डिज़ाइन नहीं है।


आपका प्रस्तावित समाधान

मैंने एक समाधान देखा है जहां IUserInterface में एक कार्यान्वित विधि है जो एक पूर्णांक लौटाती है जो कि बिटवाइज़ के कार्यों का परिणाम है।

आप अनिवार्य रूप से क्या करना चाहते हैं:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     if(backendService.canDeleteUser())
         backendService.deleteUser(user.Uid);
}

मैं इस बात की अनदेखी कर रहा हूं कि वास्तव में हम यह निर्धारित करते हैं कि एक दिया गया वर्ग किसी उपयोगकर्ता को हटाने में सक्षम है या नहीं। चाहे वह बूलियन हो, थोड़ा झंडा हो, ... कोई फर्क नहीं पड़ता। यह सभी एक द्विआधारी उत्तर को उबालता है: क्या यह उपयोगकर्ता को हटा सकता है, हां या नहीं?

यह समस्या को हल करेगा, है ना? ठीक है, तकनीकी रूप से, यह करता है। लेकिन अब, आप Liskov प्रतिस्थापन सिद्धांत (SOLID में L) का उल्लंघन कर रहे हैं ।

बल्कि जटिल विकिपीडिया स्पष्टीकरण के कारण, मुझे StackOverflow पर एक अच्छा उदाहरण मिला । "खराब" उदाहरण पर ध्यान दें:

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();

    duck.Swim();
}

मुझे लगता है कि आप यहाँ समानता देखते हैं। यह एक ऐसी विधि है जिसे एक अमूर्त वस्तु ( IDuck, IUserBackend) को संभालने के लिए माना जाता है , लेकिन एक समझौता किए गए वर्ग के डिजाइन के कारण, इसे पहले विशिष्ट कार्यान्वयन को संभालने के लिए मजबूर किया जाता है ( ElectricDuckयह सुनिश्चित करें कि यह एक IUserBackendवर्ग नहीं है जो उपयोगकर्ताओं को हटा नहीं सकता है)।

यह एक सार दृष्टिकोण विकसित करने के उद्देश्य को हराता है।

नोट: यहाँ उदाहरण आपके मामले से ठीक करने के लिए आसान है। उदाहरण के लिए, यह करने के लिए पर्याप्त होता है ElectricDuckपर बारी ही अंदरSwim() विधि। दोनों बतख अभी भी तैरने में सक्षम हैं, इसलिए कार्यात्मक परिणाम समान है।

आप ऐसा ही कुछ करना चाह सकते हैं। नहीं है । आप केवल एक उपयोगकर्ता को हटाने का दिखावा नहीं कर सकते, लेकिन वास्तव में एक खाली तरीका है। हालांकि यह एक तकनीकी दृष्टिकोण से काम करता है, लेकिन यह जानना असंभव है कि क्या आपके कार्यान्वयन वर्ग वास्तव में कुछ करने के लिए कहने पर कुछ करेंगे या नहीं। यह अकल्पनीय कोड के लिए एक प्रजनन मैदान है।


मेरा प्रस्तावित समाधान

लेकिन आपने कहा कि कार्यान्वयन वर्ग के लिए यह संभव है (और सही) केवल इनमें से कुछ तरीकों को संभालना।

उदाहरण के लिए, मान लें कि इन विधियों के हर संभव संयोजन के लिए, एक वर्ग है जो इसे लागू करेगा। यह हमारे सभी ठिकानों को कवर करता है।

इसका समाधान इंटरफ़ेस को विभाजित करना है

public interface IGetUserService
{
    User getUser(int uid);
}

public interface ICreateUserService
{
    User createUser(int uid);
}

public interface IDeleteUserService
{
    void deleteUser(int uid);
}

public interface ISetPasswordService
{
    void setPassword(int uid, string password);
}

ध्यान दें कि आपने मेरे उत्तर की शुरुआत में इसे देखा है। इंटरफ़ेस पृथक्करण सिद्धांत नाम पहले से ही पता चलता है कि इस सिद्धांत आप बनाने के लिए डिज़ाइन किया गया है इंटरफेस अलग एक पर्याप्त डिग्री करने के लिए।

यह आपको मिक्स-एंड-मैच इंटरफेस की अनुमति देता है, जैसा कि आप कृपया:

public class UserRetrievalService 
              : IGetUserService, ICreateUserService
{
    //getUser and createUser methods implemented here
}

public class UserDeleteService
              : IDeleteUserService
{
    //deleteUser method implemented here
}

public class DoesEverythingService 
              : IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
    //All methods implemented here
}

प्रत्येक वर्ग यह तय कर सकता है कि वे क्या करना चाहते हैं, बिना अपने इंटरफेस के अनुबंध को तोड़ने के।

इसका अर्थ यह भी है कि हमें यह जाँचने की आवश्यकता नहीं है कि एक निश्चित वर्ग किसी उपयोगकर्ता को हटाने में सक्षम है या नहीं। IDeleteUserServiceइंटरफ़ेस को लागू करने वाला प्रत्येक वर्ग एक उपयोगकर्ता को हटाने में सक्षम होगा = लिस्कोव प्रतिस्थापन सिद्धांत का कोई उल्लंघन नहीं

public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
     backendService.deleteUser(user.Uid); //guaranteed to work
}

यदि कोई ऐसी वस्तु को पारित करने की कोशिश करता है जो लागू नहीं होती है IDeleteUserService, तो कार्यक्रम संकलन के लिए मना कर देगा। यही कारण है कि हम टाइप सेफ्टी रखना पसंद करते हैं।

HaveUserDeleted(new DoesEverythingService());    // No problem.
HaveUserDeleted(new UserDeleteService());        // No problem.
HaveUserDeleted(new UserRetrievalService());     // COMPILE ERROR

पाद लेख

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

उदाहरण के लिए, यदि प्रत्येक सेवा जो एक उपयोगकर्ता बना सकती है वह हमेशा उपयोगकर्ता (और इसके विपरीत) को हटाने में सक्षम होती है, तो आप इन विधियों को एकल इंटरफ़ेस के भाग के रूप में रख सकते हैं:

public interface IManageUserService
{
    User createUser(int uid);
    void deleteUser(int uid);
}

छोटे विखंडों को अलग करने के बजाय ऐसा करने में कोई तकनीकी लाभ नहीं है; लेकिन यह विकास को थोड़ा आसान बना देगा क्योंकि इसमें कम बॉयलरप्लेटिंग की आवश्यकता होती है।


+1 वे व्यवहार का समर्थन करने के लिए इंटरफेस को विभाजित करते हैं, जो एक इंटरफ़ेस का संपूर्ण उद्देश्य है।
ग्रेग बरगार्ड

जवाब के लिए धन्यवाद। मुझे वास्तव में एक रनटाइम सॉल्यूशन की आवश्यकता थी लेकिन अपने प्रश्न में इस पर जोर देने में विफल रहा। चूँकि मैं आपके उत्तर का जवाब नहीं देना चाहता था, इसलिए मैंने एक नया प्रश्न बनाने का फैसला किया ।
problemofficer

@problemofficer: इन मामलों के लिए रनटाइम मूल्यांकन शायद ही कभी सबसे अच्छा विकल्प है, लेकिन ऐसा करने के लिए वास्तव में मामले हैं। ऐसे मामले में, आप या तो एक विधि बनाते हैं जिसे कॉल किया जा सकता है लेकिन कुछ भी नहीं कर सकता है (इसे TryDeleteUserप्रतिबिंबित करने के लिए कॉल करें ); या यदि आपके पास संभवतया एक अपवाद को फेंक दिया जाता है यदि यह एक संभव-लेकिन-समस्याग्रस्त स्थिति है। एक CanDoThing()और DoThing()विधि दृष्टिकोण का उपयोग करना काम करता है, लेकिन इसके लिए आपके बाहरी कॉलर्स को दो कॉल का उपयोग करना होगा (और ऐसा करने में विफल रहने के लिए दंडित होना चाहिए), जो कम सहज है और उतना सुरुचिपूर्ण नहीं है।
Flater

0

यदि आप उच्च स्तर के प्रकारों का उपयोग करना चाहते हैं, तो आप अपनी पसंद की भाषा में सेट प्रकार के साथ जा सकते हैं। उम्मीद है कि यह सेट चौराहों और उप-निर्धारण करने के लिए कुछ सिंटैक्स चीनी प्रदान करता है।

यह मूल रूप से जावा EnumSet के साथ क्या करता है (सिंटैक्स शुगर घटाता है , लेकिन हे, यह जावा है)


0

.NET दुनिया में आप कस्टम विशेषताओं के साथ तरीकों और कक्षाओं को सजा सकते हैं। यह आपके मामले के लिए प्रासंगिक नहीं हो सकता है।

हालांकि यह मुझे लगता है कि आपके पास यह मुद्दा अधिक उच्च स्तर के डिजाइन के साथ हो सकता है।

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

यूआई के लिए, आप इस बात पर ध्यान देना चाहते हैं कि आप फीचर प्रबंधन कैसे करते हैं, और उपलब्ध कार्यान्वयन की पसंद को उस पर बाँध सकते हैं, बजाय इसके कि चयनित कार्यान्वयन ड्राइव में क्या सुविधाएँ प्रबंधित की जा सकती हैं। आप सुविधा निर्भरता की रचना के लिए चौखटे देखना चाहते हैं, और अपने डोमेन मॉडल में स्पष्ट रूप से क्षमताओं को परिभाषित कर सकते हैं। यह भी प्राधिकरण में बांधा जा सकता है। अनिवार्य रूप से, यह तय करना कि क्या क्षमता उपलब्ध है या प्राधिकरण स्तर पर आधारित नहीं है, यह तय करने के लिए बढ़ाया जा सकता है कि क्या क्षमता वास्तव में कार्यान्वित की जाती है, और फिर उच्च स्तरीय UI 'सुविधाओं' में क्षमता सेट के लिए स्पष्ट मैपिंग हो सकती है।

यदि यह एक वेब एपीआई है, तो समय के साथ क्षमताओं का विस्तार होने पर 'मैनेज यूजर' एपीआई या 'यूजर' रीस्ट संसाधन के कई सार्वजनिक संस्करणों का समर्थन करने से समग्र डिजाइन विकल्प जटिल हो सकता है।

इसलिए संक्षेप में, .NET की दुनिया में आप विभिन्न रिफ्लेक्शन / एट्रीब्यूट के तरीकों का फायदा उठा सकते हैं, जो पहले से तय करते हैं कि कौन सी कक्षाएं लागू होती हैं, लेकिन किसी भी स्थिति में ऐसा लगता है कि असली मुद्दे उस जानकारी के साथ होने वाले हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.