मैं अपने कार्यक्रम में नियमों और जादू की संख्या का एक बहुत बड़ा समूह कैसे प्रबंधित करूं?


21

मैं प्रोग्रामिंग के लिए कुछ नया हूं (मैं व्यापार द्वारा एक मैकेनिकल इंजीनियर हूं), और मैं अपने डाउनटाइम के दौरान एक छोटा कार्यक्रम विकसित कर रहा हूं जो संयंत्र के चारों ओर से विभिन्न लोगों के इनपुट के आधार पर (सॉलिडवर्क्स) भाग उत्पन्न करता है।

केवल कुछ इनपुट्स (6 सटीक होने के लिए) के आधार पर, मुझे सैकड़ों एपीआई कॉल करने की आवश्यकता है जो प्रत्येक में एक दर्जन पैरामीटर ले सकते हैं; सभी नियमों के एक सेट से उत्पन्न जो मैंने उस भाग को संभालने वाले सभी का साक्षात्कार करने के बाद इकट्ठा किया है। मेरे कोड के नियम और पैरामीटर सेक्शन 250 लाइन और बढ़ते हैं।

तो, मेरे कोड को पठनीय और प्रबंधनीय रखने का सबसे अच्छा तरीका क्या है? मैं अपने सभी मैजिक नंबरों, सभी नियमों, एल्गोरिदम और कोड के प्रक्रियात्मक भागों को कैसे कंपार्टमेंटलाइज़ करूं? मैं एक बहुत ही क्रिया और बारीक एपीआई से कैसे निपटूं?

मेरा मुख्य लक्ष्य किसी को मेरे स्रोत को सौंपने में सक्षम होना है और उन्हें समझना है कि मैं क्या कर रहा था, मेरे इनपुट के बिना।


7
क्या आप इन एपीआई कॉल के कुछ उदाहरण प्रदान कर सकते हैं?
रॉबर्ट हार्वे


"कंप्यूटर विज्ञान में सभी समस्याओं को एक अन्य स्तर के अप्रत्यक्ष रूप से हल किया जा सकता है" - डेविड व्हीलर
फिल फ्रॉस्ट

... अप्रत्यक्ष के कई स्तरों को छोड़कर :)
Dan Lyons

1
आपके कोड को देखे बिना आपके प्रश्न का उत्तर देना कठिन है। आप अपना कोड codereview.stackexchange.com पर पोस्ट कर सकते हैं और अन्य प्रोग्रामर से सलाह ले सकते हैं।
गिल्बर्ट ले ब्लैंक

जवाबों:


26

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

ध्यान रखें, 'डेटाबेस' का मतलब जरूरी नहीं कि MySQL या MS-SQL हो। आप डेटा कैसे संग्रहित करते हैं, यह इस बात पर निर्भर करता है कि प्रोग्राम का उपयोग कैसे किया जाता है, आप इसे कैसे लिख रहे हैं, आदि। इसका मतलब SQL टाइप डेटाबेस हो सकता है, या इसका मतलब केवल स्वरूपित टेक्स्ट फ़ाइल हो सकता है।


7
एक डेटाबेस में डेटा को संहिताबद्ध करने से सहमत, हालांकि ऐसा लगता है कि उसे बड़ी समस्याएं हैं।
रॉबर्ट हार्वे

अगर मैं एक ऐसा प्रोग्राम बना रहा था जो पूरी तरह से अलग-अलग हिस्सों को बनाता है, तो हाँ, यह जाने का तरीका होगा। यह चार अलग अलग विन्यास के साथ केवल एक हिस्सा है, हालांकि। यह कभी भी बहुत बड़ी बात नहीं होगी (जब तक कि वे डेवलपर को ऐसा कुछ बनाने के लिए न लें, जिस स्थिति में यह कोई फर्क नहीं पड़ता)। हालाँकि, मुझे लगता है कि यह एक महान सीखने का अनुभव होगा जब मैं समाप्त हो गया और रिफ्लेक्टर करना चाहता था।
user2785724

1
सॉफ्ट कोडिंग जैसी लगती है । डेटाबेस परिवर्तनशील स्थिति के लिए हैं। जादू की संख्या परिभाषा के अनुसार, परस्पर नहीं हैं।
फिल फ्रॉस्ट

1
@PhilFrost: आप उन्हें अपरिवर्तनीय बना सकते हैं। प्रारंभिक तालिका निर्माण के बाद बस उन्हें न लिखें।
रॉबर्ट हार्वे

1
@PhilFrost: खैर, मैंने अब एपीआई को देखा है , जिसके साथ वह काम कर रहा है। यह केवल इसके विशाल आकार के लिए उल्लेखनीय है। जब तक वह ऐसा नहीं करता है तब तक उसे डेटाबेस की आवश्यकता नहीं हो सकती है।
रॉबर्ट हार्वे

14

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

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

आदर्श रूप से आप कहते हैं कि कोड के साथ समाप्त होगा:

LeftBearingHoleDepth = BearingWidth + HoleDepthTolerance;
if (not CheckPartWidth(LeftBearingHoleDepth, {other parameters})
    {whatever you need to adjust}

स्थिरांक कितने स्थानीय हैं, इस पर निर्भर करते हुए कि मैं उन्हें उन कार्यों में घोषित करने के लिए लुभाऊंगा, जहां वे संभव हैं। इसे चालू करना काफी उपयोगी है:

SomeAPICall(10,324.5, 1, 0.02, 6857);

में

const NumberOfOilDrainHoles = 10
const OilDrainHoleSpacing = 324.5
{etc}
SomeAPICall(NumberOfOilDrainHoles, OilDrainHoleSpacing, {etc}

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

नामों के लिए एक टिप: बाईं ओर सबसे महत्वपूर्ण शब्द डालें। यह बहुत अच्छी तरह से नहीं पढ़ सकता है, लेकिन यह चीजों को आसान बनाता है। ज्यादातर समय आप एक बोनी को देख रहे हैं और बोल्ट के बारे में सोच रहे हैं, बोल्ट को नहीं देख रहे हैं और सोच रहे हैं कि यह कहां होता है, इसलिए इसे SumpBoltThreadPitch नहीं BoltThreadPitchSump कहें। फिर स्थिरांक की सूची को क्रमबद्ध करें। बाद में, सभी थ्रेड पिचों को निकालने के लिए आप एक टेक्स्ट एडिटर में सूची प्राप्त कर सकते हैं और या तो फंक्शन फ़ंक्शन का उपयोग कर सकते हैं, या केवल "लाईनपाइच" वाली लाइनों को वापस करने के लिए grep जैसे टूल का उपयोग कर सकते हैं।


1
धाराप्रवाह इंटरफ़ेस बनाने पर भी विचार करें
इयान

यहाँ मेरे कोड से एक वास्तविक लाइन है। क्या यह समझ में आता है कि यहाँ क्या चल रहा है (तर्क X1, y1, z1, x2, y2, z2 दोहरे हैं), अगर आपको पता था कि चर नामों का क्या मतलब है? .CreateLine(m_trunion_support_spacing / 2, -((m_flask_length / 2) + m_sand_ledge_width + m_wall_thickness), -m_flange_thickness, m_trunion_support_spacing / 2, -((m_flask_length / 2) + m_sand_ledge_width + m_wall_thickness), -m_flask_height + m_flange_thickness)
user2785724

आप स्थिरांक खोजने के लिए संपादक एकीकरण के साथ ctags का भी उपयोग कर सकते हैं ।
फिल फ्रॉस्ट

3
@ user2785724 यह एक गड़बड़ है। यह क्या कर रहा हैं? क्या यह एक विशेष लंबाई और गहराई का खांचा बना रहा है? तब आप एक फंक्शन बना सकते हैं, जिसका नाम है createGroove(length, depth)। आपको उन कार्यों को कार्यान्वित करने की आवश्यकता है जो वर्णन करते हैं कि आप क्या पूरा करना चाहते हैं जैसा कि आप उन्हें मैकेनिकल इंजीनियर के लिए वर्णन करेंगे। यही साक्षर प्रोग्रामिंग के बारे में है।
फिल फ्रॉस्ट

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

4

मुझे लगता है कि आपका प्रश्न कम हो जाता है: मैं एक संगणना कैसे बनाऊं? कृपया ध्यान दें कि आप "नियमों का एक सेट" प्रबंधित करना चाहते हैं, जो कोड हैं, और "मैजिक नंबर का एक सेट", जो डेटा हैं। (आप उन्हें "आपके कोड में एम्बेडेड डेटा" के रूप में देख सकते हैं, लेकिन वे फिर भी डेटा हैं)।

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

इन पुस्तकों के सभी संकेत आपके प्रश्न पर लागू होंगे। मुझे विशेष रूप से "जादू नंबर" और "नियमों के सेट" के लिए कुछ संकेत निकालने दें:

  1. जादू की संख्या को बदलने के लिए नामांकित स्थिरांक और गणना का उपयोग करें

    स्थिरांक का उदाहरण :

    if (partWidth > 0.625) {
        // doSomeApiCall ...
    }
    return (partWidth - 0.625)
    

    एक नामांकित स्थिरांक के साथ प्रतिस्थापित किया जाना चाहिए ताकि बाद में कोई भी परिवर्तन टाइपो को पेश न कर सके और अपने कोड को तोड़ सके, जैसे पहले को बदलकर 0.625लेकिन दूसरा नहीं।

    const double MAX_PART_WIDTH = 0.625;
    
    if (partWidth > MAX_PART_WIDTH) {
        // doSomeApiCall ...
    }
    return (partWidth - MAX_PART_WIDTH)
    

    गणना का उदाहरण :

    Enumerations आपको डेटा को एक साथ रखने में मदद कर सकते हैं जो एक साथ हैं। यदि आप जावा का उपयोग कर रहे हैं, तो याद रखें कि एनम ऑब्जेक्ट हैं; उनके तत्व डेटा को पकड़ सकते हैं, और आप उन तरीकों को परिभाषित कर सकते हैं जो सभी तत्वों को वापस करते हैं, या कुछ संपत्ति की जांच करते हैं। यहाँ एक Enum का उपयोग किसी अन्य Enum के निर्माण में किया जाता है:

    public enum EnginePart {
        CYLINDER (100, Materials.STEEL),
        FLYWHEEL (120, Materials.STEEL),
        CRANKSHAFT (200, Materials.CARBON);
    
        private final double maxTemperature;
        private final Materials composition;
        private EnginePart(double maxTemperature, Materials composition) {
            this.maxTemperature = maxTemperature;
            this.composition = composition;
        }
    }
    
    public enum Materials {
        STEEL,
        CARBON
    }
    

    लाभ: अब कोई भी गलत तरीके से एक EnginePart को परिभाषित नहीं कर सकता है जो स्टील या कार्बन से बना नहीं है, और कोई भी "Asdfasdf" नामक एक EnginePart को पेश नहीं कर सकता है, जैसा कि अगर यह एक स्ट्रिंग है जो सामग्री पर जाँच की जाएगी।

  2. रणनीति पैटर्न और फैक्टरी विधि पैटर्न , रणनीति पैटर्न के मामले में, "नियम" संपुटित और उन्हें किसी अन्य उद्देश्य यह है कि बनाता है (उनमें से उपयोग करें फैक्टरी पैटर्न के मामले में उपयोग कुछ बनाने जा रहा है करने के लिए पारित करने के लिए कैसे का वर्णन उपयोग जो कुछ भी आप चाहते हैं)।

    फैक्टरी विधि पैटर्न का उदाहरण :

    कल्पना कीजिए कि आपके पास दो प्रकार के इंजन हैं: एक जहां प्रत्येक भाग को कंप्रेसर से जोड़ा जाना है, और एक जहां प्रत्येक भाग स्वतंत्र रूप से जो भी अन्य भागों से जुड़ा हो सकता है। विकिपीडिया से अनुकूलित

    public class EngineAssemblyLine {
        public EngineAssemblyLine() {
            EnginePart enginePart1 = makeEnginePart();
            EnginePart enginePart2 = makeEnginePart();
            enginePart1.connect(enginePart2);
            this.addEngine(engine1);
            this.addEngine(engine2);
        }
    
        protected Room makeEngine() {
            return new NormalEngine();
        }
    }
    

    और फिर दूसरी कक्षा में:

    public class CompressedEngineAssemblyLine extends EngineAssemblyLine {
        @Override
        protected Room makeRoom() {
            return new CompressedEngine();
        }
    }
    

    दिलचस्प हिस्सा यह है: अब आपका असेंबली लाइन कंस्ट्रक्टर किस प्रकार के इंजन को संभाल रहा है, से अलग हो गया है। हो सकता है कि addEngineविधियां एक दूरस्थ एपीआई को बुला रही हों ...

    रणनीति पैटर्न का उदाहरण :

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

    class PartWithStrategy:
    
        def __init__(self, func=None) :
            if func:
                self.execute = func
    
        def execute(self):
            # ... call API of quality review ...
            print "Part will be reviewed"
    
    
    def polish():
        # ... call API of polishing department ...
        print "Part will be polished"
    
    
    def paint():
        # ... call API of painting department ...
        print "Part will be painted"
    
    if __name__ == "__main__" :
        strat0 = PartWithStrategy()
        strat1 = PartWithStrategy(polish)
        strat2 = PartWithStrategy(paint)
    
        strat0.execute()  # output is "Part will be reviewed"
        strat1.execute()  # output is "Part will be polished"
        strat2.execute()  # output is "Part will be painted"
    

    आप इसे उन कार्यवाहियों की एक सूची रखने के लिए विस्तारित कर सकते हैं जिन्हें आप निष्पादित करना चाहते हैं, और फिर उन्हें executeविधि से बदले में बुला रहे हैं । हो सकता है कि इस सामान्यीकरण को एक बिल्डर पैटर्न के रूप में बेहतर तरीके से वर्णित किया जा सकता है , लेकिन हे, हम अचार प्राप्त नहीं करना चाहते हैं, क्या हम? :)


2

आप एक नियम इंजन का उपयोग करना चाह सकते हैं। एक नियम इंजन आपको एक डीएसएल (डोमेन स्पेसिफिक लैंग्वेज) देता है जो कि इस प्रश्न में बताए गए तरीके से एक निश्चित परिणाम के लिए आवश्यक मानदंड को तैयार करने के लिए डिज़ाइन किया गया है ।

नियम इंजन के कार्यान्वयन के आधार पर, कोड को फिर से जमा किए बिना भी नियम बदले जा सकते हैं। और क्योंकि नियम अपनी, सरल भाषा में लिखे गए हैं, इसलिए उन्हें उपयोगकर्ताओं द्वारा भी बदला जा सकता है।

यदि आप भाग्यशाली हैं तो आपके द्वारा उपयोग की जा रही प्रोग्रामिंग भाषा के लिए एक रेडी-टू-यूज़ रूल्स इंजन है।

नकारात्मक पक्ष यह है कि आपको एक नियम इंजन से परिचित होना होगा जो एक प्रोग्रामिंग शुरुआत करने वाले के लिए कठिन हो सकता है।


1

इस समस्या का मेरा समाधान काफी अलग है: परतें, सेटिंग्स और LOP।

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

दूसरा, एक 'सेटिंग मैनेजर' को लागू करना। यह नामों को गतिशील रूप से मूल्यों के साथ जोड़ने का एक तरीका है। कुछ इस तरह। एक और मिनी भाषा।

Baseplate.name="Base plate"
Baseplate.length=1032.5
Baseplate.width=587.3

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

मैं समझता हूं कि यह एक जटिल दृष्टिकोण है, और आपकी समस्या के लिए ओवरकिल हो सकता है, या आपके पास अभी तक प्राप्त करने वाले कौशल की आवश्यकता नहीं है। यह सिर्फ यह है कि मैं इसे कैसे करूंगा।


0

मुझे यकीन नहीं है कि मुझे सही तरीके से सवाल मिला है, लेकिन ऐसा लगता है कि आपको कुछ संरचनाओं में समूह बनाना चाहिए। यदि आप C ++ का उपयोग कर रहे हैं तो कहें, आप चीजों को परिभाषित कर सकते हैं:

struct SomeParametersClass
{
    int   p1;  // this is for that
    float p2;  // this is a different parameter
    ...
    SomeParametersClass() // constructor, assigns default values
    {
        p1 = 42; // the best value that some guy told me
        p2 = 3.14; // looks like a know value, but isn't
    {
};

struct SomeOtherParametersClass
{
    int   v1;  // this is for ...
    float v2;  // this is for ...
    ...
    SomeOtherParametersClass() // constructor, assigns default values
    {
        v1 = 24; // the best value 
        v2 = 1.23; // also the best value
    }
};

आप कार्यक्रम की शुरुआत में इन्हें शामिल कर सकते हैं:

int main()
{
    SomeParametersClass params1;
    SomeOtherParametersClass params2;
    ...

तब आपकी API कॉल दिखेंगी (यह मानते हुए कि आप हस्ताक्षर नहीं बदल सकते हैं):

 SomeAPICall( params1.p1, params1.p2 );

यदि आप एपीआई के हस्ताक्षर को बदल सकते हैं, तो आप पूरी संरचना को पारित कर सकते हैं:

 SomeAPICall( params1 );

आप सभी मापदंडों को एक बड़े आवरण में समूहित कर सकते हैं:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};

0

मुझे आश्चर्य है कि किसी और ने इसका उल्लेख नहीं किया है ...

तुमने कहा था:

मेरा मुख्य लक्ष्य किसी को मेरे स्रोत को सौंपने में सक्षम होना है और उन्हें समझना है कि मैं क्या कर रहा था, मेरे इनपुट के बिना।

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

अन्य सभी उत्तर भारी तकनीकी आधारित हैं, लेकिन वे उन बुनियादी बातों की अनदेखी कर रहे हैं जो अधिकांश प्रोग्रामर सीखते हैं। चूँकि आप एक mech engie द्वारा व्यापार करते हैं, मेरा अनुमान है कि आप प्रलेखन की इस शैली के अभ्यस्त नहीं हैं।

टिप्पणी करना और अच्छा चयन करना, रसीला चर नाम पठनीयता के साथ बहुत मदद करता है। जिसे समझना आसान है?

var x = y + z;

या:

//Where bandwidth, which was previously defined is (1000 * Info Rate) / FEC Rate / Modulation * carrier spacing / 1000000
float endFrequency = centerFrequency + (1/2 bandwidth);

यह सुंदर भाषा स्वतंत्र है। कोई फर्क नहीं पड़ता कि आप किस प्लेटफॉर्म, आईडीई, भाषा आदि के साथ काम कर रहे हैं, उचित दस्तावेज सबसे साफ, सबसे आसान तरीका है जिससे यह सुनिश्चित किया जा सकता है कि कोई आपके कोड को समझ सकता है।

आगे यह उन जादुई संख्याओं और चिंताओं के टन का प्रबंधन करने के लिए है, लेकिन मुझे लगता है कि ग्रैंडमास्टरबी की टिप्पणी ने बहुत अच्छी तरह से संभाला।

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