React组件设计原则-依赖倒置原则(DIP)

依赖倒置原则(DIP)

始终具有抽象的高级代码接口,而不是实现细节

DIP 告诉我们,我们应该始终通过抽象与低级细节进行交互,从而“将布线隐藏在墙后”。[13]这与上面详述的 SRP 和 ISP 有很强的联系。实际上,对于 React 前端,这意味着我们高级代码中的函数不应该关心特定任务是如何完成的。例如,假设我们想调用一个 API 来获取待办事项列表——就像我们在上面的 SRP 部分中所做的一样:

const TodosPage = () => {
    const todos = useTodos();

    const renderTodos = () => {
        return todos.map(todo => {
            return <Todoitem id={todo.id} title={todo.title} />
        });
    };

    return (
        <div>
            <h1>My Todos:</h1>
            <ul>
                {renderTodos()}
            </ul>
        </div>
    )
};

关心那些是如何从哪里来的吗?不!它只与隐藏了很多内部连接的函数交互。这使我们的代码更易于阅读,因为我们可以一眼看出函数的用途,以及它在整个组件中的使用方式。这与 SRP 密切相关,因为当我们从功能/组件中提取功能时,我们必然使我们的高级代码接口具有抽象。如果我们想改变获取这些待办事项的方式(例如使用API 而不是),我们可以在不进入组件的情况下做到这一点。<TodosPage />todosuseTodos()useTodos()<TodosPage />fetchaxios<TodosPage />

function useTodos(){
    const [todos, setTodos] = useState([]);

    useEffect(() => {

      // Refactored to use fetch() instead of axios.get() to call an API
        async function getTodosWithFetch() {
            const response = await fetch("https://jsonplaceholder.typicode.com/todos");
            const data = await response.json();
            setTodos(data);
        };
        getTodosWithFetch();
    }, []);

    return todos;
};

我们更改了实现细节(我们如何完成特定任务),但是我们不必更改. 事实上,我们可以更进一步。<TodosPage />

// useTodos.js
import localTodos from "./todos.json";
function useTodos(){
    const data = localTodos.todos;
    return todos;
};
export default useTodos;

// TodosPage.js
import useTodos from "./useTodos.js";

const TodosPage = () => {
    const todos = useTodos();

    const renderTodos = () => {
        return todos.map(todo => {
            return <Todoitem id={todo.id} title={todo.title} />
        });
    };

    return (
        <div>
            <h1>My Todos:</h1>
            <ul>
                {renderTodos()}
            </ul>
        </div>
    )
};

现在,我们甚至不调用 API 来获取我们的待办事项!todos.json相反,我们从一些名为;的本地文件中读取它们。我们已经疯狂地更改了一个实现细节,但是,因为我们只依赖于函数的抽象来处理实际获取待办事项的细节,所以我们不必更改.useTodos()<TodosPage />

抽象层

我们怎么知道什么时候我们已经远离“高级”代码而开始担心实现细节了?好吧,罗伯特·马丁再次给出了一些见解:

“函数有一个基本规则:函数的每一行都应该处于同一抽象级别,并且该级别应该低于函数名称。” - Robert C Martin,干净的代码,第 1 课 [5]

这意味着我们应该慢慢地抽象出功能,直到我们达到足够低的水平。这是一个有点模糊的定义,依赖于程序员的经验来了解每个概念的高级程度。抽象不够远的一个例子可能是:

const TodosList = () => {
  const [todos, setTodos] = useState([]);
  const [term, setTerm] = useState("");

  useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const filtered = data.filter(todo => todo.completed === false);
          const pattern = new RegExp(term, "g");
          const searched = filtered.filter(todo => pattern.test(todo.title));
          setTodos(searched);
      };
      getTodos();
  }, [term]);


  const renderTodos = () => {
      return todos.map(todo => {
          return (
              <li>
                  {`ID: ${todo.id}, Title: ${todo.title}`}
              </li>
          )
      });
  };

  return(
    <div>
    <input value={term} onChange={(e) => setTerm(e.target.value)} />
      <ul>
        {renderTodos()}
      </ul>
    </div>
  );
}

在这里,我们有很多实现细节隐藏在. 这使得很难判断发生了什么。然而,通过一些好的函数命名和一些抽象,我们可以解决这个问题。<TodosPage />

const TodosList = () => {
  const [term, setTerm] = useState("");
  const todos = useTodos(term);

  const renderTodos = () => {
      return todos.map(todo => {
          return (
              <li>
                  {`ID: ${todo.id}, Title: ${todo.title}`}
              </li>
          )
      });
  };

  return(
    <div>
    <input value={term} onChange={(e) => setTerm(e.target.value)} />
      <ul>
        {renderTodos()}
      </ul>
    </div>
  );
};

function useTodos(term){
    const [todos, setTodos] = useState([]);

    useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const filtered = data.filter(todo => todo.completed === false);
          const pattern = new RegExp(term, "g");
          const searched = filtered.filter(todo => pattern.test(todo.title));
          setTodos(searched);
      };
      getTodos();
    }, [term]);

    return todos;
};

所以,这要好一些——我们已经从中提取了细节,并最终得到了与我们之前提出的类似的解决方案。但是,我们似乎有一个重载的功能。我们有一些相对较高层次的抽象概念(例如网络请求)与相对较低层次的概念(例如模式匹配或数组操作)具有相同的功能。正如 Robert Martin 所说,“这很粗鲁!程序员在一行中将你从高度[高级概念]带到深度[低级概念] 。” [5]或许我们可以再分解一下:<TodosPage />useTodos()

function useTodos(term){
    const [todos, setTodos] = useState([]);

    useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const filteredAndMatchedTodos = filterAndMatchTodos(data, term);
          setTodos(filteredAndMatchedTodos);
      };
      getTodos();
    }, [term]);

    return todos;
};

function filterAndMatchTodos(todoList, searchTerm){
  const completedTodos = todoList.filter(todo => todo.completed === false);
  const pattern = new RegExp(searchTerm, "g");
  const matchingTodos = filtered.filter(todo => pattern.test(todo.title));
  return matchingTodos;
}

好的,现在看起来好多了。我们的函数已经从中提取了低级过滤/正则表达式匹配工作。这使得一切都处于相似的抽象级别;它处理网络请求和一些 React 状态管理,但不关心模式匹配等更底层的细节。此外,我们的功能对更广泛的网络请求或 React 不感兴趣;它只关心它会收到一个待办事项数组,它应该过滤已完成的待办事项,然后根据给定的搜索词对已完成的待办事项进行模式匹配。这已经使我们的整个程序更易于测试,因为我们已经将数组操作与网络请求分离。useTodos()useTodos()filterAndMatchTodos()

我们可以开始进一步抽象...

function filterAndMatchTodos(todoList, searchTerm){
  const completedTodos = filterTodoList(todoList);
  const matchingTodos = matchFilteredTodos(completedTodos, searchTerm); 
  return matchingTodos;
}

function filterTodoList(todoList){
  return todoList.filter(todo => todo.completed === false);
};

function matchFilteredTodos(compeltedTodos, searchTerm){
    const pattern = new RegExp(searchTerm, "g");
    const matchingTodos = compeltedTodos.filter(todo => pattern.test(todo.title));
    return matchingTodos;
};

...然而,在某些时候,必须对事情何时走得太远做出判断。也许上面的内容适合你,或者你觉得它在抽象层上走得太远了(例如,实际上filterTodoList只是调用函数,因此在我看来为此创建一个新函数似乎是多余的)。作为回应我多次重复的信息:这些是指南;在对您的代码产生积极影响的范围内使用它们Array.filter()

相关标签:
  • React组件设计原则
0人点赞

发表评论

当前游客模式,请登陆发言

所有评论(0)