exaop

所属分类:工具库
开发工具:Elixir
文件大小:0KB
下载次数:0
上传日期:2020-08-06 19:33:47
上 传 者sh-1993
说明:  面向方面编程的最小长生不老库。,
(A minimal elixir library for aspect-oriented programming.,)

文件列表:
.formatter.exs (252, 2020-08-06)
lib/ (0, 2020-08-06)
lib/exaop.ex (8945, 2020-08-06)
lib/exaop/ (0, 2020-08-06)
lib/exaop/checker.ex (730, 2020-08-06)
lib/exaop/preprocessor.ex (364, 2020-08-06)
lib/exaop/setter.ex (584, 2020-08-06)
mix.exs (997, 2020-08-06)
mix.lock (2257, 2020-08-06)
spec/ (0, 2020-08-06)
spec/exaop_spec.exs (14397, 2020-08-06)
spec/spec_helper.exs (41, 2020-08-06)

# Exaop [![Build Status](https://github.com/nobrick/exaop/workflows/CI/badge.svg)](https://github.com/nobrick/exaop/actions?query=workflow%3ACI) A minimal elixir library for aspect-oriented programming. ## Installation Add `exaop` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:exaop, "~> 0.1"} ] end ``` ## Usage Unlike common AOP patterns, Exaop does not introduce any additional behavior to existing functions, as it may bring complexity and make the control flow obscured. Elixir developers prefer explicit over implicit, thus invoking the cross-cutting behavior by simply calling the plain old function generated by pointcut definitions is better than using some magic like module attributes and macros to decorate and weave a function. ### Hello World Use Exaop in a module, then define some pointcuts to separate the cross-cutting logic: ```elixir defmodule Foo do use Exaop check :validity set :compute end ``` When you compile the file, the following warnings would occur: ``` warning: function check_validity/3 required by behaviour Foo.ExaopBehaviour is not implemented (in module Foo) foo.exs:1: Foo (module) warning: function set_compute/3 required by behaviour Foo.ExaopBehaviour is not implemented (in module Foo) foo.exs:1: Foo (module) ``` It reminds you to implement the corresponding callbacks required by your pointcut definitions: ```elixir defmodule Foo do use Exaop check :validity set :compute @impl true def check_validity(%{b: b} = _params, _args, _acc) do if b == 0 do {:error, :divide_by_zero} else :ok end end @impl true def set_compute(%{a: a, b: b} = _params, _args, acc) do Map.put(acc, :result, a / b) end end ``` A function `__inject__/2` is generated in the above module `Foo`. When it is called, the callbacks are triggered in the order defined by your pointcut definitions. Throughout the execution of the pointcut callbacks, an accumulator is passed and updated after running each callback. The execution process may be halted by a return value of a callback. If the execution is not halted by any callback, the final accumulator value is returned by the `__inject__/2` function. Otherwise, the return value of the callback that terminates the entire execution process is returned. In the above example, the value of the accumulator is returned if the `check_validity` is passed: ```elixir iex> params = %{a: 1, b: 2} iex> initial_acc = %{} iex> Foo.__inject__(params, initial_acc) %{result: 0.5} ``` The halted error is returned if the execution is aborted: ```elixir iex> params = %{a: 1, b: 0} iex> initial_acc = %{} iex> Foo.__inject__(params, initial_acc) {:error, :divide_by_zero} ``` ### Pointcut definitions ```elixir check :validity set :compute ``` We've already seen the pointcut definitions in the example before. `check_validity/3` and `set_compute/3` are the pointcut callback functions required by these definitions. Additional arguments can be set: ```elixir check :validity, some_option: true set :compute, {:a, :b} ``` ### Pointcut callbacks #### Naming and arguments All types of pointcut callbacks have the same function signature. Each callback function following the naming convention in the example, using an underscore to connect the pointcut type and the following atom as the callback function name. Each callback has three arguments and each argument can be of any Elixir term. The first argument of the callback function is passed from the first argument of the caller `__inject__/2`. The argument remains unchanged in each callback during the execution process. The second argument of the callback function is passed from its pointcut definition, for example, `set :compute, :my_arg` passes `:my_arg` as the second argument of its callback function `set_compute/3`. The third argument is the accumulator. It is initialized as the second argument of the caller `__inject__/2`. The value of accumulator is updated or remains the same after each callback execution, depending on the types and the return values of the callback functions. #### Types and behaviours Each kind of pointcut has different impacts on the execution process and the accumulator. - `check` - does not change the value of the accumulator. - the execution of the generated function is halted if its callback return value matches the pattern `{:error, _}`. - the execution continues if its callback returns `:ok`. - `set` - does not halt the execution process. - sets the accumulator to its callback return value. - `preprocess` - allows to change the value of the accumulator or halt the execution process. - the execution of the generated function is halted if its callback return value matches the pattern `{:error, _}`. - the accumulator is updated to the wrapped `acc` if its callback return value matches the pattern `{:ok, acc}`. View documentation of these macros for details. ### A more in-depth example Exaop is ready for production and makes complex application workflows simple and self-documenting. In practice, we combine it with some custom simple macros as a method to separate cross-cutting concerns and decouple business logic. Note that we do not recommend overusing it, it is only needed when the workflow gets complicated, and the pointcuts should be strictly restricted to the domain of cross-cutting logic, not the business logic body itself. Here's a more complex example, a wallet balance transfer. The configuration loading, context setting and transfer validations are separated, but the main transfer logic remains untouched. The example also introduces an external callback, which is defined in a module other than its pointcut definition. ```elixir defmodule Wallet do @moduledoc false use Exaop alias Wallet.AML require Logger ## Definitions for cross-cutting concerns set :config, [:max_allowed_amount, :fee_rate] set :accounts check :amount, guard: :positive check :amount, guard: {:lt_or_eq, :max_allowed_amount} check :recipient, :not_equal_to_sender check AML set :fee check :balance @doc """ A function injected by explicitly calling __inject__/2 generated by Exaop. """ def transfer(%{from: _, to: _, amount: _} = info) do info |> __inject__(%{}) |> handle_inject(info) end defp handle_inject({:error, _} = error, info) do Logger.error("transfer failed", error: error, info: info) end defp handle_inject(_acc, info) do # Put the actual transfer logic here: # Wallet.transfer!(acc, info) Logger.info("transfer validated and completed", info: info) end ## Setters required by the above concern definitions. @impl true def set_accounts(%{from: from, to: to}, _args, acc) do balances = %{"Alice" => 100, "Bob" => 30} acc |> Map.put(:sender_balance, balances[from]) |> Map.put(:recipient_balance, balances[to]) end @impl true def set_config(_params, keys, acc) do keys |> Enum.map(&{&1, Application.get_env(:my_app, &1, default_config(&1))}) |> Enum.into(acc) end defp default_config(key) do Map.get(%{fee_rate: 0.01, max_allowed_amount: 1_000}, key) end @impl true def set_fee(%{amount: amount}, _args, %{fee_rate: fee_rate} = acc) do Map.put(acc, :fee, amount * fee_rate) end ## Checkers required by the above concern definitions. @impl true def check_amount(%{amount: amount}, args, acc) do args |> Keyword.fetch!(:guard) |> do_check_amount(amount, acc) end defp do_check_amount(:positive, amount, _acc) do if amount > 0 do :ok else {:error, :amount_not_positive} end end defp do_check_amount({:lt_or_eq, key}, amount, acc) when is_atom(key) do max = Map.fetch!(acc, key) if max && amount <= max do :ok else {:error, :amount_exceeded} end end @impl true def check_recipient(%{from: from, to: to}, :not_equal_to_sender, _acc) do if from == to do {:error, :invalid_recipient} else :ok end end @impl true def check_balance(%{amount: amount}, _args, %{fee: fee, sender_balance: balance}) do if balance >= amount + fee do :ok else {:error, :insufficient_balance} end end end defmodule Wallet.AML do @moduledoc """ A module defining external Exaop callbacks. """ @behaviour Exaop.Checker @aml_blacklist ~w(Trump) @impl true def check(%{from: from, to: to}, _args, _acc) do cond do from in @aml_blacklist -> {:error, {:aml_check_failed, from}} to in @aml_blacklist -> {:error, {:aml_check_failed, to}} true -> :ok end end end ``` ## License [The MIT License](https://github.com/nobrick/exaop/blob/master/LICENSE)

近期下载者

相关文件


收藏者