洗您的代码:命名很难
#javascript #编程 #cleancode

您阅读了我即将出版的《干净代码》一书的摘录,“清洗您的代码:写一次,阅读七次。


我们都知道,命名是编程中最困难的问题之一,当我们刚刚开始编程时,我们大多数人可能已经写了这样的代码:

            // reading file signature
try
        AssignFile(fp, path+sr.Name);
                    Reset(fp, 1);
    if FileSize(fp) < sizeof(buf) then
                            continue
    else
                            BlockRead(fp, buf, sizeof(buf));
    CloseFile(fp);
except
    on E : Exception do
    begin
            ShowError(E.Message+#13#10+'('+path+sr.Name+')');
        continue;
    end; // on
end; // try
// compare
for i:=0 to FormatsCnt do
begin
    if AnsiStartsStr(Formats[i].Signature, buf) then
    begin
            // Check second signature
        if (Formats[i].Signature2Offset>0) then
            if Formats[i].Signature2Offset <> Pos(Formats[i].Signature2, buf) then
                    continue;
            // Check extension
        found := false;
        ext := LowerCase(ExtractFileExt(sr.Name));
        for j:=0 to High(Formats[i].Extensions) do
        begin
            if ext='.'+Formats[i].Extensions[j] then
            begin
                    found := true;
                break;
            end; // if
        end; // for j
        if found then
            break;
        // ..
    end;
end;

我在20年前在德尔福(Delphi)编写了此代码,老实说,我真的不记得该应用程序应该做什么。它具有所有内容:单个字符名称(ij),缩写(...Cntbuf),首字母缩写词(Esrfp)。它有一些评论! (我保留了原始的凹痕以完全沉浸。)

我曾经与一个非常高级的高级开发人员合作,他大多使用了很短的名字,并且从未写过任何评论或测试。使用他们的代码就像与汇编器一起工作非常困难。通常,我们在此代码中浪费日子跟踪和修复错误。

让我们看一下这些(以及许多其他)命名的对立面,以及如何修复它们。

负面的布尔人并不难阅读

考虑此方法:

validateInputs(values) {
  let noErrorsFound = true;
  const errorMessages = [];

  if (!values.firstName) {
    errorMessages.push('First name is required');
    noErrorsFound = false;
  }
  if (!values.lastName) {
    errorMessages.push('Last name is required');
    noErrorsFound = false;
  }

  if (!noErrorsFound) {
    this.set('error_message', errorMessages);
  }

  return noErrorsFound;
}

我可以对此代码说很多话,但让我们首先专注于这一行:

if (!noErrorsFound) {

双重否定,如果没有发现错误……我的大脑发痒,我几乎想拿一个红色标记,开始在我的屏幕上划出!s and nos,以便能够阅读代码。

在大多数情况下,我们可以通过将负面布尔转换为积极的boole来显着提高代码的可读性:

validateInputs(values) {
  let errorsFound = false;
  const errorMessages = [];

  if (!values.firstName) {
    errorMessages.push('First name is required');
    errorsFound = true;
  }
  if (!values.lastName) {
    errorMessages.push('Last name is required');
    errorsFound = true;
  }

  if (errorsFound) {
    this.set('error_message', errorMessages);
  }

  return !errorsFound;
}

积极的名称和积极条件通常比负面的更容易阅读。

到这个时候,我们应该已经注意到我们根本不需要errorsFound变量:它的值始终可以从errorMessages阵列派生:

validateInputs(values) {
  const errorMessages = [];

  if (!values.firstName) {
    errorMessages.push('First name is required');
  }
  if (!values.lastName) {
    errorMessages.push('Last name is required');
  }

  if (errorMessages.length > 0) {
    this.set('error_message', errorMessages);
    return false;
  }

  return true;
}

我还将此方法分为两种,以隔离副作用并使代码更可测试,然后删除this.set()调用的条件 - 设置一个空数组时,当没有错误似乎很安全时:

getErrorMessages(values) {
  const errorMessages = [];

  if (!values.firstName) {
    errorMessages.push('First name is required');
  }
  if (!values.lastName) {
    errorMessages.push('Last name is required');
  }

  return errorMessages;
}

validateInputs(values) {
  const errorMessages = this.getErrorMessages(values);
  this.set('error_message', errorMessages);

  return errorMessages.length === 0;
}

让我们看看另一个示例:

const noData = data.length === 0;
$(`#${bookID}_download`).toggleClass('hidden-node', noData);
$(`#${bookID}_retry`).attr('disabled', !noData);

在这里,每次我们在代码中阅读noData时,我们都需要在心理上 unegate 才能了解真正发生的事情。负面的disabled属性使情况变得更糟。让我们修复它:

const hasData = data.length > 0;
$(`#${bookID}_download`).toggleClass(
  'hidden-node',
  hasData === false
);
$(`#${bookID}_retry`).attr('disabled', hasData);

现在阅读要容易得多。 (我们会谈论像data之类的名字。)

范围越大,名称越长

我的经验法则:变量较短,较短的范围应该是其名称。

我还可以,甚至更喜欢单线的非常短的可变名称。考虑以下两个示例:

const inputRange = Object.keys(TRANSITION).map(x => parseInt(x, 16));

const breakpoints = [
  BREAKPOINT_MOBILE,
  BREAKPOINT_TABLET,
  BREAKPOINT_DESKTOP
].map(x => `${x}px`);

在这里,每个示例中的x是什么,更长的名称会膨胀代码而不会使其更可读,可能更少。我们已经在父函数中具有全名:我们对TRANSITION对象键进行映射,并解析每个密钥;或者,我们重新映射了一个断点列表,并将它们转换为字符串。在这里我们只有一个变量,这也有帮助,因此任何简短名称都将被读为“我们重新映射的内容”。

我通常在这种情况下使用x。我认为这或多或少是占位符,而不是特定词的首字母缩写。

有些开发人员更喜欢_,这是任何不是JavaScript的编程语言的不错选择,其中_通常用于Lodash Utility库。

我还可以的另一个惯例是使用a/b名称进行分类和比较功能:

dates.sort((a, b) => new Date(a).valueOf() - new Date(b).valueOf());

但是,当范围更长或当我们有多个变量时,短名称可能会令人困惑:

const hasDiscount = customers => {
  let result = false;
  const customerIds = Object.keys(customers);
  for (let k = 0; k < customerIds.length; k++) {
    const c = customers[customerIds[k]];
    if (c.ages) {
      for (let j = 0; j < c.ages.length; j++) {
        const a = c.ages[j];
        if (a && a.customerCards.length) {
          result = true;
          break;
        }
      }
    }
    if (result) {
      break;
    }
  }
  return result;
};

在这里,完全不可能理解发生了什么,而毫无意义的名称是这样做的主要原因之一。

让我们尝试重构此代码:

const hasDiscount = customers => {
  return Object.entries(customers).some(([customerId, customer]) => {
    return customer.ages?.some(
      ageGroup => ageGroup.customerCards.length > 0
    );
  });
};

不仅重构代码短三倍,而且还要清楚得多:在任何(某些)年龄段中,是否有至少一张客户卡的(某些)客户?

我看到有人使用_名称,用于整个模块中使用的东西,甚至数十个线或数百行或代码,是一个express路由器(the example来自Express Docs,但我更改了名称):

const express = require('express');
const _ = express.Router();

// middleware that is specific to this router
_.use((req, res, next) => {
  console.log('Time: ', Date.now());
  next();
});

// define the home page route
_.get('/', (req, res) => {
  res.send('Birds home page');
});

// define the about route
_.get('/about', (req, res) => {
  res.send('About birds');
});

module.exports = _;

我无法想象这一约定背后的逻辑,而且我敢肯定,对于许多使用该代码的开发人员来说,这会造成混淆。当代码生长以做有用的事情时,情况会更糟。

让我们带回原始名称:

const express = require('express');
const router = express.Router();

// middleware that is specific to this router
router.use((req, res, next) => {
  console.log('Time: ', Date.now());
  next();
});

// define the home page route
router.get('/', (req, res) => {
  res.send('Birds home page');
});

// define the about route
router.get('/about', (req, res) => {
  res.send('About birds');
});

module.exports = router;

现在,我不难理解这里发生了什么。 (将req用于请求,而res进行响应是明确的惯例:巨大的采用使得继续使用它是一个好主意。)

so,xab几乎都是我曾经使用过的所有单字符变量名称。

另一方面,短名称中的长名使代码笨拙:

const index = purchaseOrders.findIndex(
  purchaseOrder =>
    purchaseOrder.poNumber === purchaseOrderData.poNumber
);

在这里,长名称使代码看起来比以前更复杂:

const index = purchaseOrders.findIndex(
  po => po.poNumber === purchaseOrder.poNumber
);

我认为第二版更容易阅读。

简短名称最常见的情况之一是循环:ijk是有史以来最常见的变量名称之一,通常用于存储循环索引。它们在简短的不嵌套循环中适度可读,仅是因为程序员习惯于在代码中看到它们。但是,在嵌套循环中,很难理解哪个索引属于哪个数组:

const keys = Object.keys(pizzaController);
for (let i = 0; i < keys.length; i += 1) {
  pizzaController[keys[i]].mockReset();
}

我曾经在很长一段时间内使用更长的索引变量名称:

const keys = Object.keys(pizzaController);
for (let keyIdx = 0; keyIdx < keys.length; keyIdx += 1) {
  pizzaController[keys[keyIdx]].mockReset();
}

肯定,keyIdxi更可读性,但是,幸运的是,大多数现代语言都可以让我们在不编码工匠循环的情况下迭代事物,而无需索引变量:

const keys = Object.keys(pizzaController);
keys.forEach(key => {
  pizzaController[key].mockReset();
});

(有关更多示例,请参见Avoid loops章节。)

范围越短越好

我们在上一节中谈论了一些范围。变量范围的长度也会影响可读性。范围越短,可以跟踪变量发生的事情。

极端情况是:

  • 单线功能,其中变量的范围是一行:易于遵循(例如:[8, 16].map(x => x + 'px'))。
  • 全局变量:可以在项目中的任何地方使用或修改变量,并且无法知道其在任何给定时刻所拥有的值,这通常会导致错误。这就是为什么许多开发人员几十年来一直是advocating against global variables

通常,范围越短,越好。但是,宗教范围缩短与将代码分成许多小型功能相同(请参阅划分和征服,或合并和放松章节):很容易过度使用它并使代码不那么可读,而不是更多。<<<<<<<<<<<<<<<<<<<<< /p>

我发现,降低可变作品的寿命,并且没有产生许多微小的功能。这里的想法是减少变量声明与最后一次访问的线之间的行数。范围可能是整个200行函数,但是如果特定变量的寿命为三行,那么我们只需要查看这三行即可了解如何使用此变量。

function getRelatedPosts(
  posts: { slug: string; tags: string[]; timestamp: string }[],
  { slug, tags }: { slug: string; tags: string[] }
) {
  const weighted = posts
    .filter(post => post.slug !== slug)
    .map(post => {
      const common = (post.tags || []).filter(t =>
        (tags || []).includes(t)
      );
      return {
        ...post,
        weight: common.length * Number(post.timestamp)
      };
    })
    .filter(post => post.weight > 0);

  const sorted = sortBy(weighted, 'weight').reverse();
  return sorted.slice(0, MAX_RELATED);
}

在这里,sorted变量的寿命仅为两行。这种顺序处理是该技术的常见用例。

(请参阅“避免使用pascal样式变量”部分中的一个较大示例。)

缩写与首字母缩略词

通往地狱的道路上缩写了。您认为OTC,RN,PSP,SDL是什么?我也不知道,这些只是一个项目。这就是为什么我尝试避免几乎到处缩写,而不仅仅是代码。

是针对医生开药的list of dangerous abbreviations。我们应该为程序员拥有相同的功能。

我甚至走得更远,创建了批准的缩写列表。我只能找到这样一个列表的一个例子:from Apple,我认为这可能是一个很好的开始。

常见的缩写是可以的,我们甚至都不认为其中大多数是缩写:

缩写 完整期限
alt 替代
app 应用程序
arg 参数
err 错误
info 信息
init 初始化
lat 纬度
lon 经度
max 最大
min 最小
param 参数
prev 上一个(尤其是与next配对时)

以及常见的首字母缩写:

  • HTML
  • http
  • json
  • PDF
  • RGB
  • url
  • xml

可能在项目上使用的一些非常常见的东西,但仍应记录下来(新团队成员将非常感谢!),不应该是模棱两可的。

前缀和后缀

我喜欢使用几个前缀作为变量和函数名称:

  • isarehasshould用于布尔值(示例:isPhoneNumberValidhasCancellableTickets)。
  • get用于(主要)返回值的纯函数(示例:getPageTitle)。
  • set用于存储值或反应状态的函数(示例:setProducts
  • fetch用于从后端获取数据的功能(示例:fetchMessages)。
  • to用于将数据转换为某种类型的功能(示例:toStringhexToRgburlToSlug)。
  • onhandle用于活动处理程序(示例:onClickhandleSubmit)。

我认为这些约定使代码更易于阅读,并区分返回值的功能和具有副作用的功能。

但是,不将get与其他前缀结合在一起:我经常看到getIsCompaniesFilterDisabledgetShouldShowPasswordHint之类的名称,它们应该只是isCompaniesFilterDisabledshouldShowPasswordHint,甚至更好的isCompaniesFilterEnabled。另一方面,setIsVisibleisVisible配对时非常好:

const [isVisible, setIsVisible] = useState(false);

我还为React组件做一个例外,我更喜欢跳过is前缀,类似于HTML属性,例如<button disabled>

function PayButton({ loading, onClick, id, disabled }) {
  return (
    <ButtonStyled
      id={id}
      onClick={onClick}
      loading={loading}
      disabled={disabled}
    >
      Pay now!
    </ButtonStyled>
  );
}

我不会使用get for class property accessors(甚至只读):

class User {
  #firstName;
  #lastName;

  constructor(firstName, lastName) {
    this.#firstName = firstName;
    this.#lastName = lastName;
  }

  get fullName() {
    return [this.#firstName, this.#lastName].join(' ');
  }
}

通常,我不想记得太多的规则,任何惯例都可能走得太远。一个很好的例子,幸运的是几乎被遗忘了,是一个Hungarian notation,每个名称都以其类型或意图或善良的方式前缀。例如,lAccountNum(长整数),arru8NumberList(未签名的8位整数的数组),usName(不安全字符串)。

匈牙利符号对于旧的未型语言(例如C)是有意义的,但是当您悬停在名称上时,显示出类型的现代类型语言和IDE,它会使代码缩小代码并使每个名称更难阅读。因此,保持简单。

现代前端中匈牙利符号的示例之一是带有I的打字稿界面:

interface ICoordinates {
  lat: number;
  lon: number;
}

幸运的是,大多数打字稿开发人员如今都喜欢放弃它:

interface Coordinates {
  lat: number;
  lon: number;
}

我通常会避免在其类型,类名称或名称空间中可以访问的名称中重复信息。

(我们在《代码样式》一章中谈论更多有关惯例。)

处理更新

想象一个函数,允许我们基于同一对象的先前版本构建对象的新版本:

setCount(prevCount => prevCount + 1);

在这里,我们有一个简单的计数器功能,它返回下一个计数器值。 prev前缀清楚地表明,此值已过时。

同样,当尚未应用该值并且该功能可以使我们修改或防止更新:

class ReactExample extends Component<ReactExampleProps> {
  public shouldComponentUpdate(nextProps: ReactExampleProps) {
    return this.props.code !== nextProps.code;
  }
  public render() {
    return <pre>{this.props.code}</pre>;
  }
}

在这里,我们希望避免在code发生变化时避免不必要的组件读者。 next前缀清楚地表明,此值将在shouldComponentUpdate呼叫之后应用于组件。

这两个公约都被React开发人员广泛使用。

当心不正确的名称

错误的名称比魔术数字差(在常数章节中阅读有关它们)。有了魔术数字,我们可以正确猜测,但是对于不正确的名称,我们没有机会理解代码。

考虑此示例:

// Constant used to correct a Date object's time to reflect a UTC timezone
const TIMEZONE_CORRECTION = 60000;
const getUTCDateTime = datetime =>
  new Date(
    datetime.getTime() -
      datetime.getTimezoneOffset() * TIMEZONE_CORRECTION
  );

即使是评论也没有帮助了解该代码的作用。

实际上发生的是getTime()返回毫秒和getTimezoneOffset()返回分钟,因此我们需要将分钟转换为毫秒,将分钟乘以一分钟内的毫秒数。 60000正是这个数字。

让我们纠正名称:

const MILLISECONDS_IN_MINUTE = 60000;
const getUTCDateTime = datetime =>
  new Date(
    datetime.getTime() -
      datetime.getTimezoneOffset() * MILLISECONDS_IN_MINUTE
  );

现在,它更容易理解代码。

类型(例如TypeScript)可以帮助我们查看名称何时正确表示数据:

type Order = {
  id: number;
  title: string;
};

type State = {
  filteredOrder: Order[];
  selectedOrder: number[];
};

通过查看类型,很明显,两个名称都应该是复数(它们保留数组),而第二个名称仅包含订单ID,但不包含全订单对象:

type State = {
  filteredOrders: Order[];
  selectedOrderIds: number[];
};

我们经常更改逻辑,但忘记更新名称以反映这一点。这使得理解代码更加困难,并且可能会导致错误时,我们以后更改代码并根据不正确的名称做出错误的假设。

当心抽象和不精确的名称

摘要不精确名称可能比危险更无用,例如不正确的名称。

抽象名称太通用了,无法提供有关其持有数据的任何有用信息:

  • data
  • list
  • array
  • object

此类名称的问题是任何变量都包含 data ,并且任何数组均为某物的 list 。这些名称不说它是什么样的数据,或者列表所包含的东西。本质上,此类名称比x/y/zfoo/bar/bazbazNew Folder 39Untitled 47

考虑此示例:

const currencyReducer = (state = new Currency(), action) => {
  switch (action.type) {
    case UPDATE_RESULTS:
    case UPDATE_CART:
      if (!action.res.data.query) {
        return state;
      }

      const iso = get(action, 'res.data.query.userInfo.userCurrency');
      const obj = get(action, `res.data.currencies[${iso}]`);

      return state
        .set('iso', iso)
        .set('name', get(obj, 'name'))
        .set('symbol', get(obj, 'symbol'));
    default:
      return state;
  }
};

除了使用Immutchable.js和Lodash的get方法(已经使代码很难阅读)外,obj变量使代码更加难以理解。

所有这些代码所做的一切都是将有关用户货币的数据重新组织为整洁的对象:

const currencyReducer = (state = new Currency(), action) => {
  switch (action.type) {
    case UPDATE_RESULTS:
    case UPDATE_CART:
      const { data } = action.res;
      if (data.query === undefined) {
        return state;
      }

      const iso = data.query.userInfo?.userCurrency;
      const { name = '', symbol = '' } = data.currencies[iso] || {};

      return state.merge({ iso, name, symbol });
    default:
      return state;
  }
};

现在,我们在这里构建的数据形式更清楚,甚至不变。我保留了data名称,因为这是如何来自后端的,它通常用作后端API返回的任何一种根对象。只要我们不会将其泄漏到应用程序代码,并且仅在原始后端数据的初始处理过程中使用它,就可以了。

此类名称对于通用实用程序功能也可以,例如数组过滤或排序:

function findFirstNonEmptyArray(...arrays) {
  return arrays.find(array => Array.isArray(array) && array.length > 0) || [];
}

这里的arraysarray完全很好,因为这正是它们所代表的:通用数组,我们还不知道它们要持有什么,并且在此功能的背景下,这也不重要,可能是任何东西。

不精确的名称是没有足够描述对象的名称。常见情况之一是带有数字后缀的名称。通常,发生三个原因:

  1. 我们有多种对象。
  2. 我们对对象进行了一些处理,并使用数字存储处理的对象。
  3. 我们制作了已经存在的模块,功能或组件的新版本。

在所有情况下,解决方案都是澄清每个名称。

在前两种情况下,尝试找到区分对象的东西,并使名称更加精确。

考虑此示例:

test('creates new user', async () => {
  const username = 'cosmo';

  await collections.users.insertMany(users);

  // Log in
  const cookies = await login();

  // Create user
  const response = await request(app)
    .post(usersEndpoint)
    .send({ username })
    .set('Accept', 'application/json')
    .set('Cookie', cookies);

  expect(response.headers).toHaveProperty(
    'content-type',
    expect.stringContaining('json')
  );
  expect(response.status).toBe(StatusCode.SuccessCreated);
  expect(response.body).toHaveProperty('data');
  expect(response.body.data).toEqual(
    expect.objectContaining({
      username,
      password: expect.stringMatching(/^[a-z]+-[a-z]+-[a-z]+$/)
    })
  );

  // Log in with the new user
  const response2 = await request(app)
    .post(loginEndpoint)
    .send({
      username,
      password: response.body.data.password
    })
    .set('Accept', 'application/json');

  // Fetch users
  const response3 = await request(app)
    .get(usersEndpoint)
    .set('Accept', 'application/json')
    .set('Cookie', response2.headers['set-cookie']);

  expect(response3.body).toHaveProperty('data');
  expect(response3.body.data).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ username: 'chucknorris' }),
      expect.objectContaining({ username })
    ])
  );
});

在这里,我们发送了一系列网络请求来测试REST API。但是,名称responseresponse2response3使代码很难理解,尤其是当我们使用一个请求返回的数据来创建下一个时。我们可以使名称更加精确:

test('creates new user', async () => {
  const username = 'cosmo';

  await collections.users.insertMany(users);

  // Log in
  const cookies = await login();

  // Create user
  const createRes = await request(app)
    .post(usersEndpoint)
    .send({ username })
    .set('Accept', 'application/json')
    .set('Cookie', cookies);

  expect(createRes.headers).toHaveProperty(
    'content-type',
    expect.stringContaining('json')
  );
  expect(createRes.status).toBe(StatusCode.SuccessCreated);
  expect(createRes.body).toHaveProperty('data');
  expect(createRes.body.data).toEqual(
    expect.objectContaining({
      username,
      password: expect.stringMatching(/^[a-z]+-[a-z]+-[a-z]+$/)
    })
  );

  // Log in with the new user
  const loginRes = await request(app)
    .post(loginEndpoint)
    .send({
      username,
      password: createRes.body.data.password
    })
    .set('Accept', 'application/json');

  // Fetch users
  const usersRes = await request(app)
    .get(usersEndpoint)
    .set('Accept', 'application/json')
    .set('Cookie', loginRes.headers['set-cookie']);

  expect(usersRes.body).toHaveProperty('data');
  expect(usersRes.body.data).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ username: 'chucknorris' }),
      expect.objectContaining({ username })
    ])
  );
});

现在很明显我们随时访问哪些请求数据。

对于新版本的模块,我尝试将旧版本重命名为ModuleLegacy之类的东西,而不是命名新的One Module2ModuleNew,并继续使用原始名称进行新实现。这并不总是可能的,但是它使使用旧的,弃用的,模块比新的,改进的更尴尬,正是我们想要实现的。同样,即使原始模块早已消失,名称也会永远存在。但是,当新的模块尚未达到功能齐全或测试良好时,诸如Module2ModuleNew之类的名称在开发过程中都很好。

使用常用术语

最好将著名且广泛采用的术语用于编程和域概念,而不是发明可能可爱或聪明但可能会被误解的东西。对于非母语说话的人来说,这尤其有问题 - 我们不知道许多稀有且晦涩难懂的单词。

A “great” example是反应代码库,它们使用了scry”(这意味着像通过水晶球窥视未来),而不是找到。

为每个概念使用一个术语

使用不同的单词作为同一概念令人困惑:阅读代码的人可能会认为这些单词是不同的,因此这些事物是相同的,并且会试图理解两者之间的区别。它还将使代码少 grepplable (这意味着很难找到同一件事的所有用法,请参阅《使代码grepplable》一章以获取更多信息)。

想法:拥有一个项目词典,甚至是衬里,可能是避免对相同事物使用不同单词的好主意。我使用类似的方法来编写这本书:我使用Textlint terminology plugin来确保我始终使用术语并正确拼写。

使用对面对

通常,我们创建成对的变量或函数,以执行相反的操作或保持范围相对端的值。例如,startServer/stopServerminWidth/maxWidth。当我们看到一个人时,我们希望看到另一个,我们希望它有一个名称,因为它听起来很自然(如果一个人碰巧是母语者),或者已经被我们之前的几代程序员使用。<<<<<<<<<<<< /p>

其中一些常见对是:

术语 对面
add 删除
开始 结束
创建 销毁
启用 禁用
第一个 最后
get set
增量 减少
插入 delete
解锁
最小 最大
下一个 上一个
开放 关闭
阅读
show hide
开始 停止
目标

检查您的名字的拼写

名称和评论中的错别字非常普遍。它们在大多数情况下不会导致错误 ,但仍然可以稍微降低可读性,并且编码许多典型的代码看起来很草率。

最近,我在我们的Codebase:depratureDateTime中找到了这个名称,我立即注意到了它,因为我在WebStorm编辑器中启用了Spellachecker:

Spellchecker in WebStorm

SpellChecker对我有极大的帮助,因为我不是英语的人。它还有助于使代码更加grephppher:当我们搜索特定术语时,我们可能会发现拼写错误的发生。

使用破坏性

通常,我们最终以中间值的尴尬名称,例如函数参数或函数返回值:

const duration = parseMs(durationSec * 1000);
// Then later we work with the result like so:
duration.minutes;
duration.seconds;

在这里,duration变量从来没有用作整体,仅作为我们在代码中使用的minutesseconds值的容器。通过使用破坏性,我们可以跳过中间变量:

const { minutes, seconds } = parseMs(durationSec * 1000);

现在我们可以直接访问minutesseconds

用在对象中分组的可选参数函数是另一个常见示例:

function submitFormData(
  action: string,
  options: {
    method: string;
    target: '_top';
    parameters?: { [key: string]: string };
  }
) {
  const form = document.createElement('form');

  form.method = options.method;
  form.action = action;
  form.target = options.target;

  if (options.parameters) {
    Object.keys(options.parameters)
      .map(paramName =>
        hiddenInput(paramName, options.parameters![paramName])
      )
      .forEach(form.appendChild.bind(form));
  }

  document.body.appendChild(form);
  form.submit();
  document.body.removeChild(form);
}

在这里,options对象永远不会用作整体(例如,将其传递给另一个函数),而只是访问其中的单独属性。我们可以使用破坏性来简化代码:

function submitFormData(
  action: string,
  {
    method,
    target,
    parameters
  }: {
    method: string;
    target: '_top';
    parameters?: { [key: string]: string };
  }
) {
  const form = document.createElement('form');

  form.method = method;
  form.action = action;
  form.target = target;

  if (parameters) {
    Object.keys(parameters)
      .map(paramName =>
        hiddenInput(paramName, parameters![paramName])
      )
      .forEach(form.appendChild.bind(form));
  }

  document.body.appendChild(form);
  form.submit();
  document.body.removeChild(form);
}

在这里,我们删除了options对象,该对象几乎在功能体的每一行中使用,这使其更短,更可读。

避免不必要的变量

通常,我们添加中间变量以存储某些操作的结果,然后再将其传递到其他地方或从功能中返回。在许多情况下,此变量是不必要的。

考虑以下两个示例:

const result = handleUpdateResponse(response.status);
this.setState(result);
const data = await response.json();
return data;

在这两种情况下,resultdata变量都不会为代码增加很多。名称添加了新信息,并且代码足够简短以至于被绑在一起:

this.setState(handleUpdateResponse(response.status));
      return response.json();

这是另一个示例:

render() {
  let p = this.props;
  return <BaseComponent {...p} />;
}

在这里,别名p用晦涩的名字代替了一个清晰的名字this.props。同样,内部使代码更可读:

render() {
  return <BaseComponent {...this.props} />;
}

破坏可能是另一个解决方案,请参见上面的使用破坏性部分。

有时,中间变量可以用作评论,解释其持有的数据,否则可能尚不清楚:

function Tip({ type, content }: TipProps) {
  const shouldBeWrapped = hasTextLikeOnlyChildren(content);

  return (
    <Flex alignItems="flex-start">
        {shouldBeWrapped ? <Body type={type}>{content}</Body> : content}
    </Flex>
  );
};

使用中间变量的另一个充分理由是将一条长的代码划分为多行:

const borderSvg = `<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><path d='M2 2h2v2H2zM4 0h2v2H4zM10 4h2v2h-2zM0 4h2v2H0zM6 0h2v2H6zM8 2h2v2H8zM8 8h2v2H8zM6 10h2v2H6zM0 6h2v2H0zM10 6h2v2h-2zM4 10h2v2H4zM2 8h2v2H2z' fill='%23000'/></svg>`;
const borderImage = `url("data:image/svg+xml,${borderSvg}")`;

避免姓名冲突的提示

我们谈到了如何通过更精确的名称来避免数字后缀。让我们谈论其他一些情况,我们可能会发生冲突的名字,而what can we do to avoid them

我经常在争夺名称上挣扎,原因有两个:

  1. 存储函数返回值(示例:const isCrocodile = isCrocodile())。
  2. 创建一个反应组件以显示某种打字稿类型的对象(示例:const User = (props: { user: User }) => null)。

让我们从函数返回值开始。考虑此示例:

const crocodiles = getCrocodiles({ color: 'darkolivegreen' });

在这里,很明显哪个是函数,哪个是从函数值返回的数组。现在考虑一下:

const isCrocodile = isCrocodile(crocodiles[0]);

在这里,我们的命名选择有限:

  • isCrocodile是自然的选择,但与函数名称发生冲突;
  • crocodile意味着该变量容纳了crocodiles数组的一项。

那么,我们该怎么办?几件事:

  • 选择一个特定域名的名称(示例:shouldShowGreeting);
  • 内联函数调用,完全避免局部变量;
  • 选择一个更具体的名称(示例:isFirstItemCrocodileisGreenCrocodile);
  • 缩短名称,如果范围很小(示例:isCroc)。

所有选项都不是理想的:

  • 内线可以使代码更加详细,尤其是在使用多次函数的结果或函数具有多个参数时。它也可能影响性能,尽管通常不这样做。
  • 更长的名称也可能使代码更详细。
  • 缩短名称可能会令人困惑。

我通常使用特定于域的名称或内线(对于一次或两次使用一次非常简单的呼叫):

function UserProfile({ user }) {
  const shouldShowGreeting = isCrocodile(user);
  return (
    <section>
      {shouldShowGreeting && (
        <p>Welcome back, green crocodile, the ruler of the Earth!</p>
      )}
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </section>
  );
}

在这里,名称描述了如何使用该值(特定于域的名称)来检查我们是否需要显示问候,而不是值本身>用户是否是鳄鱼。这还有另一个好处:如果我们决定改变条件,则不需要重命名变量。

例如,我们可以决定只在早上问候鳄鱼:

function UserProfile({ user, date }) {
  const shouldShowGreeting =
    isCrocodile(user) && date.getHours() < 10;
  return (
    <section>
      {shouldShowGreeting && (
        <p>Guten Morgen, green crocodile, the ruler of the Earth!</p>
      )}
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </section>
  );
}

当必须更改类似isCroc的东西时,这个名字仍然很有意义。

不幸的是,我没有一个很好的解决方案来碰撞反应组件和打字稿类型。当我们创建一个组件以渲染对象或某种类型时,通常会发生这种情况:

interface User {
  name: string;
  email: string;
}

export function User({ user }: { user: User }) {
  return <p>{user.name} ({user.email})</p>
}

尽管Typescript允许我们在同一范围中使用具有相同名称的类型和值,但它使代码令人困惑。

我看到的唯一解决方案是重命名类型或组件。我通常会尝试重命名组件,尽管它需要一些创造力才能提出一个不会混淆的名称。例如,诸如UserComponentUserView之类的名称会令人困惑,因为其他组件没有这些后缀。但是像UserProfile这样的东西在这种情况下可能有效:

interface User {
  name: string;
  email: string;
}

export function UserProfile({ user }: { user: User }) {
  return <p>{user.name} ({user.email})</p>
}

这只有在其他地方导出并重复使用的类型或组件时才重要。本地名称更宽容,因为它们仅在同一文件中使用,并且在此处进行定义。


开始思考:

  • 用正取代负面布尔值。
  • 降低变量的范围或寿命。
  • 根据其范围或寿命大小选择或多或少的特定名称。
  • 使用破坏性来少考虑发明新名称。
  • 选择本地变量而不是更多字面名称的特定域名名称。

如果您有任何反馈,mastodon metweet meopen an issue在Github上,或通过artem@sapegin.ru给我发电子邮件。 Preorder the book on Leanpubread a draft online