Barbara Liskov ਦੀਆਂ ਡੇਟਾ ਐਬਸਟਰੈਕਸ਼ਨ ਸਿਧਾਂਤਾਂ ਸਿੱਖੋ ਤਾਂ ਜੋ ਤੁਸੀਂ ਸਥਿਰ ਇੰਟਰਫੇਸ ਡਿਜ਼ਾਈਨ ਕਰ ਸਕੋ, breakages ਘਟਾ ਸਕੋ, ਅਤੇ ਸਪਸ਼ਟ, ਭਰੋਸੇਯੋਗ APIs ਨਾਲ maintainable systems ਬਣਾਉ।

Barbara Liskov ਇੱਕ ਕੰਪਿਊਟਰ ਵਿਗਿਆਨੀ ਹਨ ਜਿਨ੍ਹਾਂ ਦੇ ਕੰਮ ਨੇ ਆਧੁਨਿਕ ਸਾਫਟਵੇਅਰ ਟੀਮਾਂ ਦੇ ਬਣਾਉਣ ਦੇ ਢੰਗ ਨੂੰ ਬੜੀ ਨਰਮੀ ਨਾਲ ਰੂਪ ਦਿੱਤਾ। ਉਹਨਾਂ ਦੀਆਂ ਰਿਸਰਚਾਂ data abstraction, information hiding, ਅਤੇ ਬਾਅਦ ਵਿੱਚ Liskov Substitution Principle (LSP) ਨੇ ਭਾਸ਼ਾਵਾਂ ਤੋਂ ਲੈ ਕੇ API ਢਾਂਚਿਆਂ ਤੱਕ ਸਭ ਕੁਝ ਪ੍ਰਭਾਵਤ ਕੀਤਾ: ਸਪਸ਼ਟ ਵਿਹਾਰ ਨਿਰਧਾਰਤ ਕਰੋ, ਅੰਦਰੂਨੀ ਚੀਜ਼ਾਂ ਨੂੰ ਬਚਾਓ, ਅਤੇ ਇਹ ਸੁਨਿਸ਼ਚਿਤ ਕਰੋ ਕਿ ਹੋਰ ਲੋਕ ਤੁਹਾਡੇ ਇੰਟਰਫੇਸ 'ਤੇ ਨਿਰਭਰ ਕਰ ਸਕਣ।
ਇੱਕ ਭਰੋਸੇਯੋਗ API ਸਿਰਫ਼ ਸਿਧਾਂਤਕ ਤੌਰ 'ਤੇ "ਸਹੀ" ਨਹੀਂ ਹੁੰਦਾ। ਇਹ ਇੱਕ ਐਸਾ ਇੰਟਰਫੇਸ ਹੈ ਜੋ ਉਤਪਾਦ ਨੂੰ ਤੇਜ਼ੀ ਨਾਲ ਅੱਗੇ ਵਧਣ ਵਿੱਚ ਮਦਦ ਕਰਦਾ ਹੈ:
ਉਹ ਭਰੋਸਾ ਇੱਕ ਅਨੁਭਵ ਹੈ: ਉਸ ਡਿਵੈਲਪਰ ਲਈ ਜੋ ਤੁਹਾਡੇ API ਨੂੰ ਕਾਲ ਕਰਦਾ ਹੈ, ਉਸ ਟੀਮ ਲਈ ਜੋ ਇਸ ਨੂੰ ਰੱਖਦੀ ਹੈ, ਅਤੇ ਉਹ ਉਪਭੋਗਤਾਵਾਂ ਲਈ ਜੋ ਇਸ 'ਤੇ ਅਪਰੋਕ੍ਸੀਮੀਟ ਤੌਰ 'ਤੇ ਨਿਰਭਰ ਹਨ।
ਡੇਟਾ ਐਬਸਟਰੈਕਸ਼ਨ ਇਹ ਵਿਚਾਰ ਹੈ ਕਿ callers ਨੂੰ ਕਿਸੇ ਕੰਸੈਪਟ (ਇਕ account, ਇੱਕ queue, ਇੱਕ subscription) ਨਾਲ ਕੁਝ ਚੁਣੇ ਹੋਏ ਓਪਰੇਸ਼ਨਾਂ ਰਾਹੀਂ ਇੰਟਰੈਕਟ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ—ਨਾ ਕਿ ਉਸ ਦੇ ਸਟੋਰ ਹੋਣ ਜਾਂ ਕੇਮ ਦਿਵੇਂ ਦੀਆਂ ਗੁੰਝਲਦਾਰ ਵਿਸਥਾਰਾਂ ਰਾਹੀਂ।
ਜਦੋਂ ਤੁਸੀਂ ਪ੍ਰਤਿਨਿਧਿਤਾ ਦੇ ਵੇਰਵੇ ਲੁਕਾਉਂਦੇ ਹੋ, ਤਾਂ ਤੁਸੀਂ ਗਲਤੀਆਂ ਦੇ ਪੂਰੇ ਵਰਗਾਂ ਨੂੰ ਹਟਾ ਦਿੰਦੇ ਹੋ: ਕੋਈ ਵੀ ਆਕਸਮਿਕ ਤੌਰ 'ਤੇ ਕਿਸੇ ਡੇਟਾਬੇਸ ਫੀਲਡ 'ਤੇ ਨਿਰਭਰ ਨਹੀਂ ਕਰ ਸਕਦਾ ਜੋ ਪਬਲਿਕ ਨਹੀ ਸੀ, ਜਾਂ ਸਾਂਝੇ state ਨੂੰ ਇਸ ਤਰ੍ਹਾਂ ਮਿਊਟੇਟ ਨਹੀਂ ਕਰ ਸਕਦਾ ਕਿ ਸਿਸਟਮ ਸਮਝ ਨਾ ਸਕੇ। ਸਭ ਤੋਂ ਜ਼ਰੂਰੀ ਗੱਲ, abstraction coordination ਦੀ ਲੋੜ ਘਟਾਉਂਦੀ ਹੈ: ਜਦ ਤੱਕ public ਵਿਹਾਰ ਇੱਕੋ ਜਿਹਾ ਰਹਿੰਦਾ ਹੈ, ਟੀਮਾਂ ਨੂੰ ਅੰਦਰੂਨੀ ਰੀਫੈਕਟੋਰ ਕਰਨ ਲਈ ਇਜਾਜ਼ਤ ਲੈਣ ਦੀ ਲੋੜ ਨਹੀਂ।
ਇਸ ਲੇਖ ਦੇ ਅਖੀਰ ਤੱਕ, ਤੁਸੀਂ ਪ੍ਰਯੋਗਿਕ ਤਰੀਕੇ ਜਾਣੋਂਗੇ ਜਿੰਨ੍ਹਾਂ ਨਾਲ ਤੁਸੀਂ:
ਜੇ ਤੁਸੀਂ ਬਾਅਦ ਵਿੱਚ ਇੱਕ ਛੋਟੀ ਸੰਖੇਪ ਚਾਹੁੰਦੇ ਹੋ, ਤਾਂ /blog/a-practical-checklist-for-designing-reliable-apis ਨੂੰ ਵੇਖੋ।
ਡੇਟਾ ਐਬਸਟਰੈਕਸ਼ਨ ਇੱਕ ਸਿੱਧੀ ਗੱਲ ਹੈ: ਤੁਸੀਂ ਕਿਸੇ ਚੀਜ਼ ਨਾਲ ਉਸ ਦੇ "ਕੀ ਕਰਦਾ ਹੈ" ਦੇ ਆਧਾਰ 'ਤੇ ਇੰਟਰੈਕਟ ਕਰਦੇ ਹੋ, ਨਾ ਕਿ ਇਸ ਗੱਲ 'ਤੇ ਕਿ ਇਹ ਕਿਵੇਂ ਬਣਿਆ ਹੈ।
ਇੱਕ vending machine ਬਾਰੇ ਸੋਚੋ। ਤੁਹਾਨੂੰ ਜਾਣਨ ਦੀ ਲੋੜ ਨਹੀਂ ਕਿ ਮੋਟਰ ਕਿਵੇਂ ਘੁੰਮਦੇ ਹਨ ਜਾਂ ਸਿੱਕੇ ਕਿਵੇਂ ਗਿਣੇ ਜਾਂਦੇ ਹਨ। ਤੁਸੀਂ ਸਿਰਫ਼ ਕੰਟਰੋਲ ਜਾਣਦੇ ਹੋ ("item ਚੁਣੋ", "ਭੁਗਤਾਨ ਕਰੋ", "ਆਈਟਮ ਪ੍ਰਾਪਤ ਕਰੋ") ਅਤੇ ਨਿਯਮ ("ਜੇ ਤੁਸੀਂ ਕਾਫੀ ਭੁਗਤਾਨ ਕਰੋਗੇ ਤਾਂ ਆਈਟਮ ਮਿਲੇਗੀ; ਜੇ sold out ਹੋਵੇ ਤਾਂ refund ਮਿਲੇਗਾ"). ਇਹੀ abstraction ਹੈ।
ਸਾਫਟਵੇਅਰ ਵਿੱਚ, interface "ਕੀ ਕਰਦਾ ਹੈ" ਹੁੰਦਾ ਹੈ: ਓਪਰੇਸ਼ਨਾਂ ਦੇ ਨਾਮ, ਉਹਨਾਂ ਨੂੰ ਕੀ ਇਨਪੁੱਟ ਮਿਲਦਾ ਹੈ, ਕੀ ਆਉਟਪੁੱਟ ਮਿਲਦਾ ਹੈ, ਅਤੇ ਕਿਹੜੀਆਂ errors ਦੀ ਉਮੀਦ ਹੈ। implementation "ਕਿਵੇਂ ਕੰਮ ਕਰਦਾ ਹੈ" ਹੈ: ਡੇਟਾਬੇਸ ਟੇਬਲ, caching ਰਣਨੀਤੀ, ਅੰਦਰੂਨੀ ਕਲਾਸਾਂ, ਅਤੇ ਪਰਫਾਰਮੈਂਸ ਟ੍ਰਿਕਸ।
ਇਨ੍ਹਾਂ ਨੂੰ ਵੱਖਰਾ ਰੱਖਣਾ ਤੁਹਾਨੂੰ ਅਜੇਹੇ APIs ਦਿੰਦਾ ਹੈ ਜੋ ਸਿਸਟਮ ਦੇ ਵਿਕਾਸ ਦੇ ਬਾਵਜੂਦ ਸਥਿਰ ਰਹਿੰਦੇ ਹਨ। ਤੁਸੀਂ ਅੰਦਰੂਨੀ ਨੂੰ ਮੁੜਲਿਖ ਸਕਦੇ ਹੋ, ਲਾਇਬ੍ਰੇਰੀਆਂ ਬਦਲ ਸਕਦੇ ਹੋ, ਜਾਂ ਸਟੋਰੇਜ਼ optimize ਕਰ ਸਕਦੇ ਹੋ—ਜਦਕਿ ਇੰਟਰਫੇਸ ਉਪਭੋਗਤਾਵਾਂ ਲਈ ਉਹੀ ਰਹਿੰਦਾ ਹੈ।
ਇਕ abstract data type "container + allowed operations + rules" ਹੁੰਦਾ ਹੈ, ਜਿਸ ਨੂੰ ਕਿਸੇ ਵਿਸ਼ੇਸ਼ ਅੰਦਰੂਨੀ ਸਰਚਨਾ ਨਾਲ ਬੰਨ੍ਹ ਕੇ ਨਹੀਂ ਦਿੱਤਾ ਜਾਂਦਾ।
ਉਦਾਹਰਣ: ਇੱਕ Stack (last in, first out).
ਮੁੱਖ ਵਾਅਦਾ: pop() ਆਖਰੀ push() ਨੂੰ ਵਾਪਸ ਕਰਦਾ ਹੈ। ਇਹ ਗੱਲ ਕਿ stack array, linked list ਜਾਂ ਹੋਰ ਕਿਸੇ ਚੀਜ਼ ਤੇ ਆਧਾਰਿਤ ਹੈ, ਨਿੱਜੀ ਹੈ।
ਉਹੀ ਵਿਭਾਜਨ ਹਰ ਥਾਂ ਲਾਗੂ ਹੁੰਦੀ ਹੈ:
POST /payments ਇੰਟਰਫੇਸ ਹੈ; fraud checks, retries, ਅਤੇ database writes ਇੰਪਲੀਮਿੰਟੇਸ਼ਨ ਹਨ।client.upload(file) ਇੰਟਰਫੇਸ ਹੈ; chunking, compression, ਅਤੇ parallel requests ਇੰਪਲੀਮਿੰਟੇਸ਼ਨ ਹਨ।ਜਦੋਂ ਤੁਸੀਂ abstraction ਨਾਲ ਡਿਜ਼ਾਈਨ ਕਰਦੇ ਹੋ, ਤੁਸੀਂ ਉਸ ਠੇਕੇ 'ਤੇ ਧਿਆਨ ਦਿੰਦੇ ਹੋ ਜਿਨ੍ਹਾਂ 'ਤੇ ਗਾਹਕ ਨਿਰਭਰ ਕਰਦੇ ਹਨ—ਅਤੇ ਇਸ ਤਰ੍ਹਾਂ ਪਿੱਛੇ ਦੀਆਂ ਸਾਰੀਆਂ ਚੀਜ਼ਾਂ ਨੂੰ ਬਦਲਣ ਦਾ ਸੁਤੰਤਰ ਦਿੱਤਾ ਜਾਂਦਾ ਹੈ ਬਿਨਾਂ ਉਨ੍ਹਾਂ ਨੂੰ ਤੋੜੇ।
ਇੱਕ invariant ਉਹ ਨਿਯਮ ਹੈ ਜੋ abstraction ਦੇ ਅੰਦਰ ਹਮੇਸ਼ਾ ਸੱਚ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ। ਜੇ ਤੁਸੀਂ API ਡਿਜ਼ਾਈਨ ਕਰ ਰਹੇ ਹੋ, ਤਾਂ invariants ਉਹ ਗਾਰਡਰੇਲ ਹਨ ਜੋ ਤੁਹਾਡੇ ਡੇਟਾ ਨੂੰ ਅਸੰਭਵ ਅਵਸਥਾਵਾਂ ਵਿੱਚ ਜਾਣ ਤੋਂ ਰੋਕਦੇ ਹਨ—ਜਿਵੇਂ ਇੱਕ ਬੈਂਕ ਅਕਾਊਂਟ ਜਿਸ ਵਿੱਚ ਇਕੱਠੇ ਦੋ ਮੁਦਰਾਵਾਂ ਹੋਣ, ਜਾਂ ਇੱਕ "completed" order ਜਿਸ ਵਿੱਚ ਕੋਈ ਆਈਟਮ ਨਹੀਂ।
ਇੱਕ invariant ਨੂੰ ਆਪਣੇ ਟਾਈਪ ਦੀ "ਹਕੀਕਤ ਦੀ ਸ਼ਕਲ" ਵਜੋਂ ਸੋਚੋ:
Cart ਵਿੱਚ negative quantities ਨਹੀਂ ਹੋ ਸਕਦੀਆਂ।UserEmail ਹਮੇਸ਼ਾ ਇੱਕ ਵੈਧ email ਪਤਾ ਹੋਵੇਗਾ ("ਬਾਅਦ ਵਿੱਚ validate" ਨਹੀਂ)।Reservation ਲਈ start < end ਹੋਵੇ ਅਤੇ ਦੋਹਾਂ ਸਮਿਆਂ ਇੱਕੋ timezone ਵਿੱਚ ਹੋਣ।ਜੇ ਇਹ ਬਿਆਨ ਸਹੀ ਰਹਿਣੀ ਬੰਦ ਹੋ ਜਾਂਦੀ ਹੈ, ਤਾਂ ਤੁਹਾਡਾ ਸਿਸਟਮ ਅਣਪਛਾਤਾ ਹੋ ਜਾਂਦਾ ਹੈ, ਕਿਉਂਕਿ ਹਰ ਫੀਚਰ ਨੂੰ ਹੁਣ ਇਹ ਅਨੁਮਾਨ ਲਗਾਉਣਾ ਪੈਂਦਾ ਹੈ ਕਿ "ਬ੍ਰੋਕਨ" ਡੇਟਾ ਦਾ ਕੀ ਮਤਲਬ ਹੈ।
ਚੰਗੇ APIs ਸਰਹੱਦਾਂ 'ਤੇ invariants ਨੂੰ ਲਾਗੂ ਕਰਦੇ ਹਨ:
ਇਸ ਨਾਲ error handling ਕੁਦਰਤੀ ਤੌਰ 'ਤੇ ਸੁਧਰਦੀ ਹੈ: vague failures ("ਕੁਝ ਗਲਤ ਹੋ ਗਿਆ") ਦੀ ਜਗ੍ਹਾ API ਦੱਸ ਸਕਦੀ ਹੈ ਕਿਹੜਾ ਨਿਯਮ ਉਲੰਘਿਆ ਗਿਆ ("end must be after start").
ਕਾਲਰਾਂ ਨੂੰ ਅੰਦਰੂਨੀ ਨਿਯਮ ਯਾਦ ਨਹੀਂ ਰੱਖਣੇ ਚਾਹੀਦੇ—ਜਿਵੇਂ "ਇਹ method ਸਿਰਫ਼ normalize() ਕਾਲ ਕਰਨ ਤੋਂ ਬਾਅਦ ਕੰਮ ਕਰਦੀ ਹੈ"। ਜੇ ਕੋਈ invariant ਕਿਸੇ ਵਿਸ਼ੇਸ਼ ਰਸਮ 'ਤੇ ਨਿਰਭਰ ਕਰਦੀ ਹੈ, ਤਾਂ ਉਹ invariant ਨਹੀਂ—ਉਹ ਇੱਕ footgun ਹੈ।
ਇੰਟਰਫੇਸ ਇਸ ਤਰ੍ਹਾਂ ਡਿਜ਼ਾਈਨ ਕਰੋ ਕਿ:
API ਟਾਈਪ ਨੂੰ ਦਸਤਾਵੇਜ਼ ਕਰਦੇ ਸਮੇਂ, ਲਿਖੋ:
ਅਚਛਾ API ਸਿਰਫ਼ ਫੰਕਸ਼ਨਾਂ ਦਾ ਸੈੱਟ ਨਹੀਂ—ਇਹ ਇੱਕ ਵਾਅਦਾ ਹੈ। Contracts ਉਸ ਵਾਅਦੇ ਨੂੰ ਸਪਸ਼ਟ ਕਰਦੇ ਹਨ, ਤਾਂ ਜੋ callers ਵਿਹਾਰ 'ਤੇ ਨਿਰਭਰ ਰਹਿ ਸਕਣ ਅਤੇ maintainers ਅੰਦਰੂਨੀ ਬਦਲਾਅ ਬਿਨਾਂ ਕਿਸੇ ਹੈਰਾਨੀ ਦੇ ਕਰ ਸਕਣ।
ਘੱਟੋ-ਘੱਟ, ਦਸਤਾਵੇਜ਼ ਕਰੋ:
ਇਹ ਸਪਸ਼ਟਤਾ ਵਿਹਾਰ ਨੂੰ ਪੇਸ਼ਗੋਈਯੋਗ ਬਣਾਉਂਦੀ ਹੈ: callers ਜਾਣਦੇ ਹਨ ਕਿ ਕਿਹੜੇ ਇਨਪੁੱਟ ਸੁਰੱਖਿਅਤ ਹਨ ਅਤੇ ਕਿਸ ਨਤੀਜੇ ਨੂੰ ਹੈਂਡਲ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ, ਅਤੇ ਟੈਸਟਾਂ ਕਿਰਿਆਵਾਂ ਦੀ ਜਾਂਚ ਕਰ ਸਕਦੀਆਂ ਹਨ ਘੱਟ guessing ਦੇ।
ਬਿਨਾਂ contracts ਦੇ, ਟੀਮਾਂ ਯਾਦ ਤੇ ਅਣਔਪਚਾਰਿਕ ਨਿਯਮਾਂ ਤੇ ਨਿਰਭਰ ਕਰਦੀਆਂ ਹਨ: "ਉਥੇ null ਨਾ ਪੇਸ਼ ਕਰੋ", "ਇਹ ਕਾਲ ਕਈ ਵਾਰੀ retry ਕਰਦੀ ਹੈ", "ਇਹ error 'ਤੇ ਖਾਲੀ ਵਾਪਸੀ ਦਿੰਦੀ ਹੈ"। ਇਹ ਨਿਯਮ onboarding, refactors, ਜਾਂ incidents ਦੌਰਾਨ ਖਤਮ ਹੋ ਜਾਂਦੇ ਹਨ।
ਇੱਕ ਲਿਖੀ ਹੋਈ contract ਉਹਨਾਂ ਛੁਪੇ ਨਿਯਮਾਂ ਨੂੰ ਸਾਂਝੇ ਗਿਆਨ 'ਚ ਬਦਲ ਦਿੰਦੀ ਹੈ। ਇਹ code reviews ਲਈ ਵੀ ਇੱਕ ਸਥਿਰ ਟਾਰਗੇਟ ਬਣਾਉਂਦੀ ਹੈ: ਗੱਲਬਾਤ ਹੁੰਦੀ ਹੈ "ਕੀ ਇਹ ਬਦਲਾਅ ਹੁਣ ਵੀ contract ਪੂਰਾ ਕਰਦਾ ਹੈ?" ਬਜਾਏ "ਮੇਰੇ ਕੋਲ ਕੰਮ ਕੀਤਾ।"
ਅਸਪਸ਼ਟ: "Creates a user."
ਚੰਗਾ: "Creates a user with a unique email.
email must be a valid address; caller must have users:create permission.userId; the user is persisted and immediately retrievable.409 if email already exists; returns 400 for invalid fields; no partial user is created."ਅਸਪਸ਼ਟ: "Gets items quickly."
ਚੰਗਾ: "Returns up to limit items sorted by createdAt descending.
nextCursor for the next page; cursors expire after 15 minutes."Information hiding data abstraction ਦਾ ਪ੍ਰਯੋਗਿਕ ਪੱਖ ਹੈ: callers ਨੂੰ ਕੀ API ਕਰਦਾ ਹੈ 'ਤੇ ਨਿਰਭਰ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ, ਨਾ ਕਿ ਕਿਵੇਂ। ਜੇ ਉਪਭੋਗਤਾ ਤੁਹਾਡੇ ਅੰਦਰੂਨੀ ਵੀਯੁਹ ਨੂੰ ਨਹੀਂ ਦੇਖ ਸਕਦੇ, ਤਾਂ ਤੁਸੀਂ ਬਿਨਾਂ ਹਰ ਰਿਲੀਜ਼ ਨੂੰ breaking change ਬਣਾਉਣ ਦੇ ਇਹ ਬਦਲ ਸਕਦੇ ਹੋ।
ਚੰਗਾ ਇੰਟਰਫੇਸ ਕੁਝ ਖਾਸ ਓਪਰੇਸ਼ਨਾਂ (create, fetch, update, list, validate) ਦਿਖਾਉਂਦਾ ਹੈ ਅਤੇ representation—ਟੇਬਲ, caches, queues, ਫਾਈਲ ਲੇਆਊਟ, ਸਰਵਿਸ ਬਾਊਂਡਰੀਜ਼—ਨਿੱਜੀ ਰੱਖਦਾ ਹੈ।
ਉਦਾਹਰਣ ਲਈ, "add item to cart" ਇੱਕ ਓਪਰੇਸ਼ਨ ਹੈ। ਤੁਹਾਡੇ database ਤੋਂ "CartRowId" ਇੱਕ implementation detail ਹੈ। ਜਦੋਂ ਤੁਸੀਂ ਵਿਸਥਾਰ ਪ੍ਰਗਟ ਕਰਦੇ ਹੋ, ਤੁਸੀਂ ਉਪਭੋਗਤਾਵਾਂ ਨੂੰ ਉਸ 'ਤੇ ਨਿਰਭਰ ਹੋਣ ਲਈ ਆਮੰਤ੍ਰਿਤ ਕਰਦੇ ਹੋ ਅਤੇ ਇਸ ਨਾਲ ਤੁਸੀਂ ਬਦਲਾਅ ਕਰਨ ਦੀ ਯੋਗਤਾ ਗੁਆ ਦੇਂਦੇ ਹੋ।
ਜਦੋਂ clients ਸਿਰਫ਼ stable ਵਿਹਾਰ 'ਤੇ ਨਿਰਭਰ ਕਰਦੇ ਹਨ, ਤੁਸੀਂ:
…ਅਤੇ API ਅਨੁਕੂਲ ਰਹਿੰਦੀ ਹੈ ਕਿਉਂਕਿ contract ਨਹੀਂ ਬਦਲੇ। ਇਹੀ ਸੱਚੀ ਲਾਭ ਹੈ: ਉਪਭੋਗਤਾਵਾਂ ਲਈ ਸਥਿਰਤਾ, maintainers ਲਈ ਆਜ਼ਾਦੀ।
ਕੁਝ ਤਰੀਕੇ ਜਿਨ੍ਹਾਂ ਨਾਲ ਅੰਦਰੂਨੀ ਗਲਤਫਹਮੀ ਨਾਲ ਜਹਿਰ ਹੋ ਜਾਂਦੀ ਹੈ:
status=3 ਦੀ ਵਜਾਇ ਇੱਕ ਸਪਸ਼ਟ ਨਾਮ ਜਾਂ ਸਮਰਪਿਤ ਓਪਰੇਸ਼ਨ।ਉਹ responses ਪਸੰਦ ਕਰੋ ਜੋ ਅਰਥ ਨੂੰ ਵੇਰਵਾ ਕਰਦੀਆਂ ਹਨ, ਮਿਕੈਨਿਕਸ ਨੂੰ ਨਹੀਂ:
"userId": "usr_…") ਬਜਾਏ database row numbers ਦੇ।ਜੇ ਕੋਈ ਵੇਰਵਾ ਬਦਲ ਸਕਦਾ ਹੈ, ਤਾਂ ਇਸ ਨੂੰ ਪ੍ਰਕਟ ਨਾ ਕਰੋ। ਜੇ ਉਪਭੋਗਤਾ ਨੂੰ ਇਹ ਲੋੜ ਹੈ, ਤਾਂ ਇਸ ਨੂੰ intentional ਅਤੇ ਦਸਤਾਵੇਜ਼ੀਕ੍ਰਿਤ ਹਿੱਸੇ ਦੇ ਤੌਰ 'ਤੇ ਇੰਟਰਫੇਸ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ।
LSP ਇਕ ਵਾਕ ਵਿੱਚ: ਜੇ ਕੋਡ ਕਿਸੇ ਇੰਟਰਫੇਸ ਨਾਲ ਕੰਮ ਕਰਦਾ ਹੈ, ਤਾਂ ਇਹ ਕਿਸੇ ਵੀ ਵੈਧ implementation ਨਾਲ ਕੰਮ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ—ਬਿਨਾਂ ਖਾਸ ਕੇਸਾਂ ਦੇ।
LSP ਵਿਰਾਸਤ ਤੋਂ ਜ਼ਿਆਦਾ ਭਰੋਸੇ ਬਾਰੇ ਹੈ। ਜਦੋਂ ਤੁਸੀਂ ਇੱਕ ਇੰਟਰਫੇਸ ਪ੍ਰਕਾਸ਼ਿਤ ਕਰਦੇ ਹੋ, ਤੁਸੀਂ ਵਿਹਾਰ ਬਾਰੇ ਇੱਕ ਵਾਅਦਾ ਕਰ ਰਹੇ ਹੋ। LSP ਕਹਿੰਦਾ ਹੈ ਕਿ ਹਰ implementation ਨੂੰ ਉਹ ਵਾਅਦਾ ਰੱਖਣਾ ਚਾਹੀਦਾ ਹੈ, ਭਾਵੇਂ ਉਹ ਬਹੁਤ ਵੱਖਰਾ ਹੋਵੇ।
ਕਾਲਰ ਤੁਹਾਡੇ API ਦੇ ਕਹਿਣ 'ਤੇ ਨਿਰਭਰ ਕਰਦੇ ਹਨ—ਨਾ ਕਿ ਇਸ 'ਤੇ ਜੋ ਅੱਜ ਕਰਦਾ ਹੈ। ਜੇ ਇੰਟਰਫੇਸ ਕਹਿੰਦਾ ਹੈ "ਤੁਸੀਂ save() ਨੂੰ ਕਿਸੇ ਵੀ ਵੈਧ record ਨਾਲ ਕਾਲ ਕਰ ਸਕਦੇ ਹੋ", ਤਾਂ ਹਰ implementation ਨੂੰ ਉਹ records ਸਵੀਕਾਰ ਕਰਨੇ ਚਾਹੀਦੇ ਹਨ। ਜੇ ਇੰਟਰਫੇਸ ਕਹਿੰਦਾ ਹੈ "get() ਇੱਕ value ਜਾਂ ਇੱਕ ਸਪਸ਼ਟ ‘not found’ outcome ਵਾਪਸ ਕਰਦਾ ਹੈ", ਤਾਂ implementations randomly ਨਵੀਆਂ errors throw ਨਹੀਂ ਕਰ ਸਕਦੀਆਂ ਜਾਂ partial data ਵਾਪਸ ਨਹੀਂ ਕਰ ਸਕਦੀਆਂ।
Safe extension ਦਾ ਮਤਲਬ ਹੈ ਕਿ ਤੁਸੀਂ ਨਵੇਂ implementations ਜੋੜ ਸਕਦੇ ਹੋ ਬਿਨਾਂ users ਨੂੰ ਕੋਡ ਦੁਬਾਰਾ ਲਿਖਣ 'ਤੇ ਮਜਬੂਰ ਕੀਤੇ। ਇਹੀ LSP ਦਾ ਪ੍ਰਯੋਗਿਕ ਫਾਇਦਾ ਹੈ: ਇਹ ਇੰਟਰਫੇਸ ਨੂੰ swappable ਰੱਖਦਾ ਹੈ।
ਦੋ ਆਮ ਤਰੀਕੇ ਜੋ APIs promise ਟੋੜਦੇ ਹਨ:
Narrower inputs (ਕਠੋਰ preconditions): ਨਵੀਂ implementation ਉਹ ਇਨਪੁੱਟ ਰੱਦ ਕਰਦੀ ਹੈ ਜੋ interface ਨੇ ਮਨਜ਼ੂਰ ਕੀਤੇ ਸਨ। ਉਦਾਹਰਣ: base interface ਕਿਸੇ ਵੀ UTF‑8 string ਨੂੰ ID ਵਜੋਂ ਲੈਂਦਾ ਹੈ, ਪਰ ਇੱਕ implementation ਸਿਰਫ਼ ਨੰਬਰਾਂ ਨੂੰ ਜਿਸਕਾਰਤ ਕਰਦੀ ਹੈ ਜਾਂ ਖਾਲੀ-ਪਰ-ਵੈਧ ਫੀਲਡ ਨੂੰ ਰੱਦ ਕਰ ਦਿੰਦੀ ਹੈ।
Weaker outputs (ਛੋਟੇ postconditions): ਨਵੀਂ implementation ਵਾਅਦੇ ਤੋਂ ਘੱਟ ਵਾਪਸ ਕਰਦੀ ਹੈ। ਉਦਾਹਰਣ: interface ਕਹਿੰਦਾ ਹੈ ਕਿ ਨਤੀਜੇ sorted, unique ਜਾਂ complete ਹੋਣਗੇ—ਪਰ ਇੱਕ implementation unsorted, duplicates, ਜਾਂ items drop ਕਰ ਦਿੰਦੀ ਹੈ।
ਇੱਕ ਤੀਜਾ ਸੁਤੰਤਰਕ ਉਲੰਘਣ failure behavior ਵਿੱਚ ਤਬਦੀਲੀ ਹੈ: ਜੇ ਇੱਕ implementation "not found" ਵਾਪਸ ਕਰਦੀ ਹੈ ਤੇ ਦੂਜੀ exception ਫੇਂਕਦੀ ਹੈ, ਤਾਂ callers ਇੱਕ implementation ਨੂੰ ਦੂਜੀ ਨਾਲ ਸੁਰੱਖਿਅਤ ਤੌਰ 'ਤੇ ਬਦਲ ਨਹੀਂ ਸਕਦੇ।
"Plug-ins" (ਬਹੁਤੀਆਂ implementations) ਨੂੰ ਸਹਿਯੋਗ ਦੇਣ ਲਈ, interface ਨੂੰ contract ਵਾਂਗ ਲਿਖੋ:
ਜੇ ਇੱਕ implementation ਨੂੰ ਵਾਕਈ ਕਠੋਰ rules ਦੀ ਲੋੜ ਹੈ, ਤਾਂ ਉਸ ਨੂੰ ਇੱਕ ਵੱਖਰਾ interface ਦਿਓ ਜਾਂ explicit capability ਦਿਖਾਓ (ਉਦਾਹਰਣ: supportsNumericIds()), ਤਾਂ ਕਿ clients ਜਾਣ-ਬੂਝ ਕੇ opt-in ਕਰਨ।
ਅੱਚੀ ਡਿਜ਼ਾਈਨ ਕੀਤੀ ਇੰਟਰਫੇਸ ਵਰਤਣ ਵਿੱਚ "ਸਪਸ਼ਟ" ਮਹਿਸੂਸ ਹੁੰਦੀ ਹੈ ਕਿਉਂਕਿ ਇਹ ਸਿਰਫ਼ ਉਹੀ ਚੀਜ਼ ਦਿਖਾਉਂਦੀ ਹੈ ਜਿਸ ਦੀ caller ਨੂੰ ਲੋੜ ਹੈ—ਅਤੇ ਹੋਰ ਕੁਝ ਨਹੀਂ। Liskov ਦੀ ਡੇਟਾ ਐਬਸਟਰੈਕਸ਼ਨ ਦੀ ਸੋਚ ਤੁਹਾਨੂੰ ਐਸੀਆਂ ਇੰਟਰਫੇਸ ਵੱਲ ਧੱਕਦੀ ਹੈ ਜੋ ਸੰਕੁਚਿਤ, ਸਥਿਰ, ਅਤੇ ਪੜ੍ਹਨ ਲਈ ਆਸਾਨ ਹੁੰਦੀਆਂ ਹਨ, ਤਾਂ ਜੋ ਉਪਭੋਗਤਾ ਉਨ੍ਹਾਂ 'ਤੇ ਅੰਦਰੂਨੀ ਵੇਰਵਿਆਂ ਬਿਨਾਂ ਨਿਰਭਰ ਰਹਿ ਸਕਣ।
ਵੱਡੇ APIs ਅਕਸਰ ਵੱਖ-ਵੱਖ ਜ਼ਿੰਮੇਵਾਰੀਆਂ ਨੂੰ ਮਿਲਾ ਦਿੰਦੇ ਹਨ: configuration, state changes, reporting, troubleshooting—all ਇਕੱਠੇ। ਇਹ ਸਮਝਣਾ ਮੁਸ਼ਕਲ ਬਣਾਉਂਦਾ ਹੈ ਕਿ ਕਦੋਂ ਕੀ ਸੁਰੱਖਿਅਤ ਕਾਲ ਹੈ।
ਇੱਕ cohesive interface ਉਹ ਓਪਰੇਸ਼ਨਾਂ ਨੂੰ ਗ੍ਰੁੱਪ ਕਰਦਾ ਹੈ ਜੋ ਇੱਕੋ abstraction ਨਾਲ ਸੰਬੰਧਤ ਹਨ। ਜੇ ਤੁਹਾਡਾ API ਇੱਕ queue ਦਰਸਾਉਂਦਾ ਹੈ, ਤਾਂ queue ਵਿਹਾਰ (enqueue/dequeue/peek/size) 'ਤੇ ਧਿਆਨ ਦਿਓ, general-purpose utilities 'ਤੇ ਨਹੀਂ। ਘੱਟ ਤੱਤ = ਘੱਟ ਗਲਤ ਵਰਤੋਂ।
"ਲਚਕੀਲਾ" ਅਕਸਰ "ਅਸਪਸ਼ਟ" ਹੁੰਦਾ ਹੈ। options: any, mode: string, ਜਾਂ booleans ਦੇ ਢੇਰ (ਉਦਾਹਰਣ: force, skipCache, silent) ਅਜਿਹਾ ambiguity ਬਣਾਉਂਦੇ ਹਨ।
ਪਸੰਦ ਕਰੋ:
publish() vs publishDraft()), ਜਾਂਜੇ ਕਿਸੇ parameter ਲਈ callers ਨੂੰ source ਫਾਇਲ ਦੇਖਨੀ ਪੈਂਦੀ ਹੈ ਤਾਂ ਉਹ abstraction ਦਾ ਹਿੱਸਾ ਨਹੀਂ ਹੈ।
ਨਾਮ contract ਨੂੰ ਸੁਚਿੱਤ ਕਰਦੇ ਹਨ। ਉਹ verbs ਚੁਣੋ ਜੋ ਪ੍ਰੇਖਿਆਤਮਕ ਵਿਹਾਰ ਵਰਨਨ ਕਰਦੇ ਹਨ: reserve, release, validate, list, get। ਚਤੁ੍ਰ ਰੂਪਕਾਂ ਅਤੇ overloaded ਸ਼ਬਦਾਂ ਤੋਂ ਬਚੋ। ਜੇ ਦੋ ਮੈਥਡਾਂ ਦੇ ਨਾਮ ਇੱਕੋ ਜਿਹੇ ਲੱਗਦੇ ਹਨ, callers assume ਕਰਨਗੇ ਕਿ ਉਹ ਉਹੀ ਤਰ੍ਹਾਂ ਵਰਤੋਂਗੇ—ਇਸ ਲਈ ਇਹ ਸਚ ਬਣਾਓ।
ਇੱਕ API ਨੂੰ ਵੱਖਰਾ ਕਰੋ ਜਦੋਂ ਤੁਸੀਂ ਦੇਖੋ:
ਅਲੱਗ modules ਤੁਹਾਨੂੰ ਅੰਦਰੂਨੀ ਤੌਰ 'ਤੇ ਤਬਦੀਲੀ ਕਰਨ ਦੀ ਆਜ਼ਾਦੀ ਦਿੰਦੇ ਹਨ ਜਦਕਿ ਮੁੱਖ ਵਾਅਦਾ ਸਥਿਰ ਰਿਹਾ। ਜੇ ਤੁਸੀਂ ਵਧਣ ਦੀ ਯੋਜਨਾ ਕਰ ਰਹੇ ਹੋ, ਤਾਂ ਇੱਕ ਸੁੱਟ core package ਅਤੇ add-ons 'ਤੇ ਵੀ ਸੋਚੋ; ਵੇਖੋ /blog/evolving-apis-without-breaking-users।
APIs ਕਦੇ ਇੱਕ ਜਹੜੇ 'ਤੇ ਨਹੀਂ ਰਹਿੰਦੇ। ਨਵੀਆਂ ਫੀਚਰਾਂ ਆਉਂਦੀਆਂ ਹਨ, ਏਜ ਕੇਸਾਂ ਮਿਲਦੇ ਹਨ, ਅਤੇ "ਛੋਟੀ ਸੁਧਾਰ" ਅਣਜਾਣੇ ਰੂਪ ਵਿੱਚ ਵਾਸਤਵਿਕ ਐਪਲੀਕੇਸ਼ਨਾਂ ਨੂੰ ਤੋੜ ਸਕਦੇ ਹਨ। ਲਕਸ਼੍ਯ ਇਹ ਨਹੀਂ ਕਿ ਇੰਟਰਫੇਸ ਨੂੰ ਜਮ੍ਹਾ ਦਿੱਤਾ ਜਾਵੇ—ਬਲਕਿ ਇਸ ਨੂੰ ਇਉਂ ਵਿਕਸਤ ਕੀਤਾ ਜਾਵੇ ਕਿ ਉਪਭੋਗਤਾਵਾਂ ਦੇ ਉਮੀਦਾਂ 'ਤੇ ਪਲਟੋ ਨਾ ਹੋਵੇ।
Semantic versioning ਇੱਕ ਸੰਚਾਰ ਸੰਦ ਹੈ:
ਇਸਦੀ ਸੀਮਾ: ਅਜੇ ਵੀ judgment ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ। ਜੇ ਇੱਕ "bug fix" ਨੇ ਉਹ ਵਿਹਾਰ ਬਦਲ ਦਿੱਤਾ ਜਿਸ 'ਤੇ callers ਨਿਰਭਰ ਕਰਦੇ ਸਨ, ਤਾਂ ਉਹ ਵਾਸਤਵ ਵਿੱਚ breaking ਹੈ—ਭਾਵੇਂ ਪਿਛਲਾ ਵਿਹਾਰ accident ਸੀ।
ਕਈ breaking changes compiler 'ਤੇ ਨਹੀਂ ਦਿਖਦੇ:
ਸੋਚੋ preconditions ਅਤੇ postconditions ਦੇ ਤੌਰ 'ਤੇ: callers ਨੂੰ ਕੀ ਪ੍ਰਦਾਨ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ, ਅਤੇ ਉਨ੍ਹਾਂ ਨੂੰ ਕੀ ਵਾਪਸ ਮਿਲਣ ਦੀ ਗਾਰੰਟੀ ਹੈ।
Deprecation ਤਦ ਹੀ ਕੰਮ ਕਰਦੀ ਹੈ ਜਦੋਂ ਇਹ explicit ਅਤੇ ਸਮੇਂ-ਬੱਧ ਹੋਵੇ:
Liskov-ਸ਼ੈਲੀ ਡੇਟਾ ਐਬਸਟਰੈਕਸ਼ਨ ਇਸ ਲਈ ਮਦਦਗਾਰ ਹੈ ਕਿਉਂਕਿ ਇਹ ਇਹ ਘਟਾਉਂਦਾ ਹੈ ਕਿ ਉਪਭੋਗਤਾ ਕੀ 'ਤੇ ਨਿਰਭਰ ਹੋ ਸਕਦੇ ਹਨ। ਜੇ callers ਸਿਰਫ਼ interface contract 'ਤੇ ਨਿਰਭਰ ਕਰਦੇ ਹਨ—ਨਹੀਂ ਕਿ ਅੰਦਰੂਨੀ ਸਰਚਨਾ 'ਤੇ—ਤਾਂ ਤੁਸੀਂ storage formats, algorithms, ਅਤੇ optimizations ਬਿਨਾਂ ਭਾਰੀ ਚਿੰਤਾ ਦੇ ਬਦਲ ਸਕਦੇ ਹੋ।
ਅਮਲ ਵਿੱਚ, ਇਹੀ ਥਾਂ ਹੈ ਜਿੱਥੇ ਮਜ਼ਬੂਤ tooling ਮਦਦ ਕਰਦਾ ਹੈ। ਉਦਾਹਰਣ ਲਈ, ਜੇ ਤੁਸੀਂ ਤੇਜ਼ੀ ਨਾਲ ਇੱਕ ਅੰਦਰੂਨੀ API 'ਤੇ iteration ਕਰ ਰਹੇ ਹੋ ਜਦਕਿ ਇੱਕ React web app ਜਾਂ Go + PostgreSQL backend ਤਿਆਰ ਕਰ ਰਹੇ ਹੋ, ਇੱਕ vibe-coding workflow ਜਿਵੇਂ Koder.ai implementation ਨੂੰ ਤੇਜ਼ ਕਰ ਸਕਦਾ ਹੈ ਬਿਨਾਂ ਕੋ core discipline ਨੂੰ ਘਟਾਏ: ਤੁਸੀਂ ਫਿਰ ਵੀ crisp contracts, stable identifiers, ਅਤੇ backward-compatible evolution ਚਾਹੁੰਦੇ ਹੋ। Speed ਇੱਕ multiplier ਹੈ—ਇਸ ਲਈ ਸਹੀ interface ਅਭਿਆਸਾਂ ਨੂੰ ਹੀ ਗੁਣਾ ਕਰੋ।
ਭਰੋਸੇਯੋਗ API ਉਹ ਨਹੀਂ ਜੋ ਕਦੇ ਫੇਲ ਨਹੀਂ ਹੁੰਦਾ—ਇਹ ਉਹ ਹੈ ਜੋ ਇਸ ਤਰ੍ਹਾਂ ਫੇਲ ਹੁੰਦਾ ਹੈ ਕਿ callers ਸਮਝ ਸਕਣ, ਹੈਂਡਲ ਕਰ ਸਕਣ, ਅਤੇ ਟੈਸਟ ਕਰ ਸਕਣ। Error handling abstraction ਦਾ ਹਿੱਸਾ ਹੈ: ਇਹ ਨਿਰਧਾਰਤ ਕਰਦਾ ਹੈ ਕਿ "ਸਹੀ ਵਰਤੋਂ" ਕੀ ਮਤਲਬ ਹੈ, ਅਤੇ ਜਦੋਂ ਦੁਨੀਆ (ਨੈੱਟਵਰਕ, ਡਿਸਕ, permissions, ਸਮਾਂ) ਸਹਿਮਤ ਨਹੀਂ ਹੁੰਦੀ ਤਾਂ ਕੀ ਹੁੰਦਾ ਹੈ।
ਦੋ ਵਰਗਾਂ ਨੂੰ ਅਲੱਗ ਕਰੋ:
ਇਹ ਫਰਕ ਤੁਸੀਂ API ਨੂੰ ਇਮਾਨਦਾਰ ਰੱਖਦਾ ਹੈ: callers ਸਿੱਖਦੇ ਹਨ ਕਿ ਉਹ ਕੀ ਕੋਡ 'ਚ ਠੀਕ ਕਰ ਸਕਦੇ ਹਨ ਅਤੇ ਕੀ runtime ਤੇ ਹੈਂਡਲ ਕਰਨਾ ਚਾਹੀਦਾ ਹੈ।
ਤੁਹਾਡਾ contract failure ਦੀ ਚੋਣ ਨੂੰ ਦਰਸਾਉਂਦਾ ਹੈ:
Ok | Error) ਜਦੋਂ failures ਉਮੀਦਯੋਗ ਹਨ ਅਤੇ ਤੁਸੀਂ चाहੁੰਦੇ ਹੋ ਕਿ callers ਸਪੱਸ਼ਟ ਤਰੀਕੇ ਨਾਲ ਹੈਂਡਲ ਕਰਨ।ਜੋ ਵੀ ਚੁਣੋ, API 'ਚ consistent ਰੱਖੋ ਤਾਂ ਕਿ ਉਪਭੋਗਤਾ guess ਨਾ ਕਰਨ।
ਹਰੇਕ ਓਪਰੇਸ਼ਨ ਲਈ ਸੰਭਵ failures ਨੂੰ ਅਰਥ ਦੇ ਰੂਪ ਵਿੱਚ ਲਿਸਟ ਕਰੋ, ਨਾਂ ਕਿ implementation ਵੇਰਵੇ ਦੇ ਤੌਰ 'ਤੇ: "conflict because version is stale", "not found", "permission denied", "rate limited"। stable error codes ਅਤੇ structure fields ਦਿਓ ਤਾਂ ਕਿ tests behavior ਨੂੰ string matching ਨਹੀਂ ਕਰਕੇ assert ਕਰ ਸਕਣ।
ਦੱਸੋ ਕਿ ਕੋਈ operation retry ਲਈ safe ਹੈ ਜਾਂ ਨਹੀਂ, ਕਿਸ ਹਾਲਤ ਵਿੱਚ, ਅਤੇ idempotency ਕਿਵੇਂ हासिल ਕੀਤੀ ਜਾਵੇ (idempotency keys, natural request IDs)। ਜੇ partial success ਸੰਭਵ ਹੈ (batch operations), ਤਾਂ ਦਰਸਾਓ ਕਿ successes ਅਤੇ failures ਕਿਵੇਂ report ਕੀਤੀਆਂ ਜਾਂਦੀਆਂ ਹਨ, ਅਤੇ timeout ਤੋਂ ਬਾਅਦ callers ਕਿਸ state ਦੀ ਉਮੀਦ ਰੱਖਣ।
ਇੱਕ abstraction ਇੱਕ ਵਾਅਦਾ ਹੁੰਦਾ ਹੈ: "ਜੇ ਤੁਸੀਂ ਇਨ੍ਹਾਂ ਓਪਰੇਸ਼ਨਾਂ ਨੂੰ ਵੈਧ ਇਨਪੁੱਟ ਨਾਲ ਕਾਲ ਕਰੋਗੇ, ਤਾਂ ਇਹ ਨਤੀਜੇ ਮਿਲਣਗੇ, ਅਤੇ ਇਹ ਨਿਯਮ ਸਦਾ ਸੱਚ ਰਹਿਣਗੇ।" ਟੈਸਟਿੰਗ ਇਹ ਵਾਅਦਾ ਬਣਾਈ ਰੱਖਣ ਦਾ ਤਰੀਕਾ ਹੈ ਜਦੋਂ ਕੋਡ ਬਦਲਦਾ ਹੈ।
ਸਭ ਤੋਂ ਪਹਿਲਾਂ contract ਨੂੰ ਉਹ checks ਬਣਾਓ ਜੋ ਤੁਸੀਂ ਆਟੋਮੈਟਿਕ ਚਲਾ ਸਕੋ।
Unit tests ਹਰ ਓਪਰੇਸ਼ਨ ਦੇ postconditions ਅਤੇ edge cases ਦੀ ਜਾਂਚ ਕਰਨ: return values, state changes, ਅਤੇ error behavior। ਜੇ ਤੁਹਾਡਾ interface ਕਹਿੰਦਾ ਹੈ "removing a non-existent item returns false and changes nothing"—ਇਸਦੀ ਇਕ ਦੀਖੀ unit test ਲਿਖੋ।
Integration tests contract ਨੂੰ ਅਸਲੀ ਸਰਹੱਦਾਂ 'ਤੇ validate ਕਰਨ: database, network, serialization, ਅਤੇ auth। ਕਈ ਵਾਰ contract violations ਸਿਰਫ਼ encode/decode ਜਾਂ retries/timeouts ਦੌਰਾਨ ਪ੍ਰਗਟ ਹੁੰਦੀਆਂ ਹਨ।
Invariants ਉਹ ਨਿਯਮ ਹਨ ਜੋ ਕਿਸੇ ਵੀ ਵਰਤੀ ਜਾਂਦੀਂ valid operations ਦੀ ਲੜੀ 'ਚ ਸਦਾ ਸੱਚ ਰਹਿਣੇ ਚਾਹੀਦੇ ਹਨ (ਉਦਾਹਰਣ: "balance ਕਦੇ negative ਨਹੀਂ ਹੁੰਦੀ", "IDs unique ਹਨ", "items returned by list() ਨੂੰ get(id) ਨਾਲ ਪ੍ਰਾਪਤ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ").
Property-based testing ਇਹਨਾਂ ਨਿਯਮਾਂ ਨੂੰ random-but-valid inputs ਅਤੇ operation sequences ਬਣਾਕੇ ਜਾਂਚਦਾ ਹੈ, ਅਤੇ counterexamples ਲੱਭਦਾ ਹੈ। ਇਸ ਤਰੀਕੇ ਨਾਲ ਅਕਸਰ ਉਹ ਕੌਨਰ ਕੇਸ ਮਿਲ ਜਾਂਦੇ ਹਨ ਜੋ ਮਨੁੱਖ ਨਹੀਂ ਸੋਚਦੇ।
ਪਬਲਿਕ ਜਾਂ ਸ਼ੇਅਰ ਕੀਤੀ APIs ਲਈ, consumers ਉਹ ਉਦਾਹਰਣ ਪਬਲਿਸ਼ ਕਰਨ ਜੋ ਉਹ ਬੇਨਤੀਆਂ ਕਰਦੇ ਹਨ ਅਤੇ ਜਿਸ ਪ੍ਰਕਾਰ ਦੇ responses ਉਨ੍ਹਾਂ ਨੇ ਨਿਰਭਰ ਕੀਤੇ ਹਨ। Providers ਫਿਰ ਇਹਨਾਂ contracts ਨੂੰ CI ਵਿੱਚ ਚਲਾਉਂਦੇ ਹਨ ਤਾਂ ਜੋ ਬਦਲਾਅ real usage ਨੂੰ ਨਾ ਤੋੜਨ।
ਟੈਸਟ ਸਭ ਕੁਝ ਕਵਰ ਨਹੀਂ ਕਰ ਸਕਦੇ, ਇਸ ਲਈ ਉਹ signals monitor ਕਰੋ ਜੋ ਦਰਸਾਉਂਦੇ ਹਨ ਕਿ contract ਬਦਲ ਰਿਹਾ ਹੈ: response shape changes, 4xx/5xx ਦਰਾਂ ਵਿੱਚ ਵਾਧਾ, ਨਵਿਆਂ error codes, latency spikes, ਅਤੇ "unknown field" ਜਾਂ deserialization failures। endpoint ਅਤੇ version ਦੁਆਰਾ ਇਹਨਾਂ ਨੂੰ track ਕਰੋ ਤਾਂ ਕਿ drift ਜਲਦੀ ਪਤਾ ਲੱਗੇ ਅਤੇ rollback ਕੀਤਾ ਜਾ ਸਕੇ।
ਜੇ ਤੁਸੀਂ snapshots ਜਾਂ rollbacks ਸਪੋਰਟ ਕਰਦੇ ਹੋ, ਤਾਂ ਉਹ delivery pipeline ਨਾਲ ਚੰਗਾ ਜੋੜ ਬਣਾਉਂਦੇ ਹਨ: drift ਜਲਦੀ detect ਕਰੋ, ਫਿਰ clients ਨੂੰ ਬਦਲਣ 'ਤੇ ਮਜਬੂਰ ਕੀਤੇ ਬਿਨਾਂ revert ਕਰੋ। (Koder.ai, ਉਦਾਹਰਣ ਲਈ, snapshots ਅਤੇ rollback ਨੂੰ workflow ਦਾ ਹਿੱਸਾ ਬਣਾਉਂਦਾ ਹੈ, ਜੋ "contracts first, changes second" ਸੋਚ ਨਾਲ ਮਿਲਦਾ ਹੈ.)
ਜੋ ਟੀਮ abstraction ਦੀ ਕੀਮਤ ਸਮਝਦੀਆਂ ਹਨ ਉਹ ਵੀ ਕਈ ਵਾਰ ਕੁਝ ਐਸੇ ਪੈਟਰਨ ਵਿੱਚ ਫਸ ਜਾਂਦੀਆਂ ਹਨ ਜੋ ਲੱਗਦਾ ਹੈ "ਟਿੱਪਣੀਯੋਗ" ਹੁੰਦੇ ਹਨ ਪਰ ਧੀਰੇ-ਧੀਰੇ API ਨੂੰ special-cases ਦਾ ਜਹਾਜ਼ ਬਣਾ ਦਿੰਦੇ ਹਨ। ਇੱਥੇ ਕੁਝ ਆਮ traps ਅਤੇ ਉਨ੍ਹਾਂ ਦੇ ਸਮਾਧਾਨ ਹਨ।
Feature flags rollout ਲਈ ਵਧੀਆ ਹਨ, ਪਰ ਮੁੱਦਾ ਉਸ ਵਕਤ ਸ਼ੁਰੂ ਹੁੰਦਾ ਹੈ ਜਦੋਂ flags public, ਦਿੱਲੇ-ਦਿੱਲੇ ਪਰਮੈਟਰ ਬਣ ਜਾਂਦੇ ਹਨ: ?useNewPricing=true, mode=legacy, v2=true। ਸਮੇਂ ਦੇ ਨਾਲ callers ਉਹਨਾਂ ਨੂੰ ਮਿਲਾ ਦਿੰਦੇ ਹਨ ਅਤੇ ਤੁਸੀਂ ਹਮੇਸ਼ਾਂ ਲਈ ਵੱਖ-ਵੱਖ ਵਿਹਾਰ ਸਪੋਰਟ ਕਰਨੇ ਪੈਂਦੇ ਹੋ।
ਸੁਰੱਖਿਅਤ ਦ੍ਰਿਸ਼ਟੀਕੋਣ:
ਜੇ API table IDs, join keys, ਜਾਂ "SQL-shaped" filters (ਉਦਾਹਰਣ: where=...) ਨੂੰ ਪ੍ਰਗਟ ਕਰਦਾ ਹੈ, ਤਾਂ clients ਤੁਹਾਡੇ storage ਮਾਡਲ ਨੂੰ ਸਿੱਖ ਲੈਂਦੇ ਹਨ। ਇਸ ਨਾਲ refactors ਦਰਦਨਾਕ ਬਣਾ ਦਿੰਦੇ ਹਨ: schema change API-breaking ਬਣ ਜਾਂਦਾ ਹੈ।
ਬਦਲ ਵਿੱਚ, interface ਨੂੰ domain concepts ਅਤੇ stable identifiers ਦੇ ਆਧਾਰ 'ਤੇ model ਕਰੋ। clients ਨੂੰ ਉਹ ਮੋਗਅਲ ਪੁੱਛਣ ਦਿਓ ਜੋ ਉਹ ਮਤਲਬ ਰੱਖਦੇ ਹਨ ("orders for a customer in a date range"), ਨਾ ਕਿ ਕਿੱਤਰ join ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।
ਫੀਲਡ ਜੋੜਨਾ ਨਜ਼ਰ ਵਿੱਚ harmless ਲੱਗਦਾ ਹੈ, ਪਰ ਜਦੋਂ ਕਈ ਵਾਰ "ਇੱਕ ਹੋਰ field" ਜੋੜਿਆ ਜਾਂਦਾ ਹੈ ਤਾਂ ਇਸ ਨਾਲ types ਦੀ ذمہਦਾਰੀਆਂ ਧੁੰਦਲਾਂ ਹੋ ਜਾਂਦੀਆਂ ਹਨ ਅਤੇ invariants ਨੂੰ ਕਮਜ਼ੋਰ ਕਰ ਦਿੰਦਾ ਹੈ। clients ਆਕਸਮਿਕ ਵੇਰਵਿਆਂ 'ਤੇ ਨਿਰਭਰ ਹੋਣ ਲੱਗਦੇ ਹਨ ਅਤੇ type ਇੱਕ grab-bag ਬਣ ਜਾਂਦਾ ਹੈ।
ਲੰਬੇ ਸਮੇਂ ਦੀ ਲਾਗਤ ਤੋਂ ਬਚਣ ਲਈ:
ਅਤਿ-abstract ਕਰਨ ਨਾਲ ਅਸਲੀ ਲੋੜਾਂ ਰੁਕੀ ਰਹਿ ਸਕਦੀਆਂ ਹਨ—ਜਿਵੇਂ pagination ਜੋ "start after this cursor" ਨਹੀਂ ਦਿਖਾ ਸਕਦੀ, ਜਾਂ search endpoint ਜੋ "exact match" ਨਹੀਂ ਕਰ ਸਕਦਾ। clients ਫਿਰ ਤੁਹਾਡੇ ਆਲੇ-ਦੁਰੇ workaround ਕਰਦੇ ਹਨ (multiple calls, local filtering), ਜਿਸ ਨਾਲ performance ਅਤੇ errors ਵਧਦੇ ਹਨ।
ਸਹੀ ਸੁਧਾਰ ਇਹ ਹੈ ਕਿ ਕੁਝ ਨਿਰਧਾਰਤ extension points ਦਿਓ (ਉਦਾਹਰਣ: supported filter operators) ਨਾ ਕਿ ਖੁੱਲ੍ਹਾ escape hatch।
ਸਰਲ ਬਣਾਉਣਾ ਇਹ ਨਹੀਂ ਕਿ ਸ਼ਕਤੀ ਘਟਾ ਦਿਓ। confusing options ਨੂੰ deprecate ਕਰੋ, ਪਰ underlying capability ਨੂੰ ਇੱਕ ਸਾਫ਼ ਰੂਪ ਦੁਆਰਾ ਰੱਖੋ: overlapping parameters ਨੂੰ ਇੱਕ structured request object ਨਾਲ ਬਦਲੋ, ਜਾਂ ਇੱਕ "do everything" endpoint ਨੂੰ ਦੋ cohesive endpoints ਵਿੱਚ ਵੰਡੋ। ਫਿਰ migration ਨੂੰ versioned docs ਅਤੇ ਇੱਕ ਸਪਸ਼ਟ deprecation timeline ਨਾਲ ਰਾਹ ਦਿਖਾਓ (ਵੇਖੋ /blog/evolving-apis-without-breaking-users)।
ਤੁਸੀਂ Liskov ਦੀਆਂ data abstraction ਵਿਚਾਰਾਂ ਨੂੰ ਇੱਕ ਸਧਾਰਣ, ਦੁਹਰਾਏ ਜਾ ਸਕਣ ਵਾਲੇ ਚੈੱਕਲਿਸਟ ਨਾਲ ਲਾਗੂ ਕਰ ਸਕਦੇ ਹੋ। ਮਕਸਦ ਪਰਫੈਕਸ਼ਨ ਨਹੀਂ—ਇਹ API ਦੇ ਵਾਅਦੇ ਨੂੰ explicit, testable, ਅਤੇ evolve ਕਰਨ ਯੋਗ ਬਣਾਉਣਾ ਹੈ।
ਉਪਯੋਗ ਕਰੋ ਛੋਟੇ, consistent blocks:
transfer(from, to, amount)amount > 0 ਅਤੇ accounts ਮੌਜੂਦ ਹੋਣInsufficientFunds, AccountNotFound, Timeoutਜੇ ਤੁਸੀਂ ਹੋਰ ਗਹਿਰਾਈ ਵਿੱਚ ਜਾਣਾ ਚਾਹੁੰਦੇ ਹੋ, ਤਾਂ ਵੇਖੋ: Abstract Data Types (ADTs), Design by Contract, ਅਤੇ Liskov Substitution Principle (LSP)।
ਜੇ ਤੁਹਾਡੀ ਟੀਮ ਅੰਦਰੂਨੀ ਨੋਟ ਰੱਖਦੀ ਹੈ, ਤਾਂ ਉਨ੍ਹਾਂ ਨੂੰ /docs/api-guidelines ਵਰਗੇ ਪੇਜ ਤੋਂ link ਕਰੋ ਤਾਂ ਕਿ review workflow ਦੁਬਾਰਾ ਵਰਤਣਾ ਆਸਾਨ ਰਹੇ—ਅਤੇ ਜੇ ਤੁਸੀਂ ਨਵੇਂ ਸਰਵਿਸ ਤੇਜ਼ੀ ਨਾਲ ਬਣਾਉਂਦੇ ਹੋ (ਚਾਹੇ ਹੱਥ ਨਾਲ ਜਾਂ ਇੱਕ chat-driven builder ਜਿਵੇਂ Koder.ai ਨਾਲ), ਤਾਂ ਉਹ guidelines “shipping fast” ਦਾ ਇਕ ਅਟੱਲ ਹਿੱਸਾ ਬਣਾਓ। ਭਰੋਸੇਯੋਗ ਇੰਟਰਫੇਸ ਹੀ ਉਹ ਤਰੀਕਾ ਹੈ ਜਿਸ ਨਾਲ speed compound ਕਰਦੀ ਹੈ ਨਾ ਕਿ backfire।
ਉਹਨਾਂ ਨੇ data abstraction ਅਤੇ information hiding ਨੂੰ ਪ੍ਰਸਿੱਧ ਕੀਤਾ, ਜੋ ਆਧੁਨਿਕ API ਡਿਜ਼ਾਈਨ ਨਾਲ ਸਿੱਧਾ ਜੁੜਦੇ ਹਨ: ਇੱਕ ਛੋਟਾ, ਸਥਿਰ ਠੇਕਾ ਪ੍ਰਕਾਸ਼ਿਤ ਕਰੋ ਅਤੇ ਅੰਤਰਿਮ ਲਾਜ਼ਮੀ ਤੌਰ ਤੇ ਬਦਲਣਯੋਗ ਰੱਖੋ। ਨਤੀਜਾ ਪ੍ਰਯੋਗਿਕ ਹੈ: ਘੱਟ breaking changes, ਸੁਰੱਖਿਅਤ refactors, ਅਤੇ ਭਰੋਸੇਯੋਗ ਇੰਟੇਗਰੇਸ਼ਨਾਂ।
ਇਕ ਭਰੋਸੇਯੋਗ API ਉਹ ਹੈ ਜਿਸ 'ਤੇ caller ਸਮੇਂ ਦੇ ਨਾਲ ਨਿਰਭਰ ਕਰ ਸਕਦਾ ਹੈ:
ਭਰੋਸਾ ਇਸ ਗੱਲ ਨਾਲ ਵਧਦਾ ਹੈ ਕਿ API "ਕਦੇ ਫੇਲ ਨਾ ਹੋਵੇ" ਦੀ ਬਜਾਏ ਪੇਸ਼ਗੋਈਯੋਗ ਤਰੀਕੇ ਨਾਲ ਫੇਲ ਕਰੇ ਅਤੇ ਠੇਕੇ ਦੀ ਪਾਲਣਾ ਕਰੇ।
ਇੱਕ contract ਲਿਖੋ:
ਐਜ ਕੇਸਾਂ (ਖਾਲੀ ਨਤੀਜੇ, ਡੁਪਲਿਕੇਟ, ਆਰਡਰਿੰਗ) ਨੂੰ ਵੀ ਸ਼ਾਮਲ ਕਰੋ ਤਾਂ ਕਿ callers ਤਿਆਰ ਹੋ ਕੇ ਟੈਸਟ ਕਰ ਸਕਣ।
ਇੱਕ invariant ਉਹ ਨਿਯਮ ਹੈ ਜੋ ਇੱਕ abstraction ਦੇ ਅੰਦਰ ਹਮੇਸ਼ਾ ਸੱਚ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ (ਉਦਾਹਰਣ: “quantity ਨੇਗੇਟਿਵ ਨਹੀਂ ਹੋ ਸਕਦੀ”).
ਇਹਨਾਂ ਨੂੰ ਸਰਹੱਦਾਂ 'ਤੇ ਲਾਗੂ ਕਰੋ:
ਇਸ ਨਾਲ downstream bugs ਘਟਦੇ ਹਨ ਕਿਉਂਕਿ ਸਿਸਟਮ ਅਸੰਭਵ ਅਵਸਥਾਵਾਂ ਨੂੰ ਹੱਲ ਨਹੀਂ ਕਰਦਾ।
Information hiding ਦਾ ਮਤਲਬ ਹੈ ਕਿਰਿਆਵਾਂ ਅਤੇ ਅਰਥ ਨੂੰ ਪ੍ਰਗਟ ਕਰਨਾ, ਨਾ ਕਿ ਅੰਦਰੂਨੀ ਰੂਪ-ਰੇਖਾ। ਗਾਹਕਾਂ ਨੂੰ ਉਹ ਚੀਜ਼ ਦਿਓ ਜੋ ਉਹ ਜਾਣਣਾ ਚਾਹੁੰਦੇ ਹਨ—"ਕੀ ਕਰਨਾ ਹੈ"—ਨਹੀਂ ਕਿ "ਕਿਵੇਂ ਕੀਤਾ ਗਿਆ"।
ਵ੍ਯਵਹਾਰਿਕ ਤਰੀਕੇ:
usr_...) ਬਜਾਏ ਡੇਟਾਬੇਸ row IDs ਦੇ।status=3).ਇਹ ਉਹਨਾਂ ਨੂੰ ਤੁਹਾਡੀ ਇੰਪਲੀਮੇੰਟੇਸ਼ਨ 'ਤੇ ਨਿਰਭਰ ਕਰਨ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ, ਜਿਸ ਨਾਲ ਤੁਸੀਂ ਬਾਅਦ 'ਚ ਬਦਲਾਅ ਨਹੀਂ ਕਰ ਸਕੋਗੇ। ਜੇ ਗਾਹਕ ਡੇਟਾਬੇਸ-ਸ਼ੈਪਡ ਫਿਲਟਰ ਜਾਂ ਅੰਦਰੂਨੀ IDs 'ਤੇ ਨਿਰਭਰ ਕਰਦੇ ਹਨ, ਤਾਂ schema refactor API-breaking ਬਣ ਜਾਂਦਾ ਹੈ।
ਇਸ ਲਈ domain-ਕੇਂਦ੍ਰਿਤ ਸਵਾਲ ਪੁੱਛੋ: “customer ਲਈ orders ਕਿਸ ਤਾਰੀਖ ਰੇਂਜ ਵਿੱਚ,” ਨਾ ਕਿ “ਕਿਵੇਂ join ਕਰਨਾ ਹੈ।”
LSP ਦਾ ਅਰਥ: ਜੇ ਕੋਡ ਇੱਕ interface ਨਾਲ ਚੱਲਦਾ ਹੈ, ਤਾਂ ਕਿਸੇ ਵੀ ਵੈਧ implementation ਨਾਲ ਉਹ ਬਿਨਾਂ ਖਾਸ ਕੇਸਾਂ ਦੇ ਚੱਲਣਾ ਚਾਹੀਦਾ ਹੈ। API ਵਿੱਚ ਇਹ ਮਤਲਬ ਹੈ: caller ਨੂੰ ਹੈਰਾਨ ਨਾ ਕਰੋ।
ਵਿਸ਼ੇਸ਼ ਤੌਰ 'ਤੇ, ਇਹ ਸਥਾਪਤ ਕਰਦਾ ਹੈ ਕਿ ਹਰ implementation ਨੂੰ ਇਹ promise ਰੱਖਣੀ ਚਾਹੀਦੀ ਹੈ—ਚਾਹੇ ਅੰਦਰੂਨੀ ਤਰੀਕਾ ਕਿੰਨਾ ਵੱਖਰਾ ਹੋਵੇ।
ਧਿਆਨ ਦਿਓ:
ਜੇ ਇੱਕ implementation ਨੂੰ ਵਧੇਰੇ ਕਠੋਰ ਨਿਯਮ ਦੀ ਲੋੜ ਹੈ, ਤਾਂ ਵੱਖਰਾ interface ਦਿੱਤਾ ਜਾਏ ਜਾਂ capability explicit ਕਰਕੇ ਦਿਆਂ, ਤਾਂ ਕਿ clients ਆਗਿਆ ਨਾਲ opt-in ਕਰਨ।
ਛੋਟੀ, ਸਪੱਸ਼ਟ APIs ਵਰਤਣਾ ਆਸਾਨ ਹੁੰਦਾ ਹੈ ਕਿਉਂਕਿ ਉਹ caller ਨੂੰ ਸਿਰਫ਼ ਲੋੜੀਂਦੇ ਕੰਮ ਦਿਖਾਉਂਦੇ ਹਨ—ਕੁਝ ਹੀ ਕਮਾਂਡ ਅਤੇ ਸਮਝਦਾਰ ਨਾਮ।
ਰਾਹਦਾਰੀ:
options: any ਜਾਂ booleans ਦੇ ਢੇਰ ਨਾਲ ambiguity ਬਣਦੀ ਹੈ—ਇਸ ਦੀ ਥਾਂ ਸਪੱਸ਼ਟ ਮੈਥਡ ਜਾਂ ਛੋਟਾ typed options object ਦਿਓ।ਤੁਹਾਡਾ contract ਤੈਅ ਕਰੇ ਕਿ ਕਦੋਂ programmer error ਹੈ ਅਤੇ ਕਦੋਂ runtime failure:
ਸਥਿਰ error codes/structured fields ਦਿਓ ਤਾਂ ਕਿ tests message strings 'ਤੇ ਨਿਰਭਰ ਨਾ ਕਰਨ।
Document retry safety ਅਤੇ idempotency (idempotency keys) ਅਤੇ batch operations ਲਈ partial success ਦੀ ਰਿਪੋਰਟਿੰਗ।