正在进行的勘探
面向无类JavaScript

bymanbetx官方网站多少

Ongoing Exploration: Towards class-less JavaScript

This photo was taken in特隆赫姆

我越来越相信javascript或typescript中的类是反模式的。At least in the way I use them.

I guess this will be a familiar example to you:

类PetStoreManager构造函数(Petrepository:Petrepository,account: CheckingAccount) {    this.petRepository = petRepository;this.account=账户;BubEt(姓名,price)const pet=this.petrorepository.findbyname(名称);if (!pet) throw new Error(`Pet ${name} not found!`);this.petRepository.remove(pet);this.account.addFunds(price);return pet  }}

PetStoreManageris a class,当实例化后接收依赖项,and upon calling the巴特伯method,将根据业务逻辑与这些合作者进行交互(买宠物时,add the paid price to the funds.

petRepositoryis an instance of another class:

接口Petrepository创建(名称:字符串):void findbyname(名称:字符串):pet未定义删除(pet:pet):void类文件系统Petrepository扩展Petrepository构造函数(storagedir)this.storagedir=storagedir创建(名称)写入文件(path.resolve(storagedir,`$姓名.json`),,JSON.stringify({        name      })    )  }  findByName(name) {    return JSON.parse(readFile(path.resolve(storageDir,`$name.json`))remove(pet)unlink(readfile(path.resolve(storagedir,`${pet.name}.json`)))  }}

We use these classes to achieve theS,LandDof theS.O.L.I.D.principles:

  • Single responsibility principle:我们将处理交易和存储宠物的问题分为两个单独的类,这样更容易理解它们的作用,并且简化了对代码的测试。
  • Liskov substitution principle: thePetStoreManagerclass does not care how the pets are stored,one can swap out the instance of the文件系统报告用一个mysqlperrepository数据库without needing to modify the managers implementation.ThePetStoreManageris getting passed an instance of the repository,it does not usenew ...to create an instance.这很重要,因此我们可以测试实现,并根据环境有效配置软件。
  • Dependency inversion principlePetStoreManagerdepends ona存储库接口,not on the concrete implementation.

My observation however has been that by following this pattern we eventually end up violation theSingle responsibility principlebecause classes give us and arbitrary boundary which is hard to piercebecause there is no easy and obvious way to slice a class.We end up adding more and more methods to classes who roughly interact with the same dependencies.I have seen multiple instances where class methods were added which introduced new dependencies,and they were added to the constructor:

class FileSystemPetRepository extends PetRepository {-  constructor(storageDir) {+  constructor(storageDir,storageQuota) {    this.storageDir = storageDir+   this.storageQuota = storageQuota  }  create(name) {+   this.storageQuota.hasQuota(storageDir)    writeFile(      path.resolve(storageDir,`$姓名.json`),,json.stringify(名称)

Nowcreateneeds to check if there is enough quota in the storage before adding a new entry.但是找到一个名字andremove可以保持不变,since for them the quote information is not relevant or respectively implicit.

We keep growing the class further and further and this bugs me for multiple reasons:

  • for every dependency we are also adding an import statement,构造函数参数,and a class property.This adds additional noise in the beginning of the file.
  • it gets harder to reason which method needs which dependency
  • 类文件变得越来越长,容易拉伸超过500行

What if we had no classes in the first place?

使用类的最明显原因是collaboration holders,这意味着它们提供了业务逻辑需要交互的依赖关系。There are basically no classes in my code that havestatein the classically sense: storing computation results to be reused later.Because this is hard to test I avoid having state in my classes and methods will only have local state and return results.They are already functional.

课程不是用来的,that's whyusing Classes for the purpose of dependency managementis an anti-pattern.

Now,where do we go from here?How does a better version of that code looks like,that addresses the concerns listed above:

  • only provide the dependencies for a method that are needed
  • shorter files

My current practice is to convert interfaces to TypeScript types for each method:

//petrorepository.tsexport type create=(name:string)=>voidexport type findbyname=(name:string)=>pet undefinedexport type remove=(pet:pet)=>void

The concrete implementation for e.g.the file-system backed pet repository looks like this:

// petRepository/fileSystem/remove.tsexport const remove: petRepository.remove = (pet: Pet) => {  unlink(readFile(path.resolve(storageDir,`${pet.name}.json`)))}

and convert all class methods to functions which receive the实现方式of these types.

// petstoreManager/buyPet.tsexport const buyPet: (  findPetByName: petRepository.findByName,removePet: petRepository.remove,addfunds:checkingaccount.addfunds名称:string,price: number) => {    const pet = findPetByName(name);if (!pet) throw new Error(`Pet ${name} not found!`);removePet(pet);addFunds(price);return pet  }

如果一个实现有依赖关系,但是使用者不知道这个依赖关系,那么我返回带有绑定依赖关系的实现。

Here is the example of the file-system backed pet repository methodcreatewhich needs to ask for the quota.

//storagequota.tsexport type hasquota=(location:string)=>布尔型//petrorepository/filesystem/remove.tsexport const create=(hasquota:storagequota.hasquota)=>(name:string)=>writefile(…)

当我需要一个petStore.createmethod somewhere in the code it is constructed like this:

import * as petStore from 'petStore';import { create } from 'petRepository/fileSystem/create';import { hasQuota } from 'storageQuota/fileSystem/hasQuota';const createPet: petStore.create = create(hasQuota);// The code that creats a new pet does not know // about the quota dependencycreatePet('Kitty');

这种方法解决了我列出的问题,but feels a little clunky.尤其是将依赖项传递给方法是,至少对我来说,a thing I would need to get used to.


I'd love to hear your feedback and thoughts on this topic,this is an ongoing exploration of mine,so keep checking back here or onmy Twitterfor updates.