The adjoint method is a particular way of finding derivatives of the solution to the PDE with respect to the parameters $p$. In the adjoint method you end up solving a second PDE boundary value problem (the adjoint of the original problem) to find these derivatives. You can then get a gradient of $f$ with respect to $p$ by applying the chain rule.
This can be contrasted with the brute force alternative of solving the PDE boundary value problem for perturbed values of the parameters and then computing the derivatives by finite difference approximation. If the vector $p$ has $m$ parameters, then finite differencing requires you to solve the original PDE $m+1$ times, while the adjoint method requires only one solution of the original PDE and one solution of its adjoint. Thus the adjoint method can be vastly more efficient.
Note that in practice both the original PDE and the adjoint equation have to be discretized by some kind of finite difference or finite element method.
You can in theory implement the adjoint method by finding the adjoint equation and then separately discretizing the original PDE and its adjoint problem. In practice it is often better for numerical accuracy to find the adjoint of the discretized version of the original PDE boundary value problem.
Some Automatic Differentiation (AD) tools support simultaneous development of solvers for the PDE boundary value problem and its adjoint. The right tools for this will depend very much on your computing environment (programming language, parallelism, etc.)