跳至主要內容

应用 SOLID 原则

Mr.He大约 4 分钟

应用 SOLID 原则

单一职责原则 (SRP)

单一职责原则的定义是每个类应该只有一个职责,也就是只做一件事。

将功能较多的组件拆分为较小的组件

const ActiveUsersList = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const loadUsers = async () => {
      const response = await fetch("/some-api");
      const data = await response.json();
      setUsers(data);
    };

    loadUsers();
  }, []);

  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  return (
    <ul>
      {users
        .filter((user) => !user.isBanned && user.lastActivityAt >= weekAgo)
        .map((user) => (
          <li key={user.id}>
            <img src={user.avatarUrl} />
            <p>{user.fullName}</p>
            <small>{user.role}</small>
          </li>
        ))}
    </ul>
  );
};
// 获取数据
const useUsers = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const loadUsers = async () => {
      const response = await fetch("/some-api");
      const data = await response.json();
      setUsers(data);
    };

    loadUsers();
  }, []);

  return { users };
};

// 列表渲染
const UserItem = ({ user }) => {
  return (
    <li>
      <img src={user.avatarUrl} />
      <p>{user.fullName}</p>
      <small>{user.role}</small>
    </li>
  );
};

// 列表过滤
const getOnlyActive = (users) => {
  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  return users.filter(
    (user) => !user.isBanned && user.lastActivityAt >= weekAgo
  );
};

const useActiveUsers = () => {
  const { users } = useUsers();

  const activeUsers = useMemo(() => {
    return getOnlyActive(users);
  }, [users]);

  return { activeUsers };
};

const ActiveUsersList = () => {
  const { activeUsers } = useActiveUsers();

  return (
    <ul>
      {activeUsers.map((user) => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
};

组件只做了最少的事情——渲染它从 Hook 中获取的数据。

遵循单一职责原则,我们有效地采用了大量独立的代码并使其更加模块化,模块化的代码更容易测试和维护。

开放封闭原则(OCP)

开放封闭原则指出“一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭”。

const Header = () => {
  const { pathname } = useRouter();

  return (
    <header>
      <Logo />
      <Actions>
        {pathname === "/dashboard" && (
          <Link to="/events/new">Create event</Link>
        )}
        {pathname === "/" && <Link to="/dashboard">Go to dashboard</Link>}
      </Actions>
    </header>
  );
};

const HomePage = () => (
  <>
    <Header />
    <OtherHomeStuff />
  </>
);

const DashboardPage = () => (
  <>
    <Header />
    <OtherDashboardStuff />
  </>
);

如果需要将这个 Header 组件添加到更多的页面中会发生什么呢?每次创建新页面时,都需要引用 Header 组件,并修改其内部实现。这种方式使得 Header 组件与使用它的上下文紧密耦合,并且违背了开放封闭原则。

// 可以使用组合将想要的任何内容放在Header中,而无需修改组件本身。
const Header = ({ children }) => (
  <header>
    <Logo />
    <Actions>{children}</Actions>
  </header>
);

const HomePage = () => (
  <>
    <Header>
      <Link to="/dashboard">Go to dashboard</Link>
    </Header>
    <OtherHomeStuff />
  </>
);

const DashboardPage = () => (
  <>
    <Header>
      <Link to="/events/new">Create event</Link>
    </Header>
    <OtherDashboardStuff />
  </>
);

里氏替换原则(LSP)

里氏替换原则可以理解为对象之间的一种关系,子类型对象应该可以替换为超类型对象。

使用继承编写 React 代码会使代码变得糟糕

接口隔离原则(ISP)

根据接口隔离原则的说法,客户端不应该依赖它不需要的接口。

type Video = {
  title: string;
  duration: number;
  coverUrl: string;
};

type Props = {
  items: Array<Video>;
};

const VideoList = ({ items }) => {
  return (
    <ul>
      {items.map((item) => (
        <Thumbnail key={item.title} video={item} />
      ))}
    </ul>
  );
};

Thumbnail 组件的实现

type Props = {
  video: Video;
};

const Thumbnail = ({ video }: Props) => {
  return <img src={video.coverUrl} />;
};

下面来定义直播的类型 LiveStream

type LiveStream = {
  name: string;
  previewUrl: string;
};

我们可以轻松的区分视频和直播对象,但是不能将后者传递给 Thumbnail 组件,因为 Video 和 LiveStream 类型不兼容。

它们包含了不同的属性来保存缩略图:视频对象调用 coverUrl,直播对象调用 previewUrl。

type Props = {
  items: Array<Video | LiveStream>;
};

const VideoList = ({ items }) => {
  return (
    <ul>
      {items.map((item) => {
        if ("coverUrl" in item) {
          return <Thumbnail video={item} />;
        } else {
          // 直播组件,该怎么写?
        }
      })}
    </ul>
  );
};

重构 Thumbnail 组件以确保它仅依赖于它需要的 props

type Props = {
  coverUrl: string;
};

const Thumbnail = ({ coverUrl }: Props) => {
  return <img src={coverUrl} />;
};
type Props = {
  items: Array<Video | LiveStream>;
};

const VideoList = ({ items }) => {
  return (
    <ul>
      {items.map((item) => (
        <Thumbnail
          coverUrl={"coverUrl" in item ? item.coverUrl : item.previewUrl}
        />
      ))}
    </ul>
  );
};

接口隔离原则主张最小化系统组件之间的依赖关系,使它们的耦合度降低,从而提高可重用性。

依赖倒置原则(DIP)

依赖倒置原则指出“要依赖于抽象,不要依赖于具体”。换句话说,一个组件不应该直接依赖于另一个组件,而是它们都应该依赖于一些共同的抽象。

import api from "~/common/api";

const LoginForm = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = async (evt) => {
    evt.preventDefault();
    await api.login(email, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Log in</button>
    </form>
  );
};

在这段代码中,LoginForm 组件直接引用了 api 模块,因此它们之间存在紧密耦合。

type Props = {
  onSubmit: (email: string, password: string) => Promise<void>;
};

const LoginForm = ({ onSubmit }: Props) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = async (evt) => {
    evt.preventDefault();
    await onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Log in</button>
    </form>
  );
};

通过这样修改,LoginForm 组件不再依赖于 api 模块。 向 API 提交凭证的逻辑是通过 onSubmit 回调函数抽象出来的,现在由父组件负责提供该逻辑的具体实现。

import api from "~/common/api";

const ConnectedLoginForm = () => {
  const handleSubmit = async (email, password) => {
    await api.login(email, password);
  };

  return <LoginForm onSubmit={handleSubmit} />;
};

ConnectedLoginForm 组件充当 api 和 LoginForm 之间的粘合剂,而它们本身保持完全独立。

依赖倒置原则旨在最小化应用程序不同组件之间的耦合。