Minimize API surface that is templated for `CommandHandler` (#33620)

* Define a EncodeToTLV inteface for the command handler

* Slight documentation update

* Comments update
diff --git a/src/app/CommandHandler.h b/src/app/CommandHandler.h
index cce1987..96b7af8 100644
--- a/src/app/CommandHandler.h
+++ b/src/app/CommandHandler.h
@@ -57,6 +57,32 @@
 namespace chip {
 namespace app {
 
+/// Defines an abstract class of something that can be encoded
+/// into a TLV with a given data tag
+class EncoderToTLV
+{
+public:
+    virtual ~EncoderToTLV() = default;
+
+    virtual CHIP_ERROR Encode(TLV::TLVWriter &, TLV::Tag tag) = 0;
+};
+
+/// An `EncoderToTLV` the uses `DataModel::Encode` to encode things.
+///
+/// Generally useful to encode things like <ClusterName>::Commands::<CommandName>::Type
+/// structures.
+template <typename T>
+class DataModelEncoderToTLV : public EncoderToTLV
+{
+public:
+    DataModelEncoderToTLV(const T & value) : mValue(value) {}
+
+    virtual CHIP_ERROR Encode(TLV::TLVWriter & writer, TLV::Tag tag) { return DataModel::Encode(writer, tag, mValue); }
+
+private:
+    const T & mValue;
+};
+
 class CommandHandler
 {
 public:
@@ -325,14 +351,37 @@
      * @param [in] aRequestCommandPath the concrete path of the command we are
      *             responding to.
      * @param [in] aData the data for the response.
+     *
+     * NOTE: this is a convenience function for `AddResponseDataViaEncoder`
      */
     template <typename CommandData>
-    CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData)
+    inline CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData)
+    {
+        DataModelEncoderToTLV<CommandData> encoder(aData);
+        return AddResponseDataViaEncoder(aRequestCommandPath, CommandData::GetCommandId(), encoder);
+    }
+
+    /**
+     * API for adding a data response.  The encoded is generally expected to encode
+     * a ClusterName::Commands::CommandName::Type struct, but any
+     * object should work.
+     *
+     * @param [in] aRequestCommandPath the concrete path of the command we are
+     *             responding to.
+     * @param [in] commandId the command whose content is being encoded.
+     * @param [in] encoder - an encoder that places the command data structure for `commandId`
+     *             into a TLV Writer.
+     *
+     * Most applications are likely to use `AddResponseData` as a more convenient
+     * one-call that auto-sets command ID and creates the underlying encoders.
+     */
+    CHIP_ERROR AddResponseDataViaEncoder(const ConcreteCommandPath & aRequestCommandPath, CommandId commandId,
+                                         EncoderToTLV & encoder)
     {
         // Return early when response should not be sent out.
         VerifyOrReturnValue(ResponsesAccepted(), CHIP_NO_ERROR);
-
-        return TryAddingResponse([&]() -> CHIP_ERROR { return TryAddResponseData(aRequestCommandPath, aData); });
+        return TryAddingResponse(
+            [&]() -> CHIP_ERROR { return TryAddResponseDataViaEncoder(aRequestCommandPath, commandId, encoder); });
     }
 
     /**
@@ -350,9 +399,21 @@
      * @param [in] aData the data for the response.
      */
     template <typename CommandData>
-    void AddResponse(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData)
+    inline void AddResponse(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData)
     {
-        if (AddResponseData(aRequestCommandPath, aData) != CHIP_NO_ERROR)
+        DataModelEncoderToTLV<CommandData> encoder(aData);
+        return AddResponseViaEncoder(aRequestCommandPath, CommandData::GetCommandId(), encoder);
+    }
+
+    /**
+     * API for adding a response with a given encoder of TLV data.
+     *
+     * The encoder would generally encode a ClusterName::Commands::CommandName::Type with
+     * the corresponding `GetCommandId` call.
+     */
+    void AddResponseViaEncoder(const ConcreteCommandPath & aRequestCommandPath, CommandId commandId, EncoderToTLV & encoder)
+    {
+        if (AddResponseDataViaEncoder(aRequestCommandPath, commandId, encoder) != CHIP_NO_ERROR)
         {
             AddStatus(aRequestCommandPath, Protocols::InteractionModel::Status::Failure);
         }
@@ -630,7 +691,6 @@
         return PrepareInvokeResponseCommand(aResponseCommandPath, prepareParams);
     }
 
-    // TODO(#31627): It would be awesome if we could remove this template all together.
     /**
      * If this function fails, it may leave our TLV buffer in an inconsistent state.
      * Callers should snapshot as needed before calling this function, and roll back
@@ -640,26 +700,14 @@
      *             responding to.
      * @param [in] aData the data for the response.
      */
-    template <typename CommandData>
-    CHIP_ERROR TryAddResponseData(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData)
+    CHIP_ERROR TryAddResponseDataViaEncoder(const ConcreteCommandPath & aRequestCommandPath, CommandId commandId,
+                                            EncoderToTLV & encoder)
     {
-        // This method, templated with CommandData, captures all the components needs
-        // from CommandData with as little code as possible.
-        //
-        // Previously, non-essential code was unnecessarily templated, leading to
-        // compilation and duplication N times. By isolating only the code segments
-        // that genuinely require templating, minimizes duplicate compiled code.
-        ConcreteCommandPath responseCommandPath = { aRequestCommandPath.mEndpointId, aRequestCommandPath.mClusterId,
-                                                    CommandData::GetCommandId() };
+        ConcreteCommandPath responseCommandPath = { aRequestCommandPath.mEndpointId, aRequestCommandPath.mClusterId, commandId };
         ReturnErrorOnFailure(TryAddResponseDataPreEncode(aRequestCommandPath, responseCommandPath));
         TLV::TLVWriter * writer = GetCommandDataIBTLVWriter();
         VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE);
-        ReturnErrorOnFailure(DataModel::Encode(*writer, TLV::ContextTag(CommandDataIB::Tag::kFields), aData));
-
-        // FinishCommand technically should be refactored out as it is not a command that needs templating.
-        // But, because there is only a single function call, keeping it here takes less code. If there is
-        // ever more code between DataModel::Encode and the end of this function, it should be broken out into
-        // TryAddResponseDataPostEncode.
+        ReturnErrorOnFailure(encoder.Encode(*writer, TLV::ContextTag(CommandDataIB::Tag::kFields)));
         return FinishCommand(/* aEndDataStruct = */ false);
     }